@adeu/core 1.17.1 → 1.18.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.
@@ -0,0 +1,186 @@
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 for an edit inside a foreign insertion [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
+ const engine = new RedlineEngine(doc, "QA Tester");
55
+
56
+ // A strict edit fully contained inside a foreign insertion now applies: the
57
+ // enclosing <w:ins> is split and the change nested. Dry-run and write agree.
58
+ // (Fresh edit object per call: a dry-run mutates the edit's resolution state.)
59
+ const resDry = engine.process_batch(
60
+ [{ type: "modify", target_text: "five (5)", new_text: "seven (7)" } as any],
61
+ true,
62
+ );
63
+ expect(resDry.edits_applied).toBe(1);
64
+ expect(resDry.edits_skipped).toBe(0);
65
+ expect(resDry.edits[0].status).toBe("applied");
66
+
67
+ const resWet = engine.process_batch(
68
+ [{ type: "modify", target_text: "five (5)", new_text: "seven (7)" } as any],
69
+ false,
70
+ );
71
+ expect(resWet.edits_applied).toBe(1);
72
+ expect(resWet.edits_skipped).toBe(0);
73
+ });
74
+
75
+ it("TC3: Heading targeted by markdown '#' corrupts instead of failing [report F4, F5]", async () => {
76
+ const doc = await createTestDocument();
77
+ const xmlDoc = doc.element.ownerDocument!;
78
+
79
+ // Create a styled heading "3. Pending Review" (without literal "#" in the Word doc) using Heading1 style
80
+ const p = xmlDoc.createElement("w:p");
81
+ const pPr = xmlDoc.createElement("w:pPr");
82
+ const pStyle = xmlDoc.createElement("w:pStyle");
83
+ pStyle.setAttribute("w:val", "Heading1");
84
+ pPr.appendChild(pStyle);
85
+ p.appendChild(pPr);
86
+
87
+ const r = xmlDoc.createElement("w:r");
88
+ const t = xmlDoc.createElement("w:t");
89
+ t.textContent = "3. Pending Review";
90
+ r.appendChild(t);
91
+ p.appendChild(r);
92
+ doc.element.appendChild(p);
93
+
94
+ const engine = new RedlineEngine(doc);
95
+
96
+ // Target by its markdown representation
97
+ const res = engine.process_batch([
98
+ {
99
+ type: "modify",
100
+ target_text: "# 3. Pending Review",
101
+ new_text: "# 3. Final Review",
102
+ } as any,
103
+ ]);
104
+
105
+ expect(res.edits_applied).toBe(1);
106
+
107
+ // The correct behavior must NOT produce a redline containing a literal '#' character in the CriticMarkup preview
108
+ // or body text (such as `{--# 3. Pending--}{++# 3. Final++} Review`).
109
+ // Asserting that the critic_markup does NOT contain "{--#" or "{++#" will fail precisely due to this bug.
110
+ const criticMarkup = res.edits[0].critic_markup;
111
+ expect(criticMarkup).not.toContain("{--#");
112
+ expect(criticMarkup).not.toContain("{++#");
113
+ });
114
+
115
+ it("TC5: w16du namespace stamped on untouched parts [report F9]", async () => {
116
+ const doc = await createTestDocument();
117
+ addParagraph(doc, "This is untouched body text.");
118
+
119
+ // Inject a header part without w16du namespace
120
+ const headerXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
121
+ <w:hdr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
122
+ <w:p><w:r><w:t>Header Text</w:t></w:r></w:p>
123
+ </w:hdr>`;
124
+ const headerPart = doc.pkg.addPart(
125
+ "/word/header1.xml",
126
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
127
+ headerXml,
128
+ );
129
+ doc.relateTo(
130
+ headerPart,
131
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header",
132
+ );
133
+
134
+ const engine = new RedlineEngine(doc);
135
+ engine.process_batch([
136
+ {
137
+ type: "modify",
138
+ target_text: "untouched body",
139
+ new_text: "changed body",
140
+ } as any,
141
+ ]);
142
+
143
+ // Save the package to trigger serialization of all parts
144
+ await doc.save();
145
+
146
+ // Verify the header XML does not contain the word16du namespace
147
+ const savedHeaderXml = headerPart._element.toString();
148
+ expect(savedHeaderXml).not.toContain("word16du");
149
+ });
150
+
151
+ it("TC8: Error message mislabels edit index [report F7]", async () => {
152
+ const doc = await createTestDocument();
153
+ addParagraph(doc, "First paragraph text.");
154
+
155
+ const engine = new RedlineEngine(doc);
156
+
157
+ // Run in dry_run mode where the bug manifests.
158
+ // Edit 1 succeeds, Edit 2 fails because "Non-existent text" is not found.
159
+ // On the unpatched codebase, individual validation of edit 2 passes single_errors = validate_edits([edit]),
160
+ // which hardcodes i = 0 internally. Thus, the error in res.edits[1].error is:
161
+ // "- Edit 1 Failed: Target text not found..." instead of "- Edit 2 Failed: ...".
162
+ const res = engine.process_batch([
163
+ {
164
+ type: "modify",
165
+ target_text: "First paragraph",
166
+ new_text: "Updated first paragraph",
167
+ } as any,
168
+ {
169
+ type: "modify",
170
+ target_text: "Non-existent text",
171
+ new_text: "Failed update",
172
+ } as any,
173
+ ], true);
174
+
175
+ expect(res.edits_applied).toBe(1);
176
+ expect(res.edits_skipped).toBe(1);
177
+ expect(res.edits[1].status).toBe("failed");
178
+
179
+ // Assert that the error message correctly labels it as Edit 2.
180
+ // The buggy unpatched codebase says "Edit 1 Failed:" inside the error message for Edit 2.
181
+ const errorMsg = res.edits[1].error;
182
+ expect(errorMsg).toContain("Edit 2 Failed:");
183
+ expect(errorMsg).not.toContain("Edit 1 Failed:");
184
+ });
185
+ });
186
+
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createTestDocument, addParagraph } from "./test-utils.js";
3
+ import { RedlineEngine } from "./engine.js";
4
+ import { serializeXml } from "./docx/dom.js";
5
+
6
+ describe("Report Bug Reproductions: Boundaries and Active Insertions", () => {
7
+ it("TC_BOUNDARY: target_text spans a paragraph boundary with body text on both sides", async () => {
8
+ const doc = await createTestDocument();
9
+
10
+ // Add two paragraphs
11
+ addParagraph(doc, "First paragraph text.");
12
+ addParagraph(doc, "Second paragraph text.");
13
+
14
+ const engine = new RedlineEngine(doc);
15
+
16
+ // Target text that spans across the paragraph boundary
17
+ const edit = {
18
+ type: "modify",
19
+ target_text: "First paragraph text.\n\nSecond paragraph text.",
20
+ new_text: "First paragraph text. New.\n\nSecond paragraph text. New.",
21
+ } as any;
22
+
23
+ // On the unpatched codebase, this throws a BatchValidationError ("target_text spans a paragraph boundary").
24
+ // We assert that the batch executes successfully to replicate the failure on unpatched environments.
25
+ const result = engine.process_batch([edit], false);
26
+ expect(result.edits_applied).toBe(1);
27
+ expect(result.edits_skipped).toBe(0);
28
+ });
29
+
30
+ it("TC_CONFLICT: Modification targets an active insertion from another author", async () => {
31
+ const doc = await createTestDocument();
32
+ const xmlDoc = doc.element.ownerDocument!;
33
+
34
+ // Create a paragraph with an active insertion from another author (Supplier's Counsel)
35
+ const p = addParagraph(doc, "The party shall provide ");
36
+ const ins = xmlDoc.createElement("w:ins");
37
+ ins.setAttribute("w:id", "201");
38
+ ins.setAttribute("w:author", "Supplier's Counsel");
39
+ ins.setAttribute("w:date", "2026-06-30T08:00:00Z");
40
+
41
+ const r = xmlDoc.createElement("w:r");
42
+ const t = xmlDoc.createElement("w:t");
43
+ t.textContent = "written notice";
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 = " within 30 days.";
51
+ suffixRun.appendChild(suffixText);
52
+ p.appendChild(suffixRun);
53
+
54
+ // Engine is instantiated by Reviewer AI (a different author)
55
+ const engine = new RedlineEngine(doc, "Reviewer AI");
56
+
57
+ const edit = {
58
+ type: "modify",
59
+ target_text: "written notice",
60
+ new_text: "email notification",
61
+ } as any;
62
+
63
+ // On the unpatched codebase, this throws a BatchValidationError ("Modification targets an active insertion from another author").
64
+ // We assert that the batch executes successfully to replicate the failure on unpatched environments.
65
+ const result = engine.process_batch([edit], false);
66
+ expect(result.edits_applied).toBe(1);
67
+ expect(result.edits_skipped).toBe(0);
68
+ });
69
+
70
+ it("comment-only edit on a foreign author's insertion attaches the comment without crashing", async () => {
71
+ // Regression guard (cross-engine parity): a COMMENT_ONLY edit (new_text ==
72
+ // target_text + comment) whose target lies entirely inside another author's
73
+ // pending <w:ins> must attach the comment around that insertion, not crash.
74
+ const doc = await createTestDocument();
75
+ const xmlDoc = doc.element.ownerDocument!;
76
+
77
+ const p = addParagraph(doc, "Prefix ");
78
+ const ins = xmlDoc.createElement("w:ins");
79
+ ins.setAttribute("w:id", "9");
80
+ ins.setAttribute("w:author", "Author A");
81
+ const r = xmlDoc.createElement("w:r");
82
+ const t = xmlDoc.createElement("w:t");
83
+ t.textContent = "beta";
84
+ r.appendChild(t);
85
+ ins.appendChild(r);
86
+ p.appendChild(ins);
87
+
88
+ const engine = new RedlineEngine(doc, "Editor");
89
+ const edit = {
90
+ type: "modify",
91
+ target_text: "beta",
92
+ new_text: "beta",
93
+ comment: "flag this clause",
94
+ } as any;
95
+
96
+ const result = engine.process_batch([edit], false);
97
+ expect(result.edits_applied).toBe(1);
98
+ expect(result.edits_skipped).toBe(0);
99
+
100
+ const xml = serializeXml(p);
101
+ // The foreign insertion survives and the comment range wraps it.
102
+ expect(xml).toContain('w:author="Author A"');
103
+ expect(xml.indexOf("commentRangeStart")).toBeLessThan(
104
+ xml.indexOf('w:ins w:id="9"'),
105
+ );
106
+ expect(xml).toContain("commentRangeEnd");
107
+ });
108
+ });