@adeu/core 1.14.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/dist/index.cjs +2198 -1788
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -12
- package/dist/index.d.ts +7 -12
- package/dist/index.js +2198 -1788
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/comment_dedup.test.ts +269 -0
- package/src/domain.test.ts +280 -77
- package/src/domain.ts +146 -46
- package/src/engine.qa.test.ts +162 -0
- package/src/engine.ts +274 -89
- package/src/ingest.ts +283 -133
- package/src/mapper.ts +425 -196
- package/src/models.ts +2 -0
- package/src/repro_heading_bug.test.ts +174 -0
- package/src/search_write_engine.test.ts +112 -0
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
|
+
});
|