@adeu/core 1.19.1 → 1.21.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adeu/core",
3
- "version": "1.19.1",
3
+ "version": "1.21.0",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -57,7 +57,7 @@ describe("Resolved Bugs Core Engine Verification", () => {
57
57
  "Ambiguous match. Target text appears 4 times",
58
58
  );
59
59
  expect(caught.message).toContain("[the]"); // Ensure the matched text is bracketed
60
- expect(caught.message).toContain("Please provide more surrounding context");
60
+ expect(caught.message).toContain("Provide more surrounding context");
61
61
  });
62
62
 
63
63
  it("BUG-7: Unifies review-action and text-edit validation errors in a single pass", async () => {
@@ -0,0 +1,304 @@
1
+ // Ports of the 2026-07-17 QA report regression tests (Python
2
+ // tests/test_repro_qa_report_v4.py) for the findings that also applied to the
3
+ // Node engine: H2 (run-boundary matching), C2 (bounded report echoes),
4
+ // H1 (faithful previews), M3/M4 (insert_row cell handling and reporting).
5
+
6
+ import { describe, it, expect } from "vitest";
7
+ import {
8
+ createTestDocument,
9
+ addParagraph,
10
+ addTable,
11
+ setCellText,
12
+ } from "./test-utils.js";
13
+ import { RedlineEngine, BatchValidationError } from "./engine.js";
14
+ import { DocumentMapper } from "./mapper.js";
15
+
16
+ function addFormattedParagraph(
17
+ doc: any,
18
+ segments: [string, { bold?: boolean; italic?: boolean }][],
19
+ ): Element {
20
+ const xmlDoc = doc.element.ownerDocument!;
21
+ const p = xmlDoc.createElement("w:p");
22
+ for (const [text, fmt] of segments) {
23
+ const r = xmlDoc.createElement("w:r");
24
+ if (fmt.bold || fmt.italic) {
25
+ const rPr = xmlDoc.createElement("w:rPr");
26
+ if (fmt.bold) rPr.appendChild(xmlDoc.createElement("w:b"));
27
+ if (fmt.italic) rPr.appendChild(xmlDoc.createElement("w:i"));
28
+ r.appendChild(rPr);
29
+ }
30
+ const t = xmlDoc.createElement("w:t");
31
+ t.textContent = text;
32
+ if (text.trim() !== text) t.setAttribute("xml:space", "preserve");
33
+ r.appendChild(t);
34
+ p.appendChild(r);
35
+ }
36
+ doc.element.appendChild(p);
37
+ return p;
38
+ }
39
+
40
+ function cleanText(doc: any): string {
41
+ return new DocumentMapper(doc, true).full_text;
42
+ }
43
+
44
+ async function ndaDoc() {
45
+ const doc = await createTestDocument();
46
+ addParagraph(
47
+ doc,
48
+ "This Agreement is entered into as of January 1, 2025, by Acme Corp and Beta LLC.",
49
+ );
50
+ addParagraph(
51
+ doc,
52
+ "This Agreement shall be governed by the laws of the State of California.",
53
+ );
54
+ addParagraph(doc, "Neither party shall solicit employees of the other party.");
55
+ addParagraph(
56
+ doc,
57
+ "This Agreement shall remain in effect for a period of two (2) years.",
58
+ );
59
+ return doc;
60
+ }
61
+
62
+ describe("H2 — plain targets match across bold/italic run boundaries", () => {
63
+ async function boundaryDoc() {
64
+ const doc = await createTestDocument();
65
+ addFormattedParagraph(doc, [
66
+ ["The word ", {}],
67
+ ["Al", { bold: true }],
68
+ ["pha is the bold target.", {}],
69
+ ]);
70
+ addFormattedParagraph(doc, [
71
+ ["The word ", {}],
72
+ ["Br", { italic: true }],
73
+ ["avo is the italic target.", {}],
74
+ ]);
75
+ addFormattedParagraph(doc, [
76
+ ["The word ", {}],
77
+ ["Ch", { bold: true }],
78
+ ["arlie", { italic: true }],
79
+ [" is the mixed target.", {}],
80
+ ]);
81
+ addParagraph(doc, "The word Hotel is the control target.");
82
+ return doc;
83
+ }
84
+
85
+ it.each([
86
+ ["Alpha", "AlphaEdited"],
87
+ ["Bravo", "BravoEdited"],
88
+ ["Charlie", "CharlieEdited"],
89
+ ["Hotel", "HotelEdited"],
90
+ ])("plain target %s matches and applies", async (target, replacement) => {
91
+ const engine = new RedlineEngine(await boundaryDoc());
92
+ const stats = engine.process_batch([
93
+ { type: "modify", target_text: target, new_text: replacement } as any,
94
+ ]);
95
+ expect(stats.edits_applied).toBe(1);
96
+
97
+ engine.accept_all_revisions();
98
+ const finalText = cleanText(engine.doc);
99
+ expect(finalText).toContain(replacement);
100
+ });
101
+
102
+ it("markdown-inclusive target still matches", async () => {
103
+ const engine = new RedlineEngine(await boundaryDoc());
104
+ const stats = engine.process_batch([
105
+ { type: "modify", target_text: "**Al**pha", new_text: "AlphaEdited" } as any,
106
+ ]);
107
+ expect(stats.edits_applied).toBe(1);
108
+ });
109
+
110
+ it("boundary matches are still ambiguity-checked", async () => {
111
+ const doc = await createTestDocument();
112
+ for (let i = 0; i < 2; i++) {
113
+ addFormattedParagraph(doc, [
114
+ ["Prefix ", {}],
115
+ ["Zu", { bold: true }],
116
+ ["lu suffix.", {}],
117
+ ]);
118
+ }
119
+ const engine = new RedlineEngine(doc);
120
+ const errors = engine.validate_edits([
121
+ { type: "modify", target_text: "Zulu", new_text: "Zebra" },
122
+ ]);
123
+ expect(errors.length).toBe(1);
124
+ expect(errors[0]).toContain("Ambiguous match");
125
+ });
126
+ });
127
+
128
+ describe("C2 — oversized edit values are never echoed unbounded", () => {
129
+ it("giant new_text is truncated in the report but fully applied", async () => {
130
+ const big = "X".repeat(2_000_000);
131
+ const engine = new RedlineEngine(await ndaDoc());
132
+ const stats = engine.process_batch([
133
+ { type: "modify", target_text: "California", new_text: big } as any,
134
+ ]);
135
+
136
+ expect(stats.edits_applied).toBe(1);
137
+ const report = stats.edits[0];
138
+ expect(report.new_text.length).toBeLessThan(2_000);
139
+ expect(report.new_text).toContain("chars omitted");
140
+ expect(report.critic_markup.length).toBeLessThan(2_000);
141
+ expect(report.clean_text.length).toBeLessThan(2_000);
142
+ expect(JSON.stringify(stats).length).toBeLessThan(20_000);
143
+
144
+ // Truncation is display-only: the document receives the full value.
145
+ engine.accept_all_revisions();
146
+ expect(cleanText(engine.doc)).toContain(big);
147
+ });
148
+
149
+ it("giant target_text is truncated in the not-found error", async () => {
150
+ const big = "Y".repeat(1_000_000);
151
+ const engine = new RedlineEngine(await ndaDoc());
152
+ const errors = engine.validate_edits([
153
+ { type: "modify", target_text: big, new_text: "z" },
154
+ ]);
155
+ expect(errors.length).toBe(1);
156
+ expect(errors[0].length).toBeLessThan(2_000);
157
+ expect(errors[0].toLowerCase()).toContain("not found");
158
+ });
159
+ });
160
+
161
+ describe("H1 — previews are faithful: no scaffolding, no cross-edit bleed", () => {
162
+ it("multi-edit batch previews are clean and localized", async () => {
163
+ const engine = new RedlineEngine(await ndaDoc());
164
+ const stats = engine.process_batch([
165
+ { type: "modify", target_text: "California", new_text: "Delaware" },
166
+ {
167
+ type: "modify",
168
+ target_text: "solicit employees",
169
+ new_text: "poach employees",
170
+ },
171
+ {
172
+ type: "modify",
173
+ target_text: "two (2) years",
174
+ new_text: "five (5) years",
175
+ },
176
+ ] as any[]);
177
+ expect(stats.edits_applied).toBe(3);
178
+
179
+ for (const report of stats.edits) {
180
+ expect(report.critic_markup).not.toBeNull();
181
+ // Internal scaffolding must never leak into previews.
182
+ expect(report.critic_markup).not.toContain("[Chg:");
183
+ expect(report.critic_markup).not.toContain("{>>");
184
+ expect(report.critic_markup).not.toContain("<<}");
185
+ }
186
+
187
+ expect(stats.edits[0].critic_markup).toContain(
188
+ "{--California--}{++Delaware++}",
189
+ );
190
+
191
+ // A compound change must preview the COMPLETE logical change, not just
192
+ // its first word-diff sub-edit ("{--two--}{++five++} (2) years").
193
+ expect(stats.edits[2].critic_markup).toContain(
194
+ "{--two (2)--}{++five (5)++} years",
195
+ );
196
+ expect(stats.edits[2].clean_text).toContain("five (5) years");
197
+ expect(stats.edits[2].clean_text).not.toContain("five (2)");
198
+ });
199
+
200
+ it("dry-run previews match real previews", async () => {
201
+ const batch = () =>
202
+ [
203
+ { type: "modify", target_text: "California", new_text: "Delaware" },
204
+ {
205
+ type: "modify",
206
+ target_text: "two (2) years",
207
+ new_text: "five (5) years",
208
+ },
209
+ ] as any[];
210
+
211
+ const dry = new RedlineEngine(await ndaDoc()).process_batch(batch(), true);
212
+ const wet = new RedlineEngine(await ndaDoc()).process_batch(batch(), false);
213
+
214
+ expect(dry.edits.map((r: any) => r.critic_markup)).toEqual(
215
+ wet.edits.map((r: any) => r.critic_markup),
216
+ );
217
+ expect(dry.edits.map((r: any) => r.clean_text)).toEqual(
218
+ wet.edits.map((r: any) => r.clean_text),
219
+ );
220
+ });
221
+ });
222
+
223
+ describe("M3/M4 — insert_row cell handling and reporting", () => {
224
+ async function tableDoc() {
225
+ const doc = await createTestDocument();
226
+ addParagraph(doc, "Pricing tiers:");
227
+ const table = addTable(doc, 2, 3);
228
+ setCellText(table, 0, 0, "Plan");
229
+ setCellText(table, 0, 1, "Price");
230
+ setCellText(table, 0, 2, "Seats");
231
+ setCellText(table, 1, 0, "Starter");
232
+ setCellText(table, 1, 1, "$10");
233
+ setCellText(table, 1, 2, "5");
234
+ return doc;
235
+ }
236
+
237
+ it("overfilled cells are rejected at validation time", async () => {
238
+ const engine = new RedlineEngine(await tableDoc());
239
+ let caught: any = null;
240
+ try {
241
+ engine.process_batch([
242
+ {
243
+ type: "insert_row",
244
+ target_text: "Starter",
245
+ cells: ["A", "B", "C", "D", "E"],
246
+ } as any,
247
+ ]);
248
+ } catch (e) {
249
+ caught = e;
250
+ }
251
+ expect(caught).toBeInstanceOf(BatchValidationError);
252
+ expect(caught.message).toContain("5 cells");
253
+ expect(caught.message).toContain("3 column");
254
+ });
255
+
256
+ it("underfilled cells are padded to the table width", async () => {
257
+ const engine = new RedlineEngine(await tableDoc());
258
+ const stats = engine.process_batch([
259
+ {
260
+ type: "insert_row",
261
+ target_text: "Starter",
262
+ cells: ["OnlyTwo", "Cells"],
263
+ } as any,
264
+ ]);
265
+ expect(stats.edits_applied).toBe(1);
266
+
267
+ // The inserted row must carry exactly 3 cells (2 provided + 1 padded).
268
+ const xml = engine.doc.element.toString();
269
+ const insRowMatch = xml.match(/<w:tr><w:trPr><w:ins [^>]*\/><\/w:trPr>(.*?)<\/w:tr>/s);
270
+ expect(insRowMatch).not.toBeNull();
271
+ const cellCount = (insRowMatch![1].match(/<w:tc>/g) || []).length;
272
+ expect(cellCount).toBe(3);
273
+ });
274
+
275
+ it("row ops outside a table are rejected with a specific error", async () => {
276
+ const engine = new RedlineEngine(await tableDoc());
277
+ let caught: any = null;
278
+ try {
279
+ engine.process_batch([
280
+ {
281
+ type: "insert_row",
282
+ target_text: "Pricing tiers",
283
+ cells: ["A"],
284
+ } as any,
285
+ ]);
286
+ } catch (e) {
287
+ caught = e;
288
+ }
289
+ expect(caught).toBeInstanceOf(BatchValidationError);
290
+ expect(caught.message).toContain("not inside a table row");
291
+ });
292
+
293
+ it("report shows the inserted cells instead of an empty new_text", async () => {
294
+ const engine = new RedlineEngine(await tableDoc());
295
+ const stats = engine.process_batch([
296
+ {
297
+ type: "insert_row",
298
+ target_text: "Starter",
299
+ cells: ["Pro", "$20", "10"],
300
+ } as any,
301
+ ]);
302
+ expect(stats.edits[0].new_text).toBe("Pro | $20 | 10");
303
+ });
304
+ });
@@ -0,0 +1,140 @@
1
+ // Sequential batch semantics — cross-engine parity with the Python engine
2
+ // (QA 2026-07-17 follow-up). Batches apply SEQUENTIALLY in both modes: each
3
+ // edit is validated and applied against the document state produced by the
4
+ // edits before it (chaining), validation failures reject the batch
5
+ // transactionally, and the dry-run report mirrors the real run's outcome.
6
+
7
+ import { describe, it, expect } from "vitest";
8
+ import { createTestDocument, addParagraph } from "./test-utils.js";
9
+ import { RedlineEngine, BatchValidationError } from "./engine.js";
10
+
11
+ function chainedBatch(): any[] {
12
+ return [
13
+ {
14
+ type: "modify",
15
+ target_text: "the Recipient",
16
+ new_text: "Receiving Party",
17
+ },
18
+ {
19
+ type: "modify",
20
+ target_text: "Receiving Party",
21
+ new_text: "Disclosee",
22
+ },
23
+ ];
24
+ }
25
+
26
+ async function ndaDoc() {
27
+ const doc = await createTestDocument();
28
+ addParagraph(
29
+ doc,
30
+ "As defined in Section 1, the Recipient shall maintain confidentiality of all materials.",
31
+ );
32
+ return doc;
33
+ }
34
+
35
+ describe("Sequential batch semantics (Python parity)", () => {
36
+ it("chained batch applies in BOTH modes with identical stats", async () => {
37
+ const engine = new RedlineEngine(await ndaDoc());
38
+
39
+ const resDry = engine.process_batch(chainedBatch(), true);
40
+ expect(resDry.edits_applied).toBe(2);
41
+ expect(resDry.edits_skipped).toBe(0);
42
+ expect(resDry.edits.every((r: any) => r.status === "applied")).toBe(true);
43
+
44
+ const resWet = engine.process_batch(chainedBatch(), false);
45
+ expect(resWet.edits_applied).toBe(2);
46
+ expect(resWet.edits_skipped).toBe(0);
47
+
48
+ const xml = engine.doc.element.toString();
49
+ expect(xml).toContain("Disclosee");
50
+ });
51
+
52
+ it("dry-run mirrors transactional rejection: no edit reported applied when any fails validation", async () => {
53
+ const engine = new RedlineEngine(await ndaDoc());
54
+
55
+ const res = engine.process_batch(
56
+ [
57
+ {
58
+ type: "modify",
59
+ target_text: "the Recipient",
60
+ new_text: "Receiving Party",
61
+ },
62
+ {
63
+ type: "modify",
64
+ target_text: "Nonexistent text 123",
65
+ new_text: "x",
66
+ },
67
+ ] as any[],
68
+ true,
69
+ );
70
+
71
+ expect(res.edits_applied).toBe(0);
72
+ expect(res.edits_skipped).toBe(2);
73
+ expect(res.edits.every((r: any) => r.status === "failed")).toBe(true);
74
+ expect(res.edits[0].error).toContain("transactional");
75
+ expect(res.edits[1].error.toLowerCase()).toContain("not found");
76
+ // Labeled with the edit's true position in the batch.
77
+ expect(res.edits[1].error).toContain("Edit 2 Failed");
78
+ });
79
+
80
+ it("real run rejects the same batch transactionally and leaves the document untouched", async () => {
81
+ const engine = new RedlineEngine(await ndaDoc());
82
+
83
+ let caught: any = null;
84
+ try {
85
+ engine.process_batch(
86
+ [
87
+ {
88
+ type: "modify",
89
+ target_text: "the Recipient",
90
+ new_text: "Receiving Party",
91
+ },
92
+ {
93
+ type: "modify",
94
+ target_text: "Nonexistent text 123",
95
+ new_text: "x",
96
+ },
97
+ ] as any[],
98
+ false,
99
+ );
100
+ } catch (e) {
101
+ caught = e;
102
+ }
103
+
104
+ expect(caught).toBeInstanceOf(BatchValidationError);
105
+ expect(caught.message).toContain("Edit 2 Failed");
106
+ // Rollback: edit 1's tracked change must not survive the rejection.
107
+ const xml = engine.doc.element.toString();
108
+ expect(xml).not.toContain("Receiving Party");
109
+ });
110
+
111
+ it("validation errors after applied edits carry the sequential-contract hint", async () => {
112
+ const engine = new RedlineEngine(await ndaDoc());
113
+
114
+ let caught: any = null;
115
+ try {
116
+ engine.process_batch(
117
+ [
118
+ {
119
+ type: "modify",
120
+ target_text: "the Recipient",
121
+ new_text: "Receiving Party",
122
+ },
123
+ // Stale target: "the Recipient" was just replaced by edit 1.
124
+ {
125
+ type: "modify",
126
+ target_text: "the Recipient shall maintain",
127
+ new_text: "it shall maintain",
128
+ },
129
+ ] as any[],
130
+ false,
131
+ );
132
+ } catch (e) {
133
+ caught = e;
134
+ }
135
+
136
+ expect(caught).toBeInstanceOf(BatchValidationError);
137
+ expect(caught.message).toContain("Batches apply sequentially");
138
+ expect(caught.message).toContain("AFTER the preceding edits");
139
+ });
140
+ });