@ansonlai/docx-redline-js 0.4.0 → 0.5.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/AGENTS.md +589 -287
- package/ARCHITECTURE.md +215 -9
- package/CHANGELOG.md +319 -0
- package/README.md +604 -360
- package/adapters/config.js +45 -43
- package/bin/docx-redline.js +3 -0
- package/core/list-targeting.js +101 -110
- package/core/paragraph-targeting.js +501 -61
- package/core/paragraph-text.js +209 -0
- package/core/revision-cloning.js +38 -0
- package/core/types.js +64 -10
- package/core/word-xml.js +43 -15
- package/dist/docx-redline-js.esm.js +2849 -466
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +87 -76
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/TESTING.md +342 -23
- package/docs/plans/2026-09-05-structural-revisions-and-fidelity-oracles.md +1669 -0
- package/docs/plans/completed/2026-09-01-performance-and-complexity-reduction.md +669 -0
- package/docs/plans/completed/2026-09-03-agent-friendly-document-workflows.md +427 -0
- package/docs/plans/completed/2026-09-04-comment-anchor-and-cli-reliability.md +519 -0
- package/docs/plans/completed/PERFORMANCE-CONSOLIDATION.md +69 -0
- package/docs/plans/completed/structural-revision-capability-matrix.md +115 -0
- package/docs/schemas/document-operations.schema.json +109 -0
- package/docs/test-comparison-dashboard.html +4250 -7
- package/engine/formatting-removal.js +11 -2
- package/engine/oxml-engine.js +491 -336
- package/engine/reconstruction-mode.js +15 -14
- package/engine/reconstruction-writer.js +247 -142
- package/engine/route-selection.js +35 -0
- package/engine/rpr-helpers.js +334 -35
- package/engine/run-builders.js +239 -196
- package/engine/surgical-diff-application.js +222 -37
- package/engine/surgical-mode.js +134 -6
- package/engine/surgical-spans.js +52 -1
- package/engine/table-cell-context.js +3 -6
- package/engine/table-mode.js +1 -1
- package/index.d.ts +234 -6
- package/index.js +24 -1
- package/node/cli.js +317 -0
- package/node/docx-document.js +302 -0
- package/node/index.d.ts +31 -0
- package/node/index.js +2 -0
- package/node/zip-archive.js +52 -0
- package/orchestration/list-markdown.js +10 -16
- package/orchestration/list-parsing.js +7 -12
- package/orchestration/list-structural-fallback.js +21 -10
- package/package.json +24 -3
- package/pipeline/content-analysis.js +12 -17
- package/pipeline/ingestion-export.js +3 -31
- package/pipeline/ingestion-paragraph.js +10 -5
- package/pipeline/list-generation.js +150 -55
- package/pipeline/list-markers.js +70 -3
- package/pipeline/serialization.js +4 -2
- package/pipeline/structured-content.js +160 -0
- package/scripts/apply_changes.mjs +27 -0
- package/scripts/benchmark-operation-session.mjs +137 -0
- package/scripts/benchmark-targeting-browser.html +74 -0
- package/scripts/benchmark-targeting-hot-paths.mjs +67 -0
- package/scripts/benchmark-test-runner.mjs +59 -0
- package/scripts/build-test-dashboard.mjs +23 -0
- package/scripts/export-lane1-fixtures.mjs +380 -0
- package/scripts/export-reredline-stress-fixtures.mjs +317 -0
- package/scripts/export-validation-fixtures.mjs +1 -1
- package/scripts/extract_text.mjs +7 -0
- package/scripts/generate-paragraph-boundary-fixtures.ps1 +215 -0
- package/scripts/generate-test-dashboard.mjs +362 -11
- package/scripts/lib/word-coverage-catalogue.mjs +6 -2
- package/scripts/profile-route-selection.mjs +19 -0
- package/scripts/render-agenda-multilevel.mjs +0 -5
- package/scripts/render-multilevel-cases.mjs +0 -1
- package/scripts/run-tests.mjs +107 -35
- package/scripts/word-com-corpus-suite.ps1 +3 -0
- package/scripts/word-com-differential.ps1 +64 -4
- package/scripts/word-com-suite.ps1 +3 -0
- package/services/batch-operation-orchestrator.js +494 -0
- package/services/capture-engine.js +226 -0
- package/services/comment-builders.js +23 -6
- package/services/comment-engine.js +108 -47
- package/services/comment-locator.js +187 -82
- package/services/comment-replies.js +95 -0
- package/services/document-inspection.js +258 -0
- package/services/document-operation-applier.js +372 -0
- package/services/document-operation-contract.js +323 -0
- package/services/document-operation-mutations.js +1733 -0
- package/services/document-operation-session.js +258 -0
- package/services/numbering-service.js +14 -5
- package/services/operation-heuristics.js +173 -0
- package/services/operation-preflight.js +366 -0
- package/services/receipt-collector.js +288 -0
- package/services/revision-comment-management.js +37 -5
- package/services/revision-token.js +290 -0
- package/services/standalone-docx-plumbing.js +123 -8
- package/services/standalone-operation-runner.d.ts +296 -0
- package/services/standalone-operation-runner.js +10 -1455
- package/services/table-reconciliation.js +15 -6
- package/docs/VALIDATION.md +0 -183
- package/docs/WORD-MANUAL-REVIEW.md +0 -138
- package/docs/plans/2026-09-01-performance-and-complexity-reduction.md +0 -210
- /package/docs/plans/{2026-08-30-reliability-testing-improvements.md → completed/2026-08-30-reliability-testing-improvements.md} +0 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import '../tests/setup-xml-provider.mjs';
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import { dirname, join, resolve } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
acceptTrackedChangesInOoxml,
|
|
9
|
+
rejectTrackedChangesInOoxml
|
|
10
|
+
} from '../index.js';
|
|
11
|
+
import { openDocx } from '../node/index.js';
|
|
12
|
+
import { buildZip } from './lib/minimal-zip.mjs';
|
|
13
|
+
import { unzipEntries } from './lib/zip-reader.mjs';
|
|
14
|
+
import {
|
|
15
|
+
CORPUS_ID as SMOKING_CORPUS_ID,
|
|
16
|
+
LEGAL_MARKUP_OPERATIONS as SMOKING_LEGAL_OPERATIONS,
|
|
17
|
+
loadSourceDocxBuffer as loadSmokingDocxBuffer
|
|
18
|
+
} from '../tests/smoking_cessation_mixed_batch_tests.mjs';
|
|
19
|
+
import {
|
|
20
|
+
CORPUS_ID as INTERAGENCY_CORPUS_ID,
|
|
21
|
+
AUTHOR_1 as INTERAGENCY_AUTHOR_1,
|
|
22
|
+
AUTHOR_2 as INTERAGENCY_AUTHOR_2,
|
|
23
|
+
AUTHOR_1_PASS_1_OPERATIONS as INTERAGENCY_AUTHOR_1_PASS_1_OPS,
|
|
24
|
+
AUTHOR_1_ALTERATION_OPERATIONS as INTERAGENCY_AUTHOR_1_ALT_OPS,
|
|
25
|
+
AUTHOR_2_OPERATIONS as INTERAGENCY_AUTHOR_2_OPS,
|
|
26
|
+
AUTHOR_1_FINAL_OPERATIONS as INTERAGENCY_AUTHOR_1_FINAL_OPS,
|
|
27
|
+
loadSourceDocxBuffer as loadInteragencyDocxBuffer
|
|
28
|
+
} from '../tests/interagency_agreement_multi_author_tests.mjs';
|
|
29
|
+
|
|
30
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const repoRoot = dirname(scriptDir);
|
|
32
|
+
|
|
33
|
+
function argumentPath(name, fallback) {
|
|
34
|
+
const index = process.argv.indexOf(name);
|
|
35
|
+
if (index < 0) return fallback;
|
|
36
|
+
if (!process.argv[index + 1]) throw new Error(`${name} requires a path`);
|
|
37
|
+
return resolve(process.cwd(), process.argv[index + 1]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const outputDir = argumentPath('--output-dir', join(repoRoot, 'tmp', 'lane1-docx'));
|
|
41
|
+
mkdirSync(outputDir, { recursive: true });
|
|
42
|
+
|
|
43
|
+
function packageWithPartXml(sourceEntries, partName, partXml) {
|
|
44
|
+
return buildZip(Array.from(sourceEntries, ([name, data]) => ({
|
|
45
|
+
name,
|
|
46
|
+
data: name === partName ? Buffer.from(partXml, 'utf8') : data
|
|
47
|
+
})));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const lane1Cases = [];
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Case 1: Smoking Cessation Services Agreement (NHS / Pharmacy Consortium)
|
|
54
|
+
// ============================================================================
|
|
55
|
+
console.log('Exporting Lane 1 Case: Smoking Cessation Contract Review...');
|
|
56
|
+
const smokingSourceBuffer = await loadSmokingDocxBuffer();
|
|
57
|
+
const smokingDoc = openDocx(smokingSourceBuffer);
|
|
58
|
+
const smokingAuthor = 'Senior Legal Counsel (NHS/Pharmacy Consortium)';
|
|
59
|
+
const smokingResult = await smokingDoc.applyOperations(SMOKING_LEGAL_OPERATIONS, {
|
|
60
|
+
author: smokingAuthor,
|
|
61
|
+
atomic: true,
|
|
62
|
+
validate: true
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!smokingResult.written) {
|
|
66
|
+
throw new Error(`Failed to apply smoking cessation legal markup: ${smokingResult.error?.message}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const smokingTrackedBuffer = smokingResult.toBuffer();
|
|
70
|
+
const smokingTrackedEntries = unzipEntries(smokingTrackedBuffer);
|
|
71
|
+
const smokingTrackedXml = smokingTrackedEntries.get('word/document.xml').toString('utf8');
|
|
72
|
+
|
|
73
|
+
const smokingAccepted = acceptTrackedChangesInOoxml(smokingTrackedXml, { allAuthors: true });
|
|
74
|
+
const smokingAcceptedBuffer = packageWithPartXml(smokingTrackedEntries, 'word/document.xml', smokingAccepted.oxml);
|
|
75
|
+
|
|
76
|
+
const smokingRejected = rejectTrackedChangesInOoxml(smokingTrackedXml, { allAuthors: true });
|
|
77
|
+
const smokingRejectedBuffer = packageWithPartXml(smokingTrackedEntries, 'word/document.xml', smokingRejected.oxml);
|
|
78
|
+
|
|
79
|
+
const smokingCaseName = 'smoking-cessation-contract-review';
|
|
80
|
+
writeFileSync(join(outputDir, `${smokingCaseName}.source.docx`), smokingSourceBuffer);
|
|
81
|
+
writeFileSync(join(outputDir, `${smokingCaseName}.docx`), smokingTrackedBuffer);
|
|
82
|
+
writeFileSync(join(outputDir, `${smokingCaseName}.accepted.docx`), smokingAcceptedBuffer);
|
|
83
|
+
writeFileSync(join(outputDir, `${smokingCaseName}.rejected.docx`), smokingRejectedBuffer);
|
|
84
|
+
writeFileSync(join(outputDir, `${smokingCaseName}.document.xml`), smokingTrackedXml);
|
|
85
|
+
|
|
86
|
+
const smokingComments = SMOKING_LEGAL_OPERATIONS
|
|
87
|
+
.filter(op => op.type === 'comment')
|
|
88
|
+
.map((op, idx) => ({
|
|
89
|
+
id: idx + 1,
|
|
90
|
+
author: op.author || smokingAuthor,
|
|
91
|
+
target: typeof op.target === 'string' ? op.target : op.target.exactText,
|
|
92
|
+
anchor: op.textToComment,
|
|
93
|
+
content: op.commentContent
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
const smokingOperationsList = SMOKING_LEGAL_OPERATIONS.map((op, idx) => ({
|
|
97
|
+
index: idx + 1,
|
|
98
|
+
type: op.type,
|
|
99
|
+
target: typeof op.target === 'string' ? op.target : op.target.exactText,
|
|
100
|
+
change: op.type === 'comment'
|
|
101
|
+
? `[Comment on "${op.textToComment}"] ${op.commentContent}`
|
|
102
|
+
: op.type === 'replace'
|
|
103
|
+
? op.modified
|
|
104
|
+
: `Format: ${JSON.stringify(op.format)}`,
|
|
105
|
+
author: op.author || smokingAuthor,
|
|
106
|
+
status: 'applied'
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
const smokingMeta = {
|
|
110
|
+
identity: `lane1:${smokingCaseName}`,
|
|
111
|
+
name: smokingCaseName,
|
|
112
|
+
title: 'Smoking Cessation Services Agreement (NHS / Pharmacy Consortium)',
|
|
113
|
+
category: 'legal',
|
|
114
|
+
sourceDocument: 'Smoking_Cessation_Contract_for_2020_-_2021_website.docx',
|
|
115
|
+
sourceId: SMOKING_CORPUS_ID,
|
|
116
|
+
sourceWords: 10770,
|
|
117
|
+
sourceParagraphs: 533,
|
|
118
|
+
author: smokingAuthor,
|
|
119
|
+
description: 'Comprehensive 28-operation real-world legal markup of Southampton City Council NHS smoking cessation contract: recitals, multi-paragraph clauses, liability & indemnity caps, GDPR statutory updates, termination notice floors, Schedule 1 clinical protocol CO ppm monitoring, Schedule 1 Table 3 dates, and Schedule 1 Table 5 fee tariffs.',
|
|
120
|
+
operationsCount: SMOKING_LEGAL_OPERATIONS.length,
|
|
121
|
+
breakdown: {
|
|
122
|
+
redlines: SMOKING_LEGAL_OPERATIONS.filter(o => o.type === 'replace').length,
|
|
123
|
+
comments: SMOKING_LEGAL_OPERATIONS.filter(o => o.type === 'comment').length,
|
|
124
|
+
formatting: SMOKING_LEGAL_OPERATIONS.filter(o => o.type === 'format').length
|
|
125
|
+
},
|
|
126
|
+
comments: smokingComments,
|
|
127
|
+
operations: smokingOperationsList,
|
|
128
|
+
revisions: {
|
|
129
|
+
insertions: (smokingTrackedXml.match(/<w:ins\b/g) || []).length,
|
|
130
|
+
deletions: (smokingTrackedXml.match(/<w:del\b/g) || []).length,
|
|
131
|
+
formatting: (smokingTrackedXml.match(/<w:(?:rPrChange|pPrChange)\b/g) || []).length,
|
|
132
|
+
comments: (smokingTrackedXml.match(/<w:commentRangeStart\b/g) || []).length
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
writeFileSync(join(outputDir, `${smokingCaseName}.meta.json`), JSON.stringify(smokingMeta, null, 2));
|
|
137
|
+
lane1Cases.push(smokingMeta);
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// Case 2: County Advisory Board Bylaws (Symbol Bullets & Governance)
|
|
141
|
+
// ============================================================================
|
|
142
|
+
const bylawsDocId = 'd27fe5513ca474bcd6c02136d2e4b8179203f5855c4333af81bc964755c7abe4';
|
|
143
|
+
const bylawsPath = join(repoRoot, 'tmp', 'superdoc-corpus', `${bylawsDocId}.docx`);
|
|
144
|
+
|
|
145
|
+
if (existsSync(bylawsPath)) {
|
|
146
|
+
console.log('Exporting Lane 1 Case: County Advisory Board Bylaws...');
|
|
147
|
+
const bylawsSourceBuffer = readFileSync(bylawsPath);
|
|
148
|
+
const bylawsDoc = openDocx(bylawsSourceBuffer);
|
|
149
|
+
const bylawsAuthor = 'BylawsCommittee';
|
|
150
|
+
const bylawsOps = [
|
|
151
|
+
{
|
|
152
|
+
type: 'replace',
|
|
153
|
+
target: { exactText: 'One (1) representative from the Board of Supervisors;' },
|
|
154
|
+
modified: 'Two (2) voting representatives from the County Board of Supervisors;',
|
|
155
|
+
author: bylawsAuthor
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
type: 'comment',
|
|
159
|
+
target: { exactText: 'Five (5) representatives from the incorporated cities/town;' },
|
|
160
|
+
textToComment: 'incorporated cities/town',
|
|
161
|
+
commentContent: 'Confirm municipal apportionment following 2024 census.',
|
|
162
|
+
author: bylawsAuthor
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
type: 'replace',
|
|
166
|
+
target: { exactText: 'Three (3) representatives from the community at large.' },
|
|
167
|
+
modified: 'Three (3) representatives from the community at large, including at least one youth advocate.',
|
|
168
|
+
author: bylawsAuthor
|
|
169
|
+
}
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
const bylawsResult = await bylawsDoc.applyOperations(bylawsOps, {
|
|
173
|
+
author: bylawsAuthor,
|
|
174
|
+
atomic: true,
|
|
175
|
+
validate: true
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (bylawsResult.written) {
|
|
179
|
+
const bylawsTrackedBuffer = bylawsResult.toBuffer();
|
|
180
|
+
const bylawsTrackedEntries = unzipEntries(bylawsTrackedBuffer);
|
|
181
|
+
const bylawsTrackedXml = bylawsTrackedEntries.get('word/document.xml').toString('utf8');
|
|
182
|
+
|
|
183
|
+
const bylawsAccepted = acceptTrackedChangesInOoxml(bylawsTrackedXml, { allAuthors: true });
|
|
184
|
+
const bylawsAcceptedBuffer = packageWithPartXml(bylawsTrackedEntries, 'word/document.xml', bylawsAccepted.oxml);
|
|
185
|
+
|
|
186
|
+
const bylawsRejected = rejectTrackedChangesInOoxml(bylawsTrackedXml, { allAuthors: true });
|
|
187
|
+
const bylawsRejectedBuffer = packageWithPartXml(bylawsTrackedEntries, 'word/document.xml', bylawsRejected.oxml);
|
|
188
|
+
|
|
189
|
+
const bylawsCaseName = 'bylaws-symbol-bullets';
|
|
190
|
+
writeFileSync(join(outputDir, `${bylawsCaseName}.source.docx`), bylawsSourceBuffer);
|
|
191
|
+
writeFileSync(join(outputDir, `${bylawsCaseName}.docx`), bylawsTrackedBuffer);
|
|
192
|
+
writeFileSync(join(outputDir, `${bylawsCaseName}.accepted.docx`), bylawsAcceptedBuffer);
|
|
193
|
+
writeFileSync(join(outputDir, `${bylawsCaseName}.rejected.docx`), bylawsRejectedBuffer);
|
|
194
|
+
writeFileSync(join(outputDir, `${bylawsCaseName}.document.xml`), bylawsTrackedXml);
|
|
195
|
+
|
|
196
|
+
const bylawsComments = bylawsOps
|
|
197
|
+
.filter(op => op.type === 'comment')
|
|
198
|
+
.map((op, idx) => ({
|
|
199
|
+
id: idx + 1,
|
|
200
|
+
author: op.author || bylawsAuthor,
|
|
201
|
+
target: op.target.exactText,
|
|
202
|
+
anchor: op.textToComment,
|
|
203
|
+
content: op.commentContent
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
const bylawsOperationsList = bylawsOps.map((op, idx) => ({
|
|
207
|
+
index: idx + 1,
|
|
208
|
+
type: op.type,
|
|
209
|
+
target: op.target.exactText,
|
|
210
|
+
change: op.type === 'comment'
|
|
211
|
+
? `[Comment on "${op.textToComment}"] ${op.commentContent}`
|
|
212
|
+
: op.modified,
|
|
213
|
+
author: op.author || bylawsAuthor,
|
|
214
|
+
status: 'applied'
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
const bylawsMeta = {
|
|
218
|
+
identity: `lane1:${bylawsCaseName}`,
|
|
219
|
+
name: bylawsCaseName,
|
|
220
|
+
title: 'County Advisory Board Bylaws (Governance & Symbol Bullets)',
|
|
221
|
+
category: 'administrative',
|
|
222
|
+
sourceDocument: `${bylawsDocId}.docx`,
|
|
223
|
+
sourceId: bylawsDocId,
|
|
224
|
+
author: bylawsAuthor,
|
|
225
|
+
description: 'Governance updates to Advisory Board membership: supervisor apportionment, census-based municipal representation comments, and youth advocacy inclusion.',
|
|
226
|
+
operationsCount: bylawsOps.length,
|
|
227
|
+
breakdown: {
|
|
228
|
+
redlines: bylawsOps.filter(o => o.type === 'replace').length,
|
|
229
|
+
comments: bylawsOps.filter(o => o.type === 'comment').length,
|
|
230
|
+
formatting: 0
|
|
231
|
+
},
|
|
232
|
+
comments: bylawsComments,
|
|
233
|
+
operations: bylawsOperationsList,
|
|
234
|
+
revisions: {
|
|
235
|
+
insertions: (bylawsTrackedXml.match(/<w:ins\b/g) || []).length,
|
|
236
|
+
deletions: (bylawsTrackedXml.match(/<w:del\b/g) || []).length,
|
|
237
|
+
formatting: (bylawsTrackedXml.match(/<w:(?:rPrChange|pPrChange)\b/g) || []).length,
|
|
238
|
+
comments: (bylawsTrackedXml.match(/<w:commentRangeStart\b/g) || []).length
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
writeFileSync(join(outputDir, `${bylawsCaseName}.meta.json`), JSON.stringify(bylawsMeta, null, 2));
|
|
243
|
+
lane1Cases.push(bylawsMeta);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ============================================================================
|
|
248
|
+
// Case 3: New Interagency Agreement (Multi-Author Negotiation: BCHD vs. MOHS)
|
|
249
|
+
// ============================================================================
|
|
250
|
+
console.log('Exporting Lane 1 Case: New Interagency Agreement Multi-Author Negotiation...');
|
|
251
|
+
const interagencySourceBuffer = await loadInteragencyDocxBuffer();
|
|
252
|
+
|
|
253
|
+
// Pass 1: BCHD Lead Agency Counsel Initial Draft (clean headers, no bullets)
|
|
254
|
+
const interagencyDoc1 = openDocx(interagencySourceBuffer);
|
|
255
|
+
const interagencyRes1 = await interagencyDoc1.applyOperations(INTERAGENCY_AUTHOR_1_PASS_1_OPS, {
|
|
256
|
+
author: INTERAGENCY_AUTHOR_1,
|
|
257
|
+
atomic: true,
|
|
258
|
+
validate: true
|
|
259
|
+
});
|
|
260
|
+
if (!interagencyRes1.written) {
|
|
261
|
+
throw new Error(`Failed to apply interagency Pass 1 legal markup: ${interagencyRes1.error?.message}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Pass 1b: BCHD Revision Alteration via Same-Author Merge (Term 36m -> 24m, Payment 45d -> 30d)
|
|
265
|
+
const interagencyDoc1b = openDocx(interagencyRes1.toBuffer());
|
|
266
|
+
const interagencyRes1b = await interagencyDoc1b.applyOperations(INTERAGENCY_AUTHOR_1_ALT_OPS, {
|
|
267
|
+
author: INTERAGENCY_AUTHOR_1,
|
|
268
|
+
atomic: true,
|
|
269
|
+
validate: true
|
|
270
|
+
});
|
|
271
|
+
if (!interagencyRes1b.written) {
|
|
272
|
+
throw new Error(`Failed to apply interagency Pass 1b alteration markup: ${interagencyRes1b.error?.message}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Pass 2: MOHS Counterparty Counsel Review (counters on Section E, adds cure period, pre-award, Attachments 1 & 2)
|
|
276
|
+
const interagencyDoc2 = openDocx(interagencyRes1b.toBuffer());
|
|
277
|
+
const interagencyRes2 = await interagencyDoc2.applyOperations(INTERAGENCY_AUTHOR_2_OPS, {
|
|
278
|
+
author: INTERAGENCY_AUTHOR_2,
|
|
279
|
+
atomic: true,
|
|
280
|
+
validate: true
|
|
281
|
+
});
|
|
282
|
+
if (!interagencyRes2.written) {
|
|
283
|
+
throw new Error(`Failed to apply interagency Pass 2 counterparty markup: ${interagencyRes2.error?.message}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Pass 3: BCHD Lead Agency Counsel Compromise on Section E (counters back on SAME provision: 60d + safeguard)
|
|
287
|
+
const interagencyDoc3 = openDocx(interagencyRes2.toBuffer());
|
|
288
|
+
const interagencyRes3 = await interagencyDoc3.applyOperations(INTERAGENCY_AUTHOR_1_FINAL_OPS, {
|
|
289
|
+
author: INTERAGENCY_AUTHOR_1,
|
|
290
|
+
atomic: true,
|
|
291
|
+
validate: true
|
|
292
|
+
});
|
|
293
|
+
if (!interagencyRes3.written) {
|
|
294
|
+
throw new Error(`Failed to apply interagency Pass 3 compromise markup: ${interagencyRes3.error?.message}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const interagencyTrackedBuffer = interagencyRes3.toBuffer();
|
|
298
|
+
const interagencyTrackedEntries = unzipEntries(interagencyTrackedBuffer);
|
|
299
|
+
const interagencyTrackedXml = interagencyTrackedEntries.get('word/document.xml').toString('utf8');
|
|
300
|
+
|
|
301
|
+
const interagencyAccepted = acceptTrackedChangesInOoxml(interagencyTrackedXml, { allAuthors: true });
|
|
302
|
+
const interagencyAcceptedBuffer = packageWithPartXml(interagencyTrackedEntries, 'word/document.xml', interagencyAccepted.oxml);
|
|
303
|
+
|
|
304
|
+
const interagencyRejected = rejectTrackedChangesInOoxml(interagencyTrackedXml, { allAuthors: true });
|
|
305
|
+
const interagencyRejectedBuffer = packageWithPartXml(interagencyTrackedEntries, 'word/document.xml', interagencyRejected.oxml);
|
|
306
|
+
|
|
307
|
+
const interagencyCaseName = 'interagency-agreement-multi-author';
|
|
308
|
+
writeFileSync(join(outputDir, `${interagencyCaseName}.source.docx`), interagencySourceBuffer);
|
|
309
|
+
writeFileSync(join(outputDir, `${interagencyCaseName}.docx`), interagencyTrackedBuffer);
|
|
310
|
+
writeFileSync(join(outputDir, `${interagencyCaseName}.accepted.docx`), interagencyAcceptedBuffer);
|
|
311
|
+
writeFileSync(join(outputDir, `${interagencyCaseName}.rejected.docx`), interagencyRejectedBuffer);
|
|
312
|
+
writeFileSync(join(outputDir, `${interagencyCaseName}.document.xml`), interagencyTrackedXml);
|
|
313
|
+
|
|
314
|
+
const allInteragencyOps = [
|
|
315
|
+
...INTERAGENCY_AUTHOR_1_PASS_1_OPS.map(op => ({ ...op, author: op.author || INTERAGENCY_AUTHOR_1 })),
|
|
316
|
+
...INTERAGENCY_AUTHOR_1_ALT_OPS.map(op => ({ ...op, author: op.author || INTERAGENCY_AUTHOR_1 })),
|
|
317
|
+
...INTERAGENCY_AUTHOR_2_OPS.map(op => ({ ...op, author: op.author || INTERAGENCY_AUTHOR_2 })),
|
|
318
|
+
...INTERAGENCY_AUTHOR_1_FINAL_OPS.map(op => ({ ...op, author: op.author || INTERAGENCY_AUTHOR_1 }))
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
const interagencyComments = allInteragencyOps
|
|
322
|
+
.filter(op => op.type === 'comment')
|
|
323
|
+
.map((op, idx) => ({
|
|
324
|
+
id: idx + 1,
|
|
325
|
+
author: op.author,
|
|
326
|
+
target: typeof op.target === 'string' ? op.target : op.target.exactText,
|
|
327
|
+
anchor: op.textToComment,
|
|
328
|
+
content: op.commentContent
|
|
329
|
+
}));
|
|
330
|
+
|
|
331
|
+
const interagencyOperationsList = allInteragencyOps.map((op, idx) => ({
|
|
332
|
+
index: idx + 1,
|
|
333
|
+
type: op.type,
|
|
334
|
+
target: typeof op.target === 'string' ? op.target : op.target.exactText,
|
|
335
|
+
change: op.type === 'comment'
|
|
336
|
+
? `[Comment on "${op.textToComment}"] ${op.commentContent}`
|
|
337
|
+
: op.modified,
|
|
338
|
+
author: op.author,
|
|
339
|
+
status: 'applied'
|
|
340
|
+
}));
|
|
341
|
+
|
|
342
|
+
const interagencyMeta = {
|
|
343
|
+
identity: `lane1:${interagencyCaseName}`,
|
|
344
|
+
name: interagencyCaseName,
|
|
345
|
+
title: 'New Interagency Agreement (Multi-Author Negotiation: BCHD vs. MOHS)',
|
|
346
|
+
category: 'legal',
|
|
347
|
+
sourceDocument: 'New Interagency Agreement.docx',
|
|
348
|
+
sourceId: INTERAGENCY_CORPUS_ID,
|
|
349
|
+
sourceWords: 1726,
|
|
350
|
+
sourceParagraphs: 173,
|
|
351
|
+
authors: [INTERAGENCY_AUTHOR_1, INTERAGENCY_AUTHOR_2],
|
|
352
|
+
author: `${INTERAGENCY_AUTHOR_1} & ${INTERAGENCY_AUTHOR_2}`,
|
|
353
|
+
description: 'Four-round woven negotiation on Baltimore City interagency healthcare & homelessness accord: (1) BCHD Lead Agency Counsel establishes clean legal section headers A-G (not bullets), programmatic terms ($2.45M), Attachment 3 sub-bullets, and Attachment 4 joint public announcement with tables & lists; (2) BCHD alters its initial redlines in a separate step (Term 36m -> 24m, Payment 45d -> 30d) via same-author merge; (3) MOHS Counterparty Counsel counters on the same termination provision (45 days + wind-down) and adds 30-day cure period, pre-award costs, annual review, and counter-comments; (4) BCHD responds back on the same termination provision reaching compromise (60 days + shelter safeguard).',
|
|
354
|
+
operationsCount: allInteragencyOps.length,
|
|
355
|
+
breakdown: {
|
|
356
|
+
redlines: allInteragencyOps.filter(o => o.type === 'replace').length,
|
|
357
|
+
comments: allInteragencyOps.filter(o => o.type === 'comment').length,
|
|
358
|
+
formatting: 0
|
|
359
|
+
},
|
|
360
|
+
comments: interagencyComments,
|
|
361
|
+
operations: interagencyOperationsList,
|
|
362
|
+
revisions: {
|
|
363
|
+
insertions: (interagencyTrackedXml.match(/<w:ins\b/g) || []).length,
|
|
364
|
+
deletions: (interagencyTrackedXml.match(/<w:del\b/g) || []).length,
|
|
365
|
+
formatting: (interagencyTrackedXml.match(/<w:(?:rPrChange|pPrChange)\b/g) || []).length,
|
|
366
|
+
comments: (interagencyTrackedXml.match(/<w:commentRangeStart\b/g) || []).length
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
writeFileSync(join(outputDir, `${interagencyCaseName}.meta.json`), JSON.stringify(interagencyMeta, null, 2));
|
|
371
|
+
lane1Cases.push(interagencyMeta);
|
|
372
|
+
|
|
373
|
+
const manifest = {
|
|
374
|
+
generatedAt: new Date().toISOString(),
|
|
375
|
+
casesCount: lane1Cases.length,
|
|
376
|
+
cases: lane1Cases
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
writeFileSync(join(outputDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
380
|
+
console.log(`PASS: Exported ${lane1Cases.length} Lane 1 compound visual fixtures to ${outputDir}`);
|