@adeu/core 1.13.0 → 1.15.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/src/models.ts CHANGED
@@ -3,6 +3,8 @@ export interface ModifyText {
3
3
  target_text: string;
4
4
  new_text: string;
5
5
  comment?: string | null;
6
+ match_mode?: 'strict' | 'first' | 'all';
7
+ regex?: boolean;
6
8
  _match_start_index?: number | null;
7
9
  _internal_op?: string | null;
8
10
  _active_mapper_ref?: any | null; // Typed as DocumentMapper later
@@ -0,0 +1,174 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createTestDocument, addParagraph } from "./test-utils.js";
3
+ import { extractTextFromBuffer } from "./ingest.js";
4
+ import { RedlineEngine } from "./engine.js";
5
+
6
+ describe("QA Heading Bug Repro Verification", () => {
7
+ it("TC-1: # prefixed target, no duplicate — Node fails", async () => {
8
+ const doc = await createTestDocument();
9
+
10
+ // Add Heading 1 paragraph
11
+ const p1 = doc.element.ownerDocument!.createElement('w:p');
12
+ const p1Pr = doc.element.ownerDocument!.createElement('w:pPr');
13
+ const p1Style = doc.element.ownerDocument!.createElement('w:pStyle');
14
+ p1Style.setAttribute('w:val', 'Heading1');
15
+ p1Pr.appendChild(p1Style);
16
+ p1.appendChild(p1Pr);
17
+ const r1 = doc.element.ownerDocument!.createElement('w:r');
18
+ const t1 = doc.element.ownerDocument!.createElement('w:t');
19
+ t1.textContent = '2. Confidentiality';
20
+ r1.appendChild(t1);
21
+ p1.appendChild(r1);
22
+ doc.element.appendChild(p1);
23
+
24
+ addParagraph(doc, "As defined in Section 1, the Recipient shall...");
25
+
26
+ const engine = new RedlineEngine(doc);
27
+
28
+ const res = engine.process_batch([
29
+ {
30
+ type: "modify",
31
+ target_text: "# 2. Confidentiality",
32
+ new_text: "## 2. Confidentiality"
33
+ }
34
+ ]);
35
+
36
+ // Assert that the edit was successfully applied
37
+ expect(res.edits_applied).toBe(1);
38
+ expect(res.edits_skipped).toBe(0);
39
+
40
+ // Verify style of the paragraph changed to Heading2
41
+ const pStyle = p1.getElementsByTagName('w:pStyle')[0];
42
+ expect(pStyle).toBeDefined();
43
+ expect(pStyle.getAttribute('w:val')).toBe('Heading2');
44
+ });
45
+
46
+ it("TC-2: Bare target, duplicate in body — ambiguity error expected", async () => {
47
+ const doc = await createTestDocument();
48
+
49
+ // Add Heading 1 paragraph
50
+ const p1 = doc.element.ownerDocument!.createElement('w:p');
51
+ const p1Pr = doc.element.ownerDocument!.createElement('w:pPr');
52
+ const p1Style = doc.element.ownerDocument!.createElement('w:pStyle');
53
+ p1Style.setAttribute('w:val', 'Heading1');
54
+ p1Pr.appendChild(p1Style);
55
+ p1.appendChild(p1Pr);
56
+ const r1 = doc.element.ownerDocument!.createElement('w:r');
57
+ const t1 = doc.element.ownerDocument!.createElement('w:t');
58
+ t1.textContent = '2. Confidentiality';
59
+ r1.appendChild(t1);
60
+ p1.appendChild(r1);
61
+ doc.element.appendChild(p1);
62
+
63
+ addParagraph(doc, "As defined in Section 1, the Recipient shall...");
64
+ addParagraph(doc, "Page footer notice: subject to NDA dated 2026-01-15.");
65
+ addParagraph(doc, "For further detail see section 2. Confidentiality above.");
66
+
67
+ const engine = new RedlineEngine(doc);
68
+
69
+ // Should throw BatchValidationError due to ambiguity
70
+ let caught: any = null;
71
+ try {
72
+ engine.process_batch([
73
+ {
74
+ type: "modify",
75
+ target_text: "2. Confidentiality",
76
+ new_text: "2. CONFIDENTIALITY"
77
+ }
78
+ ]);
79
+ } catch (e) {
80
+ caught = e;
81
+ }
82
+
83
+ expect(caught).toBeDefined();
84
+ expect(caught.name).toBe("BatchValidationError");
85
+ expect(caught.message).toContain("Ambiguous match");
86
+ });
87
+
88
+ it("TC-3: # prefixed target, duplicate in body — Node fails", async () => {
89
+ const doc = await createTestDocument();
90
+
91
+ // Add Heading 1 paragraph
92
+ const p1 = doc.element.ownerDocument!.createElement('w:p');
93
+ const p1Pr = doc.element.ownerDocument!.createElement('w:pPr');
94
+ const p1Style = doc.element.ownerDocument!.createElement('w:pStyle');
95
+ p1Style.setAttribute('w:val', 'Heading1');
96
+ p1Pr.appendChild(p1Style);
97
+ p1.appendChild(p1Pr);
98
+ const r1 = doc.element.ownerDocument!.createElement('w:r');
99
+ const t1 = doc.element.ownerDocument!.createElement('w:t');
100
+ t1.textContent = '2. Confidentiality';
101
+ r1.appendChild(t1);
102
+ p1.appendChild(r1);
103
+ doc.element.appendChild(p1);
104
+
105
+ addParagraph(doc, "As defined in Section 1, the Recipient shall...");
106
+ addParagraph(doc, "Page footer notice: subject to NDA dated 2026-01-15.");
107
+ addParagraph(doc, "For further detail see section 2. Confidentiality above.");
108
+
109
+ const engine = new RedlineEngine(doc);
110
+
111
+ const res = engine.process_batch([
112
+ {
113
+ type: "modify",
114
+ target_text: "# 2. Confidentiality",
115
+ new_text: "## 2. Confidentiality"
116
+ }
117
+ ]);
118
+
119
+ // Assert that the edit was successfully applied (resolving ambiguity)
120
+ expect(res.edits_applied).toBe(1);
121
+ expect(res.edits_skipped).toBe(0);
122
+
123
+ // Verify style of the paragraph changed to Heading2
124
+ const pStyle = p1.getElementsByTagName('w:pStyle')[0];
125
+ expect(pStyle).toBeDefined();
126
+ expect(pStyle.getAttribute('w:val')).toBe('Heading2');
127
+ });
128
+
129
+ it("TC-4: Bare target, no duplicate — Node matches heading but mishandles new_text", async () => {
130
+ const doc = await createTestDocument();
131
+
132
+ // Add Heading 1 paragraph
133
+ const p1 = doc.element.ownerDocument!.createElement('w:p');
134
+ const p1Pr = doc.element.ownerDocument!.createElement('w:pPr');
135
+ const p1Style = doc.element.ownerDocument!.createElement('w:pStyle');
136
+ p1Style.setAttribute('w:val', 'Heading1');
137
+ p1Pr.appendChild(p1Style);
138
+ p1.appendChild(p1Pr);
139
+ const r1 = doc.element.ownerDocument!.createElement('w:r');
140
+ const t1 = doc.element.ownerDocument!.createElement('w:t');
141
+ t1.textContent = '2. Confidentiality';
142
+ r1.appendChild(t1);
143
+ p1.appendChild(r1);
144
+ doc.element.appendChild(p1);
145
+
146
+ addParagraph(doc, "As defined in Section 1, the Recipient shall...");
147
+
148
+ const engine = new RedlineEngine(doc);
149
+
150
+ const res = engine.process_batch([
151
+ {
152
+ type: "modify",
153
+ target_text: "2. Confidentiality",
154
+ new_text: "## 2. Confidentiality"
155
+ }
156
+ ]);
157
+
158
+ expect(res.edits_applied).toBe(1);
159
+ expect(res.edits_skipped).toBe(0);
160
+
161
+ // Paragraph style should change to Heading2 (style id Heading2)
162
+ const pStyle = p1.getElementsByTagName('w:pStyle')[0];
163
+ expect(pStyle).toBeDefined();
164
+ expect(pStyle.getAttribute('w:val')).toBe('Heading2');
165
+
166
+ // Verify that the run text itself does not contain literal "##"
167
+ const textElements = p1.getElementsByTagName('w:t');
168
+ let rawText = "";
169
+ for (let j = 0; j < textElements.length; j++) {
170
+ rawText += textElements[j].textContent || "";
171
+ }
172
+ expect(rawText).not.toContain("##");
173
+ });
174
+ });
@@ -0,0 +1,112 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createTestDocument, addParagraph } from './test-utils.js';
3
+ import { RedlineEngine, BatchValidationError } from './engine.js';
4
+ import { ModifyText } from './models.js';
5
+ import { extractTextFromBuffer } from './ingest.js';
6
+
7
+ describe('Search and Targeted Write Engine', () => {
8
+
9
+ it('match_mode="strict" fails on duplicate targets', async () => {
10
+ const doc = await createTestDocument();
11
+ addParagraph(doc, "This is a repetitive clause.");
12
+ addParagraph(doc, "Some other text.");
13
+ addParagraph(doc, "This is a repetitive clause.");
14
+
15
+ const engine = new RedlineEngine(doc);
16
+
17
+ // We cast to any to bypass type checking since ModifyText doesn't have match_mode yet
18
+ const edits: any[] = [{
19
+ type: 'modify',
20
+ target_text: "This is a repetitive clause.",
21
+ new_text: "This is changed.",
22
+ match_mode: 'strict'
23
+ }];
24
+
25
+ expect(() => engine.process_batch(edits)).toThrowError(BatchValidationError);
26
+ });
27
+
28
+ it('match_mode="first" modifies only the first occurrence', async () => {
29
+ const doc = await createTestDocument();
30
+ addParagraph(doc, "This is a repetitive clause.");
31
+ addParagraph(doc, "This is a repetitive clause.");
32
+
33
+ const engine = new RedlineEngine(doc);
34
+ const edits: any[] = [{
35
+ type: 'modify',
36
+ target_text: "This is a repetitive clause.",
37
+ new_text: "This is changed.",
38
+ match_mode: 'first'
39
+ }];
40
+
41
+ const stats = engine.process_batch(edits);
42
+
43
+ // Should be applied successfully
44
+ expect(stats.edits_applied).toBe(1);
45
+
46
+ const buf = await doc.save();
47
+ const text = await extractTextFromBuffer(buf, true);
48
+
49
+ // Only one occurrence should be modified in the accepted state
50
+ const newMatches = text.match(/This is changed/g);
51
+ expect(newMatches?.length).toBe(1);
52
+
53
+ const oldMatches = text.match(/This is a repetitive clause/g);
54
+ expect(oldMatches?.length).toBe(1);
55
+ });
56
+
57
+ it('match_mode="all" modifies all occurrences', async () => {
58
+ const doc = await createTestDocument();
59
+ addParagraph(doc, "This is a repetitive clause.");
60
+ addParagraph(doc, "This is a repetitive clause.");
61
+
62
+ const engine = new RedlineEngine(doc);
63
+ const edits: any[] = [{
64
+ type: 'modify',
65
+ target_text: "This is a repetitive clause.",
66
+ new_text: "This is changed.",
67
+ match_mode: 'all'
68
+ }];
69
+
70
+ const stats = engine.process_batch(edits);
71
+
72
+ // It's still 1 edit instruction applied
73
+ expect(stats.edits_applied).toBe(1);
74
+
75
+ // The enriched report should show 2 occurrences modified
76
+ expect(stats.edits[0].occurrences_modified).toBe(2);
77
+
78
+ const buf = await doc.save();
79
+ const text = await extractTextFromBuffer(buf, true);
80
+
81
+ // Both occurrences should be modified
82
+ const newMatches = text.match(/This is changed/g);
83
+ expect(newMatches?.length).toBe(2);
84
+ });
85
+
86
+ it('supports regex replacements with RegExp engine', async () => {
87
+ const doc = await createTestDocument();
88
+ addParagraph(doc, "Item cost: $500.");
89
+ addParagraph(doc, "Item cost: $1200.");
90
+
91
+ const engine = new RedlineEngine(doc);
92
+
93
+ // Using ES2022 RegExp capture group $1
94
+ const edits: any[] = [{
95
+ type: 'modify',
96
+ target_text: "Item cost: \\$(\\d+)\\.",
97
+ new_text: "Item cost: EUR $1.",
98
+ match_mode: 'all',
99
+ regex: true
100
+ }];
101
+
102
+ const stats = engine.process_batch(edits);
103
+ expect(stats.edits_applied).toBe(1);
104
+
105
+ const buf = await doc.save();
106
+ const text = await extractTextFromBuffer(buf, true);
107
+
108
+ // Both should be correctly substituted
109
+ expect(text).toContain("EUR 500");
110
+ expect(text).toContain("EUR 1200");
111
+ });
112
+ });