@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/dist/index.cjs +2215 -1789
- 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 +2215 -1789
- 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.bugs.test.ts +20 -0
- package/src/engine.qa.test.ts +162 -0
- package/src/engine.ts +302 -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/domain.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { DocumentObject } from
|
|
2
|
-
import { Paragraph, Run } from
|
|
3
|
-
import { iter_block_items, get_run_text } from
|
|
4
|
-
import { findAllDescendants } from
|
|
1
|
+
import { DocumentObject } from "./docx/bridge.js";
|
|
2
|
+
import { Paragraph, Run } from "./docx/primitives.js";
|
|
3
|
+
import { iter_block_items, get_run_text } from "./utils/docx.js";
|
|
4
|
+
import { findAllDescendants } from "./docx/dom.js";
|
|
5
|
+
import { findDescendantsByLocalName } from "./sanitize/transforms.js";
|
|
5
6
|
|
|
6
7
|
function boundedLevenshtein(a: string, b: string, maxDist: number = 2): number {
|
|
7
8
|
if (a === b) return 0;
|
|
@@ -22,11 +23,7 @@ function boundedLevenshtein(a: string, b: string, maxDist: number = 2): number {
|
|
|
22
23
|
let minInRow = i;
|
|
23
24
|
for (let j = 1; j <= a.length; j++) {
|
|
24
25
|
const cost = a[j - 1] === b[i - 1] ? 0 : 1;
|
|
25
|
-
const val = Math.min(
|
|
26
|
-
row[j] + 1,
|
|
27
|
-
newRow[j - 1] + 1,
|
|
28
|
-
row[j - 1] + cost
|
|
29
|
-
);
|
|
26
|
+
const val = Math.min(row[j] + 1, newRow[j - 1] + 1, row[j - 1] + cost);
|
|
30
27
|
newRow.push(val);
|
|
31
28
|
if (val < minInRow) minInRow = val;
|
|
32
29
|
}
|
|
@@ -37,8 +34,8 @@ function boundedLevenshtein(a: string, b: string, maxDist: number = 2): number {
|
|
|
37
34
|
}
|
|
38
35
|
|
|
39
36
|
function _get_paragraph_text(p: Paragraph): string {
|
|
40
|
-
let text =
|
|
41
|
-
const runs = findAllDescendants(p._element,
|
|
37
|
+
let text = "";
|
|
38
|
+
const runs = findAllDescendants(p._element, "w:r");
|
|
42
39
|
for (const r of runs) {
|
|
43
40
|
text += get_run_text(new Run(r, p));
|
|
44
41
|
}
|
|
@@ -47,14 +44,22 @@ function _get_paragraph_text(p: Paragraph): string {
|
|
|
47
44
|
|
|
48
45
|
export function extract_all_domain_metadata(
|
|
49
46
|
doc: DocumentObject,
|
|
50
|
-
base_text: string
|
|
51
|
-
): [
|
|
47
|
+
base_text: string,
|
|
48
|
+
): [
|
|
49
|
+
Record<string, { count: number }>,
|
|
50
|
+
string[],
|
|
51
|
+
Record<string, { anchored_to: string; referenced_from: string[] }>,
|
|
52
|
+
] {
|
|
52
53
|
const definitions: Record<string, { count: number }> = {};
|
|
53
54
|
const duplicates = new Set<string>();
|
|
54
|
-
const raw_anchors: Record<
|
|
55
|
+
const raw_anchors: Record<
|
|
56
|
+
string,
|
|
57
|
+
{ anchored_to: string; referenced_from: string[] }
|
|
58
|
+
> = {};
|
|
55
59
|
const raw_references: [string, string][] = [];
|
|
56
60
|
|
|
57
|
-
const leading_re =
|
|
61
|
+
const leading_re =
|
|
62
|
+
/^(?:[\d.\-()a-zA-Z]+\s*)?["“]([A-Z][A-Za-z0-9\s\-&'’]{1,60})["”]/;
|
|
58
63
|
const inline_re = /\([^)]*?["“]([A-Z][A-Za-z0-9\s\-&'’]{1,60})["”][^)]*?\)/g;
|
|
59
64
|
|
|
60
65
|
for (const item of iter_block_items(doc)) {
|
|
@@ -77,28 +82,31 @@ export function extract_all_domain_metadata(
|
|
|
77
82
|
else definitions[term] = { count: 0 };
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
const short_text = text.length > 60 ? text.substring(0, 60) +
|
|
85
|
+
const short_text = text.length > 60 ? text.substring(0, 60) + "..." : text;
|
|
81
86
|
|
|
82
|
-
const nodes = findAllDescendants(item._element,
|
|
87
|
+
const nodes = findAllDescendants(item._element, "*");
|
|
83
88
|
for (const node of nodes) {
|
|
84
|
-
if (node.tagName ===
|
|
85
|
-
const b_name = node.getAttribute(
|
|
86
|
-
if (b_name && (!b_name.startsWith(
|
|
89
|
+
if (node.tagName === "w:bookmarkStart") {
|
|
90
|
+
const b_name = node.getAttribute("w:name");
|
|
91
|
+
if (b_name && (!b_name.startsWith("_") || b_name.startsWith("_Ref"))) {
|
|
87
92
|
if (!raw_anchors[b_name]) {
|
|
88
|
-
raw_anchors[b_name] = {
|
|
93
|
+
raw_anchors[b_name] = {
|
|
94
|
+
anchored_to: short_text,
|
|
95
|
+
referenced_from: [],
|
|
96
|
+
};
|
|
89
97
|
}
|
|
90
98
|
}
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
let target: string | null = null;
|
|
94
|
-
if (node.tagName ===
|
|
95
|
-
const instr = node.getAttribute(
|
|
102
|
+
if (node.tagName === "w:fldSimple") {
|
|
103
|
+
const instr = node.getAttribute("w:instr") || "";
|
|
96
104
|
const parts = instr.trim().split(/\s+/);
|
|
97
|
-
if (parts.length > 1 && parts[0] ===
|
|
98
|
-
} else if (node.tagName ===
|
|
99
|
-
const instr = node.textContent ||
|
|
105
|
+
if (parts.length > 1 && parts[0] === "REF") target = parts[1];
|
|
106
|
+
} else if (node.tagName === "w:instrText") {
|
|
107
|
+
const instr = node.textContent || "";
|
|
100
108
|
const parts = instr.trim().split(/\s+/);
|
|
101
|
-
if (parts.length > 1 && parts[0] ===
|
|
109
|
+
if (parts.length > 1 && parts[0] === "REF") target = parts[1];
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
if (target) raw_references.push([target, short_text]);
|
|
@@ -116,9 +124,10 @@ export function extract_all_domain_metadata(
|
|
|
116
124
|
const def_keys = Object.keys(definitions);
|
|
117
125
|
if (def_keys.length > 0) {
|
|
118
126
|
const sorted_terms = def_keys.sort((a, b) => b.length - a.length);
|
|
119
|
-
const escapeRegExp = (str: string) =>
|
|
120
|
-
|
|
121
|
-
const
|
|
127
|
+
const escapeRegExp = (str: string) =>
|
|
128
|
+
str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
129
|
+
const alt = sorted_terms.map(escapeRegExp).join("|");
|
|
130
|
+
const usage_pattern = new RegExp(`(?<!["“])\\b(${alt})\\b(?![”"])`, "g");
|
|
122
131
|
|
|
123
132
|
for (const m of base_text.matchAll(usage_pattern)) {
|
|
124
133
|
const matched_term = m[1];
|
|
@@ -134,12 +143,32 @@ export function extract_all_domain_metadata(
|
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
for (const term of duplicates) {
|
|
137
|
-
diagnostics.push(
|
|
146
|
+
diagnostics.push(
|
|
147
|
+
`[Error] Duplicate Definition: '${term}' is defined multiple times.`,
|
|
148
|
+
);
|
|
138
149
|
}
|
|
139
150
|
|
|
140
151
|
const stop_words = new Set([
|
|
141
|
-
"The",
|
|
142
|
-
"
|
|
152
|
+
"The",
|
|
153
|
+
"This",
|
|
154
|
+
"That",
|
|
155
|
+
"Such",
|
|
156
|
+
"A",
|
|
157
|
+
"An",
|
|
158
|
+
"Any",
|
|
159
|
+
"All",
|
|
160
|
+
"Some",
|
|
161
|
+
"No",
|
|
162
|
+
"Every",
|
|
163
|
+
"Each",
|
|
164
|
+
"As",
|
|
165
|
+
"In",
|
|
166
|
+
"Of",
|
|
167
|
+
"For",
|
|
168
|
+
"To",
|
|
169
|
+
"On",
|
|
170
|
+
"By",
|
|
171
|
+
"With",
|
|
143
172
|
]);
|
|
144
173
|
|
|
145
174
|
const all_cap_pattern = /\b[A-Z][a-zA-Z]*(?:\s+[A-Z][a-zA-Z]*)*\b/g;
|
|
@@ -160,11 +189,12 @@ export function extract_all_domain_metadata(
|
|
|
160
189
|
const words = candidate.split(/\s+/);
|
|
161
190
|
while (words.length > 0) {
|
|
162
191
|
const first = words[0];
|
|
163
|
-
const title =
|
|
192
|
+
const title =
|
|
193
|
+
first.charAt(0).toUpperCase() + first.slice(1).toLowerCase();
|
|
164
194
|
if (stop_words.has(title)) words.shift();
|
|
165
195
|
else break;
|
|
166
196
|
}
|
|
167
|
-
candidate = words.join(
|
|
197
|
+
candidate = words.join(" ");
|
|
168
198
|
|
|
169
199
|
if (candidate.length < 4) continue;
|
|
170
200
|
if (valid_terms.has(candidate)) continue;
|
|
@@ -180,8 +210,8 @@ export function extract_all_domain_metadata(
|
|
|
180
210
|
|
|
181
211
|
for (const term of candidate_terms) {
|
|
182
212
|
if (Math.abs(candidate.length - term.length) > 2) continue;
|
|
183
|
-
if (candidate === term +
|
|
184
|
-
if (term === candidate +
|
|
213
|
+
if (candidate === term + "s" || candidate === term + "es") continue;
|
|
214
|
+
if (term === candidate + "s" || term === candidate + "es") continue;
|
|
185
215
|
|
|
186
216
|
const dist = boundedLevenshtein(candidate, term, 2);
|
|
187
217
|
if (dist === 0 || dist > 2) continue;
|
|
@@ -192,19 +222,20 @@ export function extract_all_domain_metadata(
|
|
|
192
222
|
}
|
|
193
223
|
|
|
194
224
|
if (!candidates_by_term[term]) candidates_by_term[term] = [];
|
|
195
|
-
if (!candidates_by_term[term].includes(candidate))
|
|
225
|
+
if (!candidates_by_term[term].includes(candidate))
|
|
226
|
+
candidates_by_term[term].push(candidate);
|
|
196
227
|
}
|
|
197
228
|
}
|
|
198
229
|
|
|
199
230
|
for (const [term, candidates] of Object.entries(candidates_by_term)) {
|
|
200
231
|
candidates.sort();
|
|
201
|
-
const c_str = candidates.map(c => `'${c}'`).join(
|
|
232
|
+
const c_str = candidates.map((c) => `'${c}'`).join(", ");
|
|
202
233
|
diagnostics.push(`[Info] Possible Typos for '${term}': Found ${c_str}`);
|
|
203
234
|
}
|
|
204
235
|
|
|
205
236
|
function diag_sort_key(msg: string) {
|
|
206
|
-
if (msg.startsWith(
|
|
207
|
-
if (msg.startsWith(
|
|
237
|
+
if (msg.startsWith("[Error]")) return 0;
|
|
238
|
+
if (msg.startsWith("[Warning]")) return 1;
|
|
208
239
|
return 2;
|
|
209
240
|
}
|
|
210
241
|
|
|
@@ -218,19 +249,88 @@ export function extract_all_domain_metadata(
|
|
|
218
249
|
return [definitions, diagnostics, raw_anchors];
|
|
219
250
|
}
|
|
220
251
|
|
|
221
|
-
|
|
222
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Inspects word/settings.xml for privacy flags that cause Microsoft Word to
|
|
254
|
+
* silently strip authorship metadata (w:author, w:date, w:initials) from
|
|
255
|
+
* tracked changes and comments on the next save. These flags do not affect
|
|
256
|
+
* what this engine writes — but they will scrub the engine's attribution the
|
|
257
|
+
* next time Word opens and saves the file, breaking auditability and
|
|
258
|
+
* multi-turn agent state tracking.
|
|
259
|
+
*
|
|
260
|
+
* Returns a list of human-readable warning lines (without leading bullets);
|
|
261
|
+
* empty when no relevant flag is present or all flags are explicitly disabled.
|
|
262
|
+
*/
|
|
263
|
+
export function extract_document_settings_warnings(
|
|
264
|
+
doc: DocumentObject,
|
|
265
|
+
): string[] {
|
|
266
|
+
const warnings: string[] = [];
|
|
267
|
+
const settingsPart = doc.pkg.getPartByPath("word/settings.xml");
|
|
268
|
+
if (!settingsPart) return warnings;
|
|
269
|
+
|
|
270
|
+
const isTruthy = (el: Element): boolean => {
|
|
271
|
+
// OOXML boolean rule: element present with no w:val attribute defaults to true.
|
|
272
|
+
// w:val of "0", "false", or "off" means disabled. Anything else (including
|
|
273
|
+
// "1", "true", "on") is enabled.
|
|
274
|
+
if (!el.hasAttribute("w:val")) return true;
|
|
275
|
+
const val = (el.getAttribute("w:val") || "").toLowerCase();
|
|
276
|
+
return val !== "0" && val !== "false" && val !== "off";
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const removePersonal = findDescendantsByLocalName(
|
|
280
|
+
settingsPart._element,
|
|
281
|
+
"removePersonalInformation",
|
|
282
|
+
);
|
|
283
|
+
if (removePersonal.length > 0 && isTruthy(removePersonal[0])) {
|
|
284
|
+
warnings.push(
|
|
285
|
+
"[Warning] Privacy flag `removePersonalInformation` is enabled in word/settings.xml. " +
|
|
286
|
+
"Microsoft Word will strip the `w:author`, `w:initials`, and `w:date` attributes from every tracked change and comment the next time this document is opened and saved. " +
|
|
287
|
+
"Edits made by this agent will lose attribution, breaking audit trails and any multi-turn workflow that relies on identifying prior edits.",
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const removeDateTime = findDescendantsByLocalName(
|
|
292
|
+
settingsPart._element,
|
|
293
|
+
"removeDateAndTime",
|
|
294
|
+
);
|
|
295
|
+
if (removeDateTime.length > 0 && isTruthy(removeDateTime[0])) {
|
|
296
|
+
warnings.push(
|
|
297
|
+
"[Warning] Privacy flag `removeDateAndTime` is enabled in word/settings.xml. " +
|
|
298
|
+
"Microsoft Word will strip the `w:date` attribute from every tracked change and comment the next time this document is opened and saved. " +
|
|
299
|
+
"Timestamps on this agent's edits will be lost on the next Word save.",
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return warnings;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function build_structural_appendix(
|
|
307
|
+
doc: DocumentObject,
|
|
308
|
+
base_text: string,
|
|
309
|
+
): string {
|
|
310
|
+
const [defs, diagnostics, anchors] = extract_all_domain_metadata(
|
|
311
|
+
doc,
|
|
312
|
+
base_text,
|
|
313
|
+
);
|
|
314
|
+
const settings_warnings = extract_document_settings_warnings(doc);
|
|
223
315
|
|
|
224
316
|
const lines: string[] = [
|
|
225
317
|
"\n\n---",
|
|
226
318
|
"",
|
|
227
319
|
"<!-- READONLY_BOUNDARY_START -->",
|
|
228
320
|
"# Document Structure (Read-Only)",
|
|
229
|
-
"The content below is metadata describing the document's reference structure. Do not include this section in any tracked changes or edits \u2014 it is for your context only and will be discarded on write."
|
|
321
|
+
"The content below is metadata describing the document's reference structure. Do not include this section in any tracked changes or edits \u2014 it is for your context only and will be discarded on write.",
|
|
230
322
|
];
|
|
231
323
|
|
|
232
324
|
let has_content = false;
|
|
233
325
|
|
|
326
|
+
if (settings_warnings.length > 0) {
|
|
327
|
+
has_content = true;
|
|
328
|
+
lines.push("\n## Document Settings");
|
|
329
|
+
for (const warning of settings_warnings) {
|
|
330
|
+
lines.push(`- ${warning}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
234
334
|
if (Object.keys(defs).length > 0) {
|
|
235
335
|
has_content = true;
|
|
236
336
|
lines.push("\n## Defined Terms");
|
|
@@ -259,7 +359,7 @@ export function build_structural_appendix(doc: DocumentObject, base_text: string
|
|
|
259
359
|
}
|
|
260
360
|
|
|
261
361
|
if (has_content) {
|
|
262
|
-
return lines.join(
|
|
362
|
+
return lines.join("\n");
|
|
263
363
|
}
|
|
264
364
|
return "";
|
|
265
|
-
}
|
|
365
|
+
}
|
package/src/engine.bugs.test.ts
CHANGED
|
@@ -531,4 +531,24 @@ describe("Resolved Bugs Core Engine Verification", () => {
|
|
|
531
531
|
const final_comment_parts = doc.pkg.parts.filter(p => p.contentType.includes("comments"));
|
|
532
532
|
expect(final_comment_parts.length).toBe(0);
|
|
533
533
|
});
|
|
534
|
+
|
|
535
|
+
it("Double-Serialization Core: process_batch successfully processes double-serialized JSON strings", async () => {
|
|
536
|
+
const doc = await createTestDocument();
|
|
537
|
+
addParagraph(doc, "original text");
|
|
538
|
+
const engine = new RedlineEngine(doc);
|
|
539
|
+
|
|
540
|
+
// On unpatched code, this will throw a raw TypeError: Cannot create property '_applied_status' on string
|
|
541
|
+
// On patched code, it will successfully parse and apply the modify change
|
|
542
|
+
engine.process_batch([
|
|
543
|
+
JSON.stringify({
|
|
544
|
+
type: "modify",
|
|
545
|
+
target_text: "original text",
|
|
546
|
+
new_text: "updated text",
|
|
547
|
+
}),
|
|
548
|
+
] as any);
|
|
549
|
+
|
|
550
|
+
const buf = await doc.save();
|
|
551
|
+
const text = await extractTextFromBuffer(buf, true);
|
|
552
|
+
expect(text).toContain("updated text");
|
|
553
|
+
});
|
|
534
554
|
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { RedlineEngine } from "./engine.js";
|
|
3
|
+
import { createTestDocument, addParagraph } from "./test-utils.js";
|
|
4
|
+
|
|
5
|
+
describe("QA Report V2: Engine Logic", () => {
|
|
6
|
+
it("F6: Breadcrumb resolver does not dump paragraph bodies", async () => {
|
|
7
|
+
const doc = await createTestDocument();
|
|
8
|
+
|
|
9
|
+
// Setup a heading
|
|
10
|
+
const p = addParagraph(doc, "2. Confidentiality");
|
|
11
|
+
const docEl = p.ownerDocument!;
|
|
12
|
+
const pPr = docEl.createElement("w:pPr");
|
|
13
|
+
const pStyle = docEl.createElement("w:pStyle");
|
|
14
|
+
pStyle.setAttribute("w:val", "Heading1");
|
|
15
|
+
pPr.appendChild(pStyle);
|
|
16
|
+
p.insertBefore(pPr, p.firstChild);
|
|
17
|
+
|
|
18
|
+
// Add a massive paragraph that could erroneously get caught by a greedy regex
|
|
19
|
+
addParagraph(doc, "This is a massive body paragraph ".repeat(20));
|
|
20
|
+
addParagraph(doc, "Target phrase is here.");
|
|
21
|
+
|
|
22
|
+
const engine = new RedlineEngine(doc);
|
|
23
|
+
engine.mapper["_build_map"]();
|
|
24
|
+
|
|
25
|
+
const text = engine.mapper.full_text;
|
|
26
|
+
const start_idx = text.indexOf("Target phrase");
|
|
27
|
+
|
|
28
|
+
const [path, page] = (engine as any)._get_heading_path_and_page(start_idx, text, [0, 1000]);
|
|
29
|
+
|
|
30
|
+
expect(path).toContain("2. Confidentiality");
|
|
31
|
+
expect(path).not.toContain("This is a massive body paragraph");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("F9: Empty insertion CriticMarkup artifact {++++} is suppressed", async () => {
|
|
35
|
+
const doc = await createTestDocument();
|
|
36
|
+
addParagraph(doc, "the Board of Directors");
|
|
37
|
+
const engine = new RedlineEngine(doc);
|
|
38
|
+
|
|
39
|
+
// Mock an edit that acts as a strict deletion/shrink (new_text is empty)
|
|
40
|
+
const edit = {
|
|
41
|
+
type: "modify",
|
|
42
|
+
target_text: " of Directors",
|
|
43
|
+
new_text: "",
|
|
44
|
+
_resolved_start_idx: "the Board".length,
|
|
45
|
+
_active_mapper_ref: engine.mapper
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
engine.mapper["_build_map"]();
|
|
49
|
+
|
|
50
|
+
const [critic_markup, clean_text] = (engine as any)._build_edit_context_previews(edit);
|
|
51
|
+
|
|
52
|
+
expect(critic_markup).toContain("{-- of Directors--}");
|
|
53
|
+
// The {++++} artifact shouldn't exist if new_text is empty
|
|
54
|
+
expect(critic_markup).not.toContain("{++++}");
|
|
55
|
+
expect(clean_text).not.toContain("{++++}");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("R2 & R3: all-mode page list is ascending and breadcrumb anchors on first occurrence", async () => {
|
|
59
|
+
const doc = await createTestDocument();
|
|
60
|
+
const xmlDoc = doc.element.ownerDocument!;
|
|
61
|
+
|
|
62
|
+
const p1 = addParagraph(doc, "Heading one");
|
|
63
|
+
const pPr1 = xmlDoc.createElement("w:pPr");
|
|
64
|
+
const pStyle1 = xmlDoc.createElement("w:pStyle");
|
|
65
|
+
pStyle1.setAttribute("w:val", "Heading1");
|
|
66
|
+
pPr1.appendChild(pStyle1);
|
|
67
|
+
p1.insertBefore(pPr1, p1.firstChild);
|
|
68
|
+
|
|
69
|
+
addParagraph(doc, "Occurrence one.");
|
|
70
|
+
|
|
71
|
+
const p3 = addParagraph(doc, "Heading two");
|
|
72
|
+
const pPr2 = xmlDoc.createElement("w:pPr");
|
|
73
|
+
const pStyle2 = xmlDoc.createElement("w:pStyle");
|
|
74
|
+
pStyle2.setAttribute("w:val", "Heading2");
|
|
75
|
+
pPr2.appendChild(pStyle2);
|
|
76
|
+
p3.insertBefore(pPr2, p3.firstChild);
|
|
77
|
+
|
|
78
|
+
addParagraph(doc, "Occurrence two.");
|
|
79
|
+
|
|
80
|
+
const engine = new RedlineEngine(doc);
|
|
81
|
+
const edit = {
|
|
82
|
+
type: "modify",
|
|
83
|
+
target_text: "Occurrence",
|
|
84
|
+
new_text: "Match",
|
|
85
|
+
match_mode: "all"
|
|
86
|
+
};
|
|
87
|
+
const stats = engine.process_batch([edit], false);
|
|
88
|
+
|
|
89
|
+
const report = stats.edits[0];
|
|
90
|
+
expect(report.heading_path).toContain("Heading one");
|
|
91
|
+
expect(report.heading_path).not.toContain("Heading two");
|
|
92
|
+
|
|
93
|
+
if (report.pages && report.pages.length > 1) {
|
|
94
|
+
expect(report.pages[0]).toBeLessThanOrEqual(report.pages[1]);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("§5.3.2 Transactional rollback for foreign author", async () => {
|
|
99
|
+
const doc = await createTestDocument();
|
|
100
|
+
const xmlDoc = doc.element.ownerDocument!;
|
|
101
|
+
|
|
102
|
+
const p = addParagraph(doc, "Target ");
|
|
103
|
+
const ins = xmlDoc.createElement("w:ins");
|
|
104
|
+
ins.setAttribute("w:id", "1");
|
|
105
|
+
ins.setAttribute("w:author", "Other User");
|
|
106
|
+
const r = xmlDoc.createElement("w:r");
|
|
107
|
+
const t = xmlDoc.createElement("w:t");
|
|
108
|
+
t.textContent = "word";
|
|
109
|
+
r.appendChild(t);
|
|
110
|
+
ins.appendChild(r);
|
|
111
|
+
p.appendChild(ins);
|
|
112
|
+
|
|
113
|
+
const engine = new RedlineEngine(doc);
|
|
114
|
+
const edit = {
|
|
115
|
+
type: "modify",
|
|
116
|
+
target_text: "Target word",
|
|
117
|
+
new_text: "Replaced",
|
|
118
|
+
match_mode: "all"
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
expect(() => engine.process_batch([edit], false)).toThrow(/targets an active insertion from another author/);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("§5.3.3 Double-sided paragraph merge regex rejection", async () => {
|
|
125
|
+
const doc = await createTestDocument();
|
|
126
|
+
addParagraph(doc, "First part.");
|
|
127
|
+
addParagraph(doc, "Second part.");
|
|
128
|
+
|
|
129
|
+
const engine = new RedlineEngine(doc);
|
|
130
|
+
const edit = {
|
|
131
|
+
type: "modify",
|
|
132
|
+
target_text: "part.\\n\\nSecond",
|
|
133
|
+
new_text: "merged",
|
|
134
|
+
regex: true
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
expect(() => engine.process_batch([edit], false)).toThrow(/spans a paragraph boundary with body text on both sides/);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("S1: Transactional rollback blocks all-mode edit overlapping foreign COMMENT_ONLY edit", async () => {
|
|
141
|
+
const doc = await createTestDocument();
|
|
142
|
+
addParagraph(doc, "This is constituting the Board of Directors today.");
|
|
143
|
+
|
|
144
|
+
const engineAlice = new RedlineEngine(doc, "Alice");
|
|
145
|
+
engineAlice.process_batch([{
|
|
146
|
+
type: "modify",
|
|
147
|
+
target_text: "constituting the Board of Directors",
|
|
148
|
+
new_text: "constituting the Board of Directors",
|
|
149
|
+
comment: "Alice touches this clause"
|
|
150
|
+
}]);
|
|
151
|
+
|
|
152
|
+
const engineBob = new RedlineEngine(doc, "Bob");
|
|
153
|
+
const editBob = {
|
|
154
|
+
type: "modify",
|
|
155
|
+
target_text: "the Board of Directors",
|
|
156
|
+
new_text: "the Supervisory Board",
|
|
157
|
+
match_mode: "all"
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
expect(() => engineBob.process_batch([editBob], false)).toThrow(/another author/i);
|
|
161
|
+
});
|
|
162
|
+
});
|