@adeu/core 1.17.1 → 1.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ingest.ts CHANGED
@@ -184,13 +184,28 @@ export function extract_table(
184
184
 
185
185
  if (!first_cell) cell_cursor += 3;
186
186
 
187
- const cell_content = _extract_blocks(
187
+ let cell_content = _extract_blocks(
188
188
  cell,
189
189
  comments_map,
190
190
  cleanView,
191
191
  cell_cursor,
192
192
  paragraph_offsets,
193
193
  );
194
+ // Emit a stable, document-native anchor for this cell so empty/short
195
+ // value cells are addressable by the engine. Reuses the {#...} bookmark
196
+ // projection (already protected by validate_edit_strings and resolvable
197
+ // via the mapper). We key on the cell's first paragraph w14:paraId, which
198
+ // Word assigns and keeps stable across reads.
199
+ if (!cleanView) {
200
+ const firstP = cell._element.getElementsByTagName("w:p")[0] as
201
+ | Element
202
+ | undefined;
203
+ const paraId = firstP ? firstP.getAttribute("w14:paraId") : null;
204
+ if (paraId) {
205
+ const anchor = `{#cell:${paraId}}`;
206
+ cell_content = cell_content + anchor;
207
+ }
208
+ }
194
209
  cell_texts.push(cell_content);
195
210
  cell_cursor += cell_content.length;
196
211
  first_cell = false;
package/src/mapper.ts CHANGED
@@ -260,7 +260,29 @@ export class DocumentMapper {
260
260
  current += 3;
261
261
  }
262
262
 
263
+ const cell_start = current;
263
264
  current = this._map_blocks(cell, current);
265
+
266
+ // Parity with ingest.extract_table: emit a {#cell:<paraId>} anchor and
267
+ // bind a zero-width span to the cell's first paragraph so the engine
268
+ // can resolve "write into this cell" even when the cell is empty
269
+ // (pPr-only paragraph with no run).
270
+ if (!this.clean_view && !this.original_view) {
271
+ const firstP = cell._element.getElementsByTagName("w:p")[0] as
272
+ | Element
273
+ | undefined;
274
+ const paraId = firstP ? firstP.getAttribute("w14:paraId") : null;
275
+ if (paraId && firstP) {
276
+ // Zero-width span bound to the empty cell paragraph: gives
277
+ // get_insertion_anchor a paragraph to land on. Placed at the anchor
278
+ // token offset so resolution targets THIS cell, not a neighbour.
279
+ const cellPara = new Paragraph(firstP, cell);
280
+ this._add_virtual_text("", current, cellPara);
281
+ const anchor = `{#cell:${paraId}}`;
282
+ this._add_virtual_text(anchor, current, cellPara);
283
+ current += anchor.length;
284
+ }
285
+ }
264
286
  cells_processed += 1;
265
287
  }
266
288
 
@@ -738,7 +760,7 @@ export class DocumentMapper {
738
760
  target_text = this._replace_smart_quotes(target_text);
739
761
 
740
762
  const parts: string[] = [];
741
- const token_pattern = /(\[_+\])|(\s+)|(['"])|([.,;:\/])/g;
763
+ const token_pattern = /(\[_+\])|(\s+)|(['"])|([.,;:\/\-\[\](){}+=$?*!|#^<>\\%&@~`_])/g;
742
764
 
743
765
  let last_idx = 0;
744
766
  let match;
package/src/markup.ts CHANGED
@@ -138,7 +138,7 @@ export function _make_fuzzy_regex(target_text: string): string {
138
138
  target_text = _replace_smart_quotes(target_text);
139
139
 
140
140
  const parts: string[] = [];
141
- const token_pattern = /(_+)|(\s+)|(['"])|([.,;:\/])/g;
141
+ const token_pattern = /(_+)|(\s+)|(['"])|([.,;:\/\-\[\](){}+=$?*!|#^<>\\%&@~`])/g;
142
142
 
143
143
  // Note: JS does not support atomic groups (?>...).
144
144
  // However, because we only match markdown characters * and _,
@@ -0,0 +1,160 @@
1
+ // FILE: node/packages/core/src/repro.feedback.test.ts
2
+ //
3
+ // Regression repros for the field observations against the Node engine.
4
+ //
5
+ // STYLE: these assert the DESIRED (correct) behaviour, so a test is RED while
6
+ // the bug is present and turns GREEN once the engine is fixed. (This is the
7
+ // "isolate the bug before fixing" pattern from AI_CONTEXT.md > Testing.)
8
+ //
9
+ // Issue 1 — accept + modify the same span in one batch -> currently RED (bug)
10
+ // Issue 2 — comment-only run-shredding -> NOT a bug; see note
11
+ //
12
+ // (Issue 3 lives in node/packages/mcp-server/src/repro.feedback.test.ts.)
13
+
14
+ import { describe, it, expect } from "vitest";
15
+ import { RedlineEngine } from "./engine.js";
16
+ import { createTestDocument, addParagraph } from "./test-utils.js";
17
+
18
+ /** Append a paragraph that already contains a tracked insertion by `author`. */
19
+ function addParagraphWithForeignInsertion(
20
+ doc: any,
21
+ prefix: string,
22
+ insertedText: string,
23
+ insId: string,
24
+ author: string,
25
+ ): Element {
26
+ const x = doc.element.ownerDocument!;
27
+ const p = x.createElement("w:p");
28
+ const r0 = x.createElement("w:r");
29
+ const t0 = x.createElement("w:t");
30
+ t0.textContent = prefix;
31
+ t0.setAttribute("xml:space", "preserve");
32
+ r0.appendChild(t0);
33
+ p.appendChild(r0);
34
+
35
+ const ins = x.createElement("w:ins");
36
+ ins.setAttribute("w:id", insId);
37
+ ins.setAttribute("w:author", author);
38
+ ins.setAttribute("w:date", "2024-01-01T00:00:00Z");
39
+ const r = x.createElement("w:r");
40
+ const t = x.createElement("w:t");
41
+ t.textContent = insertedText;
42
+ r.appendChild(t);
43
+ ins.appendChild(r);
44
+ p.appendChild(ins);
45
+
46
+ doc.element.appendChild(p);
47
+ return p;
48
+ }
49
+
50
+ function countTag(el: any, tag: string): number {
51
+ let n = 0;
52
+ const walk = (node: any) => {
53
+ if (node.tagName === tag) n++;
54
+ let c = node.firstChild;
55
+ while (c) {
56
+ if (c.nodeType === 1) walk(c);
57
+ c = c.nextSibling;
58
+ }
59
+ };
60
+ walk(el);
61
+ return n;
62
+ }
63
+
64
+ describe("Field feedback repro — Node engine", () => {
65
+ // ───────────────────────────────────────────────────────────────────────
66
+ // ISSUE 1 — RED until fixed.
67
+ //
68
+ // Accepting a foreign author's insertion and then editing that now-accepted
69
+ // text, in a SINGLE batch, MUST succeed: by the time the modify runs the span
70
+ // has been accepted and is no longer foreign. Node rejects it because the
71
+ // unified up-front validation pass (engine.ts `_process_batch_internal`, the
72
+ // `validate_edits(edits)` call BEFORE `apply_review_actions`) validates the
73
+ // modify against the PRE-accept document. This test fails today and will pass
74
+ // once validation runs after the accepts are applied.
75
+ // ───────────────────────────────────────────────────────────────────────
76
+ it("Issue 1: accept + modify of the SAME foreign insertion in one batch should succeed", async () => {
77
+ const doc = await createTestDocument();
78
+ addParagraphWithForeignInsertion(
79
+ doc,
80
+ "The term is ",
81
+ "24 months",
82
+ "5",
83
+ "Supplier's Counsel",
84
+ );
85
+
86
+ const engine = new RedlineEngine(doc, "Acme's Counsel");
87
+ const batch = [
88
+ { type: "accept", target_id: "Chg:5" },
89
+ { type: "modify", target_text: "24 months", new_text: "36 months" },
90
+ ] as any[];
91
+
92
+ // DESIRED behaviour — RED on current Node (throws "active insertion from
93
+ // another author"), GREEN after the fix.
94
+ const stats = engine.process_batch(batch, false);
95
+ expect(stats.actions_applied).toBe(1);
96
+ expect(stats.edits_applied).toBe(1);
97
+ expect(stats.edits_skipped).toBe(0);
98
+ });
99
+
100
+ it("Issue 1 (control, GREEN): the same accept + modify succeeds across two batches", async () => {
101
+ // Proves the rejection above is purely a single-round-trip batching
102
+ // artifact — the content and intent are valid.
103
+ const doc = await createTestDocument();
104
+ addParagraphWithForeignInsertion(
105
+ doc,
106
+ "The term is ",
107
+ "24 months",
108
+ "5",
109
+ "Supplier's Counsel",
110
+ );
111
+
112
+ const engine = new RedlineEngine(doc, "Acme's Counsel");
113
+ const r1 = engine.process_batch([{ type: "accept", target_id: "Chg:5" }], false);
114
+ expect(r1.actions_applied).toBe(1);
115
+ const r2 = engine.process_batch(
116
+ [{ type: "modify", target_text: "24 months", new_text: "36 months" }],
117
+ false,
118
+ );
119
+ expect(r2.edits_applied).toBe(1);
120
+ });
121
+
122
+ // ───────────────────────────────────────────────────────────────────────
123
+ // ISSUE 2 — NOT a bug in the current engine, so there is nothing to fail on.
124
+ // The described mechanism (one match whose physical runs are shredded →
125
+ // comment id duplicated per run segment) does not occur: COMMENT_ONLY anchors
126
+ // a single range across the first→last run regardless of fragmentation. These
127
+ // assertions are GREEN BY DESIGN and exist as evidence for the "not
128
+ // replicable" conclusion. (The real gap — no dedicated comment-only change
129
+ // type, so comments must ride a self-replacing ModifyText — is a feature
130
+ // request, not a failing-test-able defect.)
131
+ // ───────────────────────────────────────────────────────────────────────
132
+ it("Issue 2 (GREEN, evidence): self-replacement comment over 9 fragmented runs emits ONE comment", async () => {
133
+ const doc = await createTestDocument();
134
+ const x = doc.element.ownerDocument!;
135
+ const p = x.createElement("w:p");
136
+ const words = [
137
+ "The ", "Purchase ", "Price ", "is ", "[ ] ",
138
+ "dollars ", "per ", "unit ", "total.",
139
+ ];
140
+ words.forEach((w, i) => {
141
+ if (i % 2 === 0) p.appendChild(x.createElement("w:proofErr"));
142
+ const r = x.createElement("w:r");
143
+ const t = x.createElement("w:t");
144
+ t.textContent = w;
145
+ t.setAttribute("xml:space", "preserve");
146
+ r.appendChild(t);
147
+ p.appendChild(r);
148
+ });
149
+ doc.element.appendChild(p);
150
+
151
+ const engine = new RedlineEngine(doc, "Reviewer");
152
+ const sentence = words.join("").trim();
153
+ engine.process_batch(
154
+ [{ type: "modify", target_text: sentence, new_text: sentence, comment: "Missing value" }],
155
+ false,
156
+ );
157
+
158
+ expect(countTag(doc.element, "w:commentReference")).toBe(1);
159
+ });
160
+ });
@@ -0,0 +1,130 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createTestDocument, addParagraph, addTable, setCellText } from "./test-utils.js";
3
+ import { RedlineEngine } from "./engine.js";
4
+ import { DocumentObject } from "./docx/bridge.js";
5
+
6
+ describe("Webinar Report Bug Reproductions", () => {
7
+ it("Bug 1: Diff Engine Fragility (Token-Splitting Punctuation)", async () => {
8
+ const doc = await createTestDocument();
9
+ const xmlDoc = doc.element.ownerDocument!;
10
+ const p = xmlDoc.createElement("w:p");
11
+
12
+ // Replicate MS Word fragmented runs for token-splitting punctuation: [Company Name]
13
+ // with style/formatting run fragmentation where only some runs are bold.
14
+ // This generates: **[**Company Name**]**
15
+ // The target "[Company Name]" will fail to be matched because brackets are not treated
16
+ // as fuzzy boundaries, so the fuzzy regex expects "[Company" directly without the intermediate "**".
17
+ const r1 = xmlDoc.createElement("w:r");
18
+ const rPr1 = xmlDoc.createElement("w:rPr");
19
+ rPr1.appendChild(xmlDoc.createElement("w:b"));
20
+ r1.appendChild(rPr1);
21
+ const t1 = xmlDoc.createElement("w:t");
22
+ t1.textContent = "[";
23
+ r1.appendChild(t1);
24
+ p.appendChild(r1);
25
+
26
+ const r2 = xmlDoc.createElement("w:r");
27
+ // Run 2 is NOT bold
28
+ const t2 = xmlDoc.createElement("w:t");
29
+ t2.textContent = "Company Name";
30
+ r2.appendChild(t2);
31
+ p.appendChild(r2);
32
+
33
+ const r3 = xmlDoc.createElement("w:r");
34
+ const rPr3 = xmlDoc.createElement("w:rPr");
35
+ rPr3.appendChild(xmlDoc.createElement("w:b"));
36
+ r3.appendChild(rPr3);
37
+ const t3 = xmlDoc.createElement("w:t");
38
+ t3.textContent = "]";
39
+ r3.appendChild(t3);
40
+ p.appendChild(r3);
41
+
42
+ doc.element.appendChild(p);
43
+
44
+ const engine = new RedlineEngine(doc);
45
+
46
+ // Under correct engine behavior, targeting "[Company Name]" should succeed.
47
+ // However, on the unpatched codebase, it throws a BatchValidationError (Target text not found).
48
+ const res = engine.process_batch([
49
+ {
50
+ type: "modify",
51
+ target_text: "[Company Name]",
52
+ new_text: "Google LLC",
53
+ } as any,
54
+ ]);
55
+
56
+ expect(res.edits_applied).toBe(1);
57
+ expect(res.edits_skipped).toBe(0);
58
+ });
59
+
60
+ it("Bug 2: Revision History Lockouts (Active Insertion Collisions)", async () => {
61
+ const doc = await createTestDocument();
62
+ const xmlDoc = doc.element.ownerDocument!;
63
+
64
+ const p = addParagraph(doc, "The party shall provide ");
65
+ const ins = xmlDoc.createElement("w:ins");
66
+ ins.setAttribute("w:id", "123");
67
+ ins.setAttribute("w:author", "Supplier's Counsel");
68
+ const r = xmlDoc.createElement("w:r");
69
+ const t = xmlDoc.createElement("w:t");
70
+ t.textContent = "written notice";
71
+ r.appendChild(t);
72
+ ins.appendChild(r);
73
+ p.appendChild(ins);
74
+
75
+ const suffixRun = xmlDoc.createElement("w:r");
76
+ const suffixText = xmlDoc.createElement("w:t");
77
+ suffixText.textContent = " within 30 days.";
78
+ suffixRun.appendChild(suffixText);
79
+ p.appendChild(suffixRun);
80
+
81
+ // Create an engine with a different user name to trigger co-authoring lockout
82
+ const engine = new RedlineEngine(doc, "Reviewer");
83
+
84
+ // Standard edit targeting the paragraph text containing the active insertion
85
+ const edit = {
86
+ type: "modify",
87
+ target_text: "written notice within 30 days.",
88
+ new_text: "notice within 15 business days.",
89
+ };
90
+
91
+ // Under normal collaborative conditions, this edit should succeed.
92
+ // However, on the current unpatched codebase, the dry-run engine aborts with a BatchValidationError.
93
+ const res = engine.process_batch([edit as any], true);
94
+ expect(res.edits_applied).toBe(1);
95
+ });
96
+
97
+ it("Bug 3: Table Layout Ambiguity and Markdown Loss of Fidelity", async () => {
98
+ const doc = await createTestDocument();
99
+
100
+ // Create a table with 2 rows, 4 columns
101
+ const tbl = addTable(doc, 2, 4);
102
+
103
+ // Set cell text to match identical structure: Date | | |
104
+ setCellText(tbl, 0, 0, "Date");
105
+ setCellText(tbl, 0, 1, "");
106
+ setCellText(tbl, 0, 2, "");
107
+ setCellText(tbl, 0, 3, "");
108
+
109
+ setCellText(tbl, 1, 0, "Date");
110
+ setCellText(tbl, 1, 1, "");
111
+ setCellText(tbl, 1, 2, "");
112
+ setCellText(tbl, 1, 3, "");
113
+
114
+ const buf = await doc.save();
115
+ const midDoc = await DocumentObject.load(buf);
116
+ const engine = new RedlineEngine(midDoc);
117
+
118
+ // Target one of the empty rows to apply a date.
119
+ const edit = {
120
+ type: "modify",
121
+ target_text: "Date | | | ",
122
+ new_text: "Date | June 29, 2026 | | ",
123
+ };
124
+
125
+ // Ideally, the system should allow distinct targeting of cells (e.g. preserving grid coordinates).
126
+ // But due to the coordinate-flattening design flaw, it throws "Ambiguous match".
127
+ const res = engine.process_batch([edit as any]);
128
+ expect(res.edits_applied).toBe(1);
129
+ });
130
+ });
@@ -0,0 +1,194 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createTestDocument, addParagraph } from "./test-utils.js";
3
+ import { RedlineEngine } from "./engine.js";
4
+
5
+ describe("QA Report V3 Defects Reproductions", () => {
6
+ it("TC1: Sequential batch evaluation (modify->modify chaining) [report F1]", async () => {
7
+ const doc = await createTestDocument();
8
+ addParagraph(doc, "As defined in Section 1, the Recipient shall maintain confidentiality of all materials.");
9
+ const engine = new RedlineEngine(doc);
10
+
11
+ // If sequential evaluation works (like Python), the second edit will find "Receiving Party" (the output of the first edit).
12
+ // On the current unpatched Node engine, this will FAIL and throw "Target text not found in document: Receiving Party".
13
+ const res = engine.process_batch([
14
+ {
15
+ type: "modify",
16
+ target_text: "the Recipient",
17
+ new_text: "Receiving Party",
18
+ } as any,
19
+ {
20
+ type: "modify",
21
+ target_text: "Receiving Party",
22
+ new_text: "Disclosee",
23
+ } as any,
24
+ ]);
25
+
26
+ expect(res.edits_applied).toBe(2);
27
+ expect(res.edits_skipped).toBe(0);
28
+ });
29
+
30
+ it("TC2: NODE dry-run ≠ real write (active-insertion guard) [report F2, F3]", async () => {
31
+ const doc = await createTestDocument();
32
+ const xmlDoc = doc.element.ownerDocument!;
33
+
34
+ // Create a paragraph with an active insertion by "Original Drafter"
35
+ const p = addParagraph(doc, "The party shall provide ");
36
+ const ins = xmlDoc.createElement("w:ins");
37
+ ins.setAttribute("w:id", "101");
38
+ ins.setAttribute("w:author", "Original Drafter");
39
+ ins.setAttribute("w:date", "2026-06-29T12:00:00Z");
40
+
41
+ const r = xmlDoc.createElement("w:r");
42
+ const t = xmlDoc.createElement("w:t");
43
+ t.textContent = "five (5)";
44
+ r.appendChild(t);
45
+ ins.appendChild(r);
46
+ p.appendChild(ins);
47
+
48
+ const suffixRun = xmlDoc.createElement("w:r");
49
+ const suffixText = xmlDoc.createElement("w:t");
50
+ suffixText.textContent = " years.";
51
+ suffixRun.appendChild(suffixText);
52
+ p.appendChild(suffixRun);
53
+
54
+ // Create an engine with a different user name ("QA Tester") to trigger lockout
55
+ const engine = new RedlineEngine(doc, "QA Tester");
56
+
57
+ const edit = {
58
+ type: "modify",
59
+ target_text: "five (5)",
60
+ new_text: "seven (7)",
61
+ } as any;
62
+
63
+ // Run A: dry_run = true
64
+ const resDry = engine.process_batch([edit], true);
65
+ expect(resDry.edits_applied).toBe(0);
66
+ expect(resDry.edits_skipped).toBe(1);
67
+ expect(resDry.edits[0].status).toBe("failed");
68
+ expect(resDry.edits[0].error).toContain("active insertion");
69
+
70
+ // Run B: dry_run = false
71
+ let wetRunError: any = null;
72
+ try {
73
+ engine.process_batch([edit], false);
74
+ } catch (e) {
75
+ wetRunError = e;
76
+ }
77
+
78
+ expect(wetRunError).not.toBeNull();
79
+ expect(wetRunError.name).toBe("BatchValidationError");
80
+ expect(wetRunError.message).toContain("active insertion");
81
+ });
82
+
83
+ it("TC3: Heading targeted by markdown '#' corrupts instead of failing [report F4, F5]", async () => {
84
+ const doc = await createTestDocument();
85
+ const xmlDoc = doc.element.ownerDocument!;
86
+
87
+ // Create a styled heading "3. Pending Review" (without literal "#" in the Word doc) using Heading1 style
88
+ const p = xmlDoc.createElement("w:p");
89
+ const pPr = xmlDoc.createElement("w:pPr");
90
+ const pStyle = xmlDoc.createElement("w:pStyle");
91
+ pStyle.setAttribute("w:val", "Heading1");
92
+ pPr.appendChild(pStyle);
93
+ p.appendChild(pPr);
94
+
95
+ const r = xmlDoc.createElement("w:r");
96
+ const t = xmlDoc.createElement("w:t");
97
+ t.textContent = "3. Pending Review";
98
+ r.appendChild(t);
99
+ p.appendChild(r);
100
+ doc.element.appendChild(p);
101
+
102
+ const engine = new RedlineEngine(doc);
103
+
104
+ // Target by its markdown representation
105
+ const res = engine.process_batch([
106
+ {
107
+ type: "modify",
108
+ target_text: "# 3. Pending Review",
109
+ new_text: "# 3. Final Review",
110
+ } as any,
111
+ ]);
112
+
113
+ expect(res.edits_applied).toBe(1);
114
+
115
+ // The correct behavior must NOT produce a redline containing a literal '#' character in the CriticMarkup preview
116
+ // or body text (such as `{--# 3. Pending--}{++# 3. Final++} Review`).
117
+ // Asserting that the critic_markup does NOT contain "{--#" or "{++#" will fail precisely due to this bug.
118
+ const criticMarkup = res.edits[0].critic_markup;
119
+ expect(criticMarkup).not.toContain("{--#");
120
+ expect(criticMarkup).not.toContain("{++#");
121
+ });
122
+
123
+ it("TC5: w16du namespace stamped on untouched parts [report F9]", async () => {
124
+ const doc = await createTestDocument();
125
+ addParagraph(doc, "This is untouched body text.");
126
+
127
+ // Inject a header part without w16du namespace
128
+ const headerXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
129
+ <w:hdr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
130
+ <w:p><w:r><w:t>Header Text</w:t></w:r></w:p>
131
+ </w:hdr>`;
132
+ const headerPart = doc.pkg.addPart(
133
+ "/word/header1.xml",
134
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
135
+ headerXml,
136
+ );
137
+ doc.relateTo(
138
+ headerPart,
139
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header",
140
+ );
141
+
142
+ const engine = new RedlineEngine(doc);
143
+ engine.process_batch([
144
+ {
145
+ type: "modify",
146
+ target_text: "untouched body",
147
+ new_text: "changed body",
148
+ } as any,
149
+ ]);
150
+
151
+ // Save the package to trigger serialization of all parts
152
+ await doc.save();
153
+
154
+ // Verify the header XML does not contain the word16du namespace
155
+ const savedHeaderXml = headerPart._element.toString();
156
+ expect(savedHeaderXml).not.toContain("word16du");
157
+ });
158
+
159
+ it("TC8: Error message mislabels edit index [report F7]", async () => {
160
+ const doc = await createTestDocument();
161
+ addParagraph(doc, "First paragraph text.");
162
+
163
+ const engine = new RedlineEngine(doc);
164
+
165
+ // Run in dry_run mode where the bug manifests.
166
+ // Edit 1 succeeds, Edit 2 fails because "Non-existent text" is not found.
167
+ // On the unpatched codebase, individual validation of edit 2 passes single_errors = validate_edits([edit]),
168
+ // which hardcodes i = 0 internally. Thus, the error in res.edits[1].error is:
169
+ // "- Edit 1 Failed: Target text not found..." instead of "- Edit 2 Failed: ...".
170
+ const res = engine.process_batch([
171
+ {
172
+ type: "modify",
173
+ target_text: "First paragraph",
174
+ new_text: "Updated first paragraph",
175
+ } as any,
176
+ {
177
+ type: "modify",
178
+ target_text: "Non-existent text",
179
+ new_text: "Failed update",
180
+ } as any,
181
+ ], true);
182
+
183
+ expect(res.edits_applied).toBe(1);
184
+ expect(res.edits_skipped).toBe(1);
185
+ expect(res.edits[1].status).toBe("failed");
186
+
187
+ // Assert that the error message correctly labels it as Edit 2.
188
+ // The buggy unpatched codebase says "Edit 1 Failed:" inside the error message for Edit 2.
189
+ const errorMsg = res.edits[1].error;
190
+ expect(errorMsg).toContain("Edit 2 Failed:");
191
+ expect(errorMsg).not.toContain("Edit 1 Failed:");
192
+ });
193
+ });
194
+