@ansonlai/docx-redline-js 0.5.3 → 0.6.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 +82 -667
- package/ARCHITECTURE.md +51 -4
- package/CHANGELOG.md +11 -0
- package/README.md +176 -39
- package/core/paragraph-revision-safety.js +10 -8
- package/core/paragraph-targeting.js +14 -2
- package/core/redline-validation.js +7 -4
- package/core/revision-cloning.js +21 -0
- package/core/validation-delta.js +23 -0
- package/dist/docx-redline-js.esm.js +275 -45
- package/dist/docx-redline-js.esm.js.map +3 -3
- package/dist/docx-redline-js.esm.min.js +82 -82
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/AGENT_FAST_START.md +59 -0
- package/docs/AGENT_KNOWLEDGE_BASE.md +868 -0
- package/docs/TESTING.md +20 -1
- package/docs/schemas/document-operations.schema.json +16 -2
- package/docs/validation-reports/2026-09-12-agent-protocol-rollout.md +82 -0
- package/engine/oxml-engine.js +80 -13
- package/engine/run-builders.js +5 -15
- package/engine/surgical-mode.js +148 -3
- package/engine/surgical-run-splitting.js +19 -7
- package/engine/surgical-spans.js +2 -1
- package/index.d.ts +17 -1
- package/node/cli.js +235 -36
- package/node/docx-document.js +137 -83
- package/node/index.d.ts +6 -2
- package/package.json +10 -3
- package/pipeline/diff-engine.js +15 -0
- package/scripts/generate-cross-author-slicing-fixtures.ps1 +25 -25
- package/services/batch-operation-orchestrator.js +215 -120
- package/services/document-inspection.js +5 -3
- package/services/document-operation-applier.js +99 -36
- package/services/document-operation-contract.js +50 -6
- package/services/document-operation-mutations.js +404 -41
- package/services/document-operation-session.js +4 -0
- package/services/error-recovery.js +174 -0
- package/services/operation-batch-compiler.js +394 -0
- package/services/operation-preflight.js +91 -72
- package/services/standalone-operation-runner.d.ts +35 -1
- package/docs/plans/2026-09-05-structural-revisions-and-fidelity-oracles.md +0 -1669
- package/docs/plans/2026-09-08-cross-author-revision-slicing.md +0 -856
- package/docs/plans/completed/2026-03-01-release-0.1.4-design.md +0 -33
- package/docs/plans/completed/2026-03-01-release-0.1.4.md +0 -110
- package/docs/plans/completed/2026-05-31-architectural changes.md +0 -593
- package/docs/plans/completed/2026-08-02-reliability-improvements.md +0 -1155
- package/docs/plans/completed/2026-08-30-reliability-testing-improvements.md +0 -488
- package/docs/plans/completed/2026-09-01-performance-and-complexity-reduction.md +0 -669
- package/docs/plans/completed/2026-09-03-agent-friendly-document-workflows.md +0 -427
- package/docs/plans/completed/2026-09-04-comment-anchor-and-cli-reliability.md +0 -519
- package/docs/plans/completed/PERFORMANCE-CONSOLIDATION.md +0 -69
- package/docs/plans/completed/structural-revision-capability-matrix.md +0 -115
- package/docs/test-comparison-dashboard.html +0 -4338
- package/docs/validation-reports/2026-08-30-phase-1-word-visual-preflight.md +0 -22
- package/docs/validation-reports/2026-08-30-phase-2-word-visual-preflight.md +0 -24
- package/docs/validation-reports/2026-08-30-phase-3-coverage.md +0 -73
- package/docs/validation-reports/2026-09-02-multilevel-bullets-visual-review.md +0 -82
- package/docs/validation-reports/2026-09-02-multimodal-visual-samples.md +0 -114
- package/docs/validation-reports/2026-09-02-visual-failures-preflight.md +0 -79
package/node/cli.js
CHANGED
|
@@ -6,28 +6,35 @@ import { validateDocxPackage } from '../services/standalone-docx-plumbing.js';
|
|
|
6
6
|
import { validateRedlineOoxml } from '../core/redline-validation.js';
|
|
7
7
|
import { configureLogger } from '../adapters/logger.js';
|
|
8
8
|
import { isExistingRevisionsPolicy } from '../services/document-operation-contract.js';
|
|
9
|
+
import { normalizeErrorWithRecovery } from '../services/error-recovery.js';
|
|
9
10
|
|
|
10
11
|
const suffixes = { apply: 'redlined', accept: 'accepted', reject: 'rejected', 'delete-comments': 'comments-removed' };
|
|
11
|
-
const CLI_CONTRACT_VERSION =
|
|
12
|
+
const CLI_CONTRACT_VERSION = 5;
|
|
12
13
|
const CLI_CAPABILITIES = [
|
|
13
14
|
'atomic-batch-results-on-package-failure',
|
|
14
15
|
'baseline-aware-validation',
|
|
16
|
+
'compact-mutation-results',
|
|
15
17
|
'cross-author-revision-slicing',
|
|
16
|
-
'document-scoped-list-revision-ids'
|
|
17
|
-
|
|
18
|
+
'document-scoped-list-revision-ids',
|
|
19
|
+
'batch-start-source-binding',
|
|
20
|
+
'recovery-envelope-v1',
|
|
21
|
+
'require-complete-exit',
|
|
22
|
+
'operations-stdin',
|
|
23
|
+
'agent-profile-v1'
|
|
24
|
+
];
|
|
18
25
|
const commandOptions = {
|
|
19
26
|
version: new Set(['help']),
|
|
20
27
|
inspect: new Set(['help', 'search', 'revised', 'table', 'body', 'nonEmpty', 'index', 'indexes', 'range', 'view']),
|
|
21
28
|
extract: new Set(['help', 'search', 'revised', 'table', 'body', 'nonEmpty', 'index', 'indexes', 'range', 'view']),
|
|
22
29
|
preflight: new Set(['help', 'operations', 'author', 'strictTargets', 'target', 'modified', 'comment', 'textToComment', 'targetRef', 'existingRevisions']),
|
|
23
|
-
apply: new Set(['help', 'operations', 'author', 'output', 'inPlace', 'force', 'noOverwrite', 'noClobber', 'expectedRevision', 'target', 'modified', 'comment', 'textToComment', 'targetRef', 'existingRevisions', 'atomic', 'generateRedlines', 'noRedlines']),
|
|
30
|
+
apply: new Set(['help', 'operations', 'author', 'output', 'inPlace', 'force', 'noOverwrite', 'noClobber', 'expectedRevision', 'target', 'modified', 'comment', 'textToComment', 'targetRef', 'existingRevisions', 'atomic', 'generateRedlines', 'noRedlines', 'requireComplete', 'profile']),
|
|
24
31
|
accept: new Set(['help', 'author', 'allAuthors', 'output', 'inPlace', 'force', 'noOverwrite', 'noClobber']),
|
|
25
32
|
reject: new Set(['help', 'author', 'allAuthors', 'output', 'inPlace', 'force', 'noOverwrite', 'noClobber']),
|
|
26
33
|
'delete-comments': new Set(['help', 'author', 'allAuthors', 'output', 'inPlace', 'force', 'noOverwrite', 'noClobber']),
|
|
27
34
|
validate: new Set(['help', 'baseline'])
|
|
28
35
|
};
|
|
29
36
|
|
|
30
|
-
function cliError(code, message, exitCode = 2, details) { return { status: 'error', error: { code, message, ...(details ? { details } : {}) }, exitCode }; }
|
|
37
|
+
function cliError(code, message, exitCode = 2, details) { return { status: 'error', error: normalizeErrorWithRecovery({ code, message, ...(details ? { details } : {}) }), exitCode }; }
|
|
31
38
|
const optionAliases = new Map([
|
|
32
39
|
['operationsFile', 'operations'],
|
|
33
40
|
['o', 'output'],
|
|
@@ -38,7 +45,8 @@ const optionAliases = new Map([
|
|
|
38
45
|
['no-overwrite', 'noOverwrite'],
|
|
39
46
|
['no-clobber', 'noClobber'],
|
|
40
47
|
['no-redlines', 'noRedlines'],
|
|
41
|
-
['generate-redlines', 'generateRedlines']
|
|
48
|
+
['generate-redlines', 'generateRedlines'],
|
|
49
|
+
['require-complete', 'requireComplete']
|
|
42
50
|
]);
|
|
43
51
|
function parseArgs(argv) {
|
|
44
52
|
const positionals = []; const flags = {};
|
|
@@ -50,7 +58,7 @@ function parseArgs(argv) {
|
|
|
50
58
|
const normalizedKey = rawKey.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
51
59
|
const key = optionAliases.get(normalizedKey) || normalizedKey;
|
|
52
60
|
if (inline !== undefined) flags[key] = inline;
|
|
53
|
-
else if (argv[index + 1] && (!argv[index + 1].startsWith('-') || /^-\d/.test(argv[index + 1]))) flags[key] = argv[++index];
|
|
61
|
+
else if (argv[index + 1] && (argv[index + 1] === '-' || !argv[index + 1].startsWith('-') || /^-\d/.test(argv[index + 1]))) flags[key] = argv[++index];
|
|
54
62
|
else flags[key] = true;
|
|
55
63
|
}
|
|
56
64
|
return { command: positionals[0], input: positionals[1], extraPositionals: positionals.slice(2), flags };
|
|
@@ -112,7 +120,16 @@ function inspectionOptions(flags) {
|
|
|
112
120
|
}
|
|
113
121
|
return options;
|
|
114
122
|
}
|
|
115
|
-
async function
|
|
123
|
+
async function readUtf8Stream(stream) {
|
|
124
|
+
if (!stream || typeof stream[Symbol.asyncIterator] !== 'function') {
|
|
125
|
+
throw new Error('No readable stdin stream was provided.');
|
|
126
|
+
}
|
|
127
|
+
const chunks = [];
|
|
128
|
+
for await (const chunk of stream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
129
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function readOperations(file, flags = {}, stdin = process.stdin) {
|
|
116
133
|
if (!file && flags?.target) {
|
|
117
134
|
let op;
|
|
118
135
|
if (flags.comment) {
|
|
@@ -137,7 +154,14 @@ async function readOperations(file, flags = {}) {
|
|
|
137
154
|
return { operations: [op], expectedRevision: null };
|
|
138
155
|
}
|
|
139
156
|
if (!file) throw Object.assign(new Error('Use --operations <file.json> or --target <text>.'), { code: 'OPERATIONS_REQUIRED' });
|
|
140
|
-
let parsed;
|
|
157
|
+
let parsed;
|
|
158
|
+
try {
|
|
159
|
+
const source = file === '-' ? await readUtf8Stream(stdin) : await readFile(file, 'utf8');
|
|
160
|
+
parsed = JSON.parse(source);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
const location = file === '-' ? ' from stdin' : '';
|
|
163
|
+
throw Object.assign(new Error(`Could not read operations JSON${location}: ${error.message}`), { code: 'INVALID_OPERATIONS_FILE' });
|
|
164
|
+
}
|
|
141
165
|
const operations = Array.isArray(parsed) ? parsed : (parsed?.operations || parsed?.changes);
|
|
142
166
|
if (!Array.isArray(operations)) throw Object.assign(new Error('Operations JSON must be an array or an object with an operations or changes array.'), { code: 'INVALID_OPERATIONS_FILE' });
|
|
143
167
|
return { operations, expectedRevision: parsed?.expectedRevision || null };
|
|
@@ -162,10 +186,156 @@ async function writeMutation(command, input, flags, result) {
|
|
|
162
186
|
await writeFile(destination, result.toBuffer());
|
|
163
187
|
return { status: result.status || 'ok', ...result, outputPath: destination };
|
|
164
188
|
}
|
|
165
|
-
function serializable(value) {
|
|
166
|
-
const { buffer: _buffer, toBuffer: _toBuffer, ...rest } = value || {};
|
|
167
|
-
return rest;
|
|
168
|
-
}
|
|
189
|
+
function serializable(value) {
|
|
190
|
+
const { buffer: _buffer, toBuffer: _toBuffer, ...rest } = value || {};
|
|
191
|
+
return rest;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function boundedText(value, limit = 512) {
|
|
195
|
+
const text = String(value ?? '');
|
|
196
|
+
return text.length > limit ? `${text.slice(0, limit)}…` : text;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function compactError(error) {
|
|
200
|
+
if (!error || typeof error !== 'object') return error;
|
|
201
|
+
const fields = [
|
|
202
|
+
'code', 'stage', 'mismatchOffset', 'expectedExcerpt', 'actualExcerpt',
|
|
203
|
+
'expectedCodePoint', 'actualCodePoint', 'ownerAuthor', 'commentIds',
|
|
204
|
+
'recoveryVersion', 'category', 'field', 'captureRef', 'operationIndexes',
|
|
205
|
+
'consumedByOperation', 'expectedScope', 'actualScope'
|
|
206
|
+
];
|
|
207
|
+
const compact = {};
|
|
208
|
+
for (const field of fields) {
|
|
209
|
+
if (error[field] !== undefined) compact[field] = error[field];
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(error.comments)) {
|
|
212
|
+
compact.comments = error.comments.map(comment => ({
|
|
213
|
+
...(comment?.id !== undefined ? { id: comment.id } : {}),
|
|
214
|
+
...(comment?.author !== undefined ? { author: boundedText(comment.author, 160) } : {}),
|
|
215
|
+
...(comment?.text !== undefined ? { text: boundedText(comment.text, 512) } : {})
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
if (Array.isArray(error.candidates)) {
|
|
219
|
+
compact.candidates = error.candidates.map(candidate => {
|
|
220
|
+
if (!candidate || typeof candidate !== 'object') return candidate;
|
|
221
|
+
const excerpt = boundedText(candidate.excerpt ?? candidate.exactText ?? candidate.text ?? '', 240);
|
|
222
|
+
return { ...compactResolvedTarget(candidate), ...(excerpt ? { excerpt } : {}) };
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
for (const field of ['recovery', 'issueSummary', 'expectedRevision', 'currentRevision']) {
|
|
226
|
+
if (error[field] !== undefined) compact[field] = error[field];
|
|
227
|
+
}
|
|
228
|
+
if (error.context && typeof error.context === 'object') {
|
|
229
|
+
compact.context = {
|
|
230
|
+
...error.context,
|
|
231
|
+
...(error.context.currentTarget ? {
|
|
232
|
+
currentTarget: {
|
|
233
|
+
...compactResolvedTarget(error.context.currentTarget),
|
|
234
|
+
excerpt: boundedText(
|
|
235
|
+
error.context.currentTarget.excerpt
|
|
236
|
+
?? error.context.currentTarget.exactText
|
|
237
|
+
?? error.context.currentTarget.text
|
|
238
|
+
?? '',
|
|
239
|
+
240
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
} : {})
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (error.sourceTarget && typeof error.sourceTarget === 'object') {
|
|
246
|
+
compact.sourceTarget = {
|
|
247
|
+
...compactResolvedTarget(error.sourceTarget),
|
|
248
|
+
excerpt: boundedText(error.sourceTarget.text ?? error.sourceTarget.exactText ?? '', 240)
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
compact.message = boundedText(error.message || String(error));
|
|
252
|
+
return compact;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function compactResolvedTarget(target) {
|
|
256
|
+
if (!target || typeof target !== 'object') return target;
|
|
257
|
+
const { text: _text, exactText: _exactText, ...compact } = target;
|
|
258
|
+
return compact;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function compactReceipt(receipt) {
|
|
262
|
+
if (!receipt || typeof receipt !== 'object') return receipt;
|
|
263
|
+
return {
|
|
264
|
+
...receipt,
|
|
265
|
+
affectedTargets: Array.isArray(receipt.affectedTargets)
|
|
266
|
+
? receipt.affectedTargets.map(compactResolvedTarget)
|
|
267
|
+
: [],
|
|
268
|
+
warnings: Array.isArray(receipt.warnings) ? receipt.warnings.map(warning => boundedText(warning)) : []
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function compactOperationResult(result) {
|
|
273
|
+
if (!result || typeof result !== 'object') return result;
|
|
274
|
+
return {
|
|
275
|
+
...result,
|
|
276
|
+
...(result.resolvedTarget ? { resolvedTarget: compactResolvedTarget(result.resolvedTarget) } : {}),
|
|
277
|
+
...(result.resolvedAnchor ? { resolvedAnchor: compactResolvedTarget(result.resolvedAnchor) } : {}),
|
|
278
|
+
...(result.error ? { error: compactError(result.error) } : {}),
|
|
279
|
+
...(result.receipt ? { receipt: compactReceipt(result.receipt) } : {}),
|
|
280
|
+
...(Array.isArray(result.warnings) ? { warnings: result.warnings.map(warning => boundedText(warning)) } : {})
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function summarizeIssues(issues) {
|
|
285
|
+
const list = Array.isArray(issues) ? issues : [];
|
|
286
|
+
const grouped = new Map();
|
|
287
|
+
for (const issue of list) {
|
|
288
|
+
const code = issue?.code || 'UNKNOWN';
|
|
289
|
+
const source = issue?.source || 'unknown';
|
|
290
|
+
const severity = issue?.severity || 'error';
|
|
291
|
+
const key = `${source}:${severity}:${code}`;
|
|
292
|
+
const current = grouped.get(key) || { source, severity, code, count: 0 };
|
|
293
|
+
current.count++;
|
|
294
|
+
grouped.set(key, current);
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
total: list.length,
|
|
298
|
+
errors: list.filter(issue => issue?.severity === 'error').length,
|
|
299
|
+
warnings: list.filter(issue => issue?.severity === 'warning').length,
|
|
300
|
+
byCode: Array.from(grouped.values()).sort((a, b) => (
|
|
301
|
+
a.source.localeCompare(b.source) || a.code.localeCompare(b.code) || a.severity.localeCompare(b.severity)
|
|
302
|
+
))
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function compactMutationResult(value) {
|
|
307
|
+
const serialized = serializable(value);
|
|
308
|
+
const {
|
|
309
|
+
documentXml: _documentXml,
|
|
310
|
+
oxml: _oxml,
|
|
311
|
+
commentsXml: _commentsXml,
|
|
312
|
+
commentsExtendedXml: _commentsExtendedXml,
|
|
313
|
+
numberingXml: _numberingXml,
|
|
314
|
+
numberingXmlParts: _numberingXmlParts,
|
|
315
|
+
inspection: _inspection,
|
|
316
|
+
issues: _issues,
|
|
317
|
+
...compact
|
|
318
|
+
} = serialized;
|
|
319
|
+
const results = Array.isArray(compact.results) ? compact.results.map(compactOperationResult) : [];
|
|
320
|
+
const status = compact.status || 'ok';
|
|
321
|
+
return {
|
|
322
|
+
...compact,
|
|
323
|
+
...(Array.isArray(compact.results) ? { results } : {}),
|
|
324
|
+
...(Array.isArray(compact.receipts) ? { receipts: compact.receipts.map(compactReceipt) } : {}),
|
|
325
|
+
...(compact.error ? { error: compactError(compact.error) } : {}),
|
|
326
|
+
...(Array.isArray(compact.warnings) ? { warnings: compact.warnings.map(warning => boundedText(warning)) } : {}),
|
|
327
|
+
...(compact.validation ? {
|
|
328
|
+
validation: {
|
|
329
|
+
originalIssues: summarizeIssues(compact.validation.originalIssues),
|
|
330
|
+
generatedIssues: summarizeIssues(compact.validation.generatedIssues)
|
|
331
|
+
}
|
|
332
|
+
} : {}),
|
|
333
|
+
completion: compact.written === true
|
|
334
|
+
&& status !== 'error'
|
|
335
|
+
&& status !== 'partial'
|
|
336
|
+
&& results.every(result => result?.status !== 'error')
|
|
337
|
+
};
|
|
338
|
+
}
|
|
169
339
|
|
|
170
340
|
async function collectValidationIssues(buffer) {
|
|
171
341
|
const entries = unzipDocx(buffer);
|
|
@@ -199,7 +369,7 @@ function subtractValidationIssues(issues, baselineIssues) {
|
|
|
199
369
|
});
|
|
200
370
|
}
|
|
201
371
|
|
|
202
|
-
export async function executeCli(argv) {
|
|
372
|
+
export async function executeCli(argv, io = process) {
|
|
203
373
|
const { command, input: rawInput, extraPositionals, flags } = parseArgs(argv);
|
|
204
374
|
if (command === 'help' || flags.help) return { status: 'ok', command: 'help', usage: 'docx-redline <version|inspect|extract|preflight|apply|accept|reject|delete-comments|validate> [file.docx] [options]' };
|
|
205
375
|
if (!command) return cliError('COMMAND_REQUIRED', 'A command is required.');
|
|
@@ -221,6 +391,10 @@ export async function executeCli(argv) {
|
|
|
221
391
|
if (flags.existingRevisions != null && !isExistingRevisionsPolicy(flags.existingRevisions)) {
|
|
222
392
|
return cliError('INVALID_OPERATION', `Unsupported existing-revisions policy: "${String(flags.existingRevisions)}".`);
|
|
223
393
|
}
|
|
394
|
+
const profile = flags.profile == null ? null : String(flags.profile);
|
|
395
|
+
if (profile && profile !== 'agent') {
|
|
396
|
+
return cliError('INVALID_PROFILE', `Unknown execution profile: "${profile}". Supported profiles: agent.`);
|
|
397
|
+
}
|
|
224
398
|
let inspectOptions = null;
|
|
225
399
|
if (command === 'inspect' || command === 'extract') {
|
|
226
400
|
try { inspectOptions = inspectionOptions(flags); }
|
|
@@ -259,7 +433,9 @@ export async function executeCli(argv) {
|
|
|
259
433
|
const hasErrors = issues.some(issue => issue.severity === 'error');
|
|
260
434
|
return { status: hasErrors ? 'error' : 'ok', command, input, valid: !hasErrors, issues };
|
|
261
435
|
}
|
|
262
|
-
const opsData = command === 'preflight' || command === 'apply'
|
|
436
|
+
const opsData = command === 'preflight' || command === 'apply'
|
|
437
|
+
? await readOperations(flags.operations, flags, io.stdin || process.stdin)
|
|
438
|
+
: null;
|
|
263
439
|
const operations = opsData?.operations || null;
|
|
264
440
|
let expectedRevision = opsData?.expectedRevision || null;
|
|
265
441
|
if (flags.expectedRevision) {
|
|
@@ -285,15 +461,31 @@ export async function executeCli(argv) {
|
|
|
285
461
|
}),
|
|
286
462
|
command,
|
|
287
463
|
input
|
|
288
|
-
};
|
|
289
|
-
if (command === 'apply') {
|
|
290
|
-
const
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
464
|
+
};
|
|
465
|
+
if (command === 'apply') {
|
|
466
|
+
const agentProfile = profile === 'agent';
|
|
467
|
+
const author = flags.author || process.env.DOCX_REDLINE_AUTHOR || 'AI Redliner';
|
|
468
|
+
const generateRedlines = flags.generateRedlines !== undefined
|
|
469
|
+
? (flags.generateRedlines !== 'false' && flags.generateRedlines !== false)
|
|
470
|
+
: (!flags.noRedlines);
|
|
471
|
+
const atomic = flags.atomic !== undefined
|
|
472
|
+
? (flags.atomic === true || flags.atomic === 'true')
|
|
473
|
+
: agentProfile;
|
|
474
|
+
const requireComplete = flags.requireComplete !== undefined
|
|
475
|
+
? (flags.requireComplete === true || flags.requireComplete === 'true')
|
|
476
|
+
: agentProfile;
|
|
477
|
+
const effectiveOptions = {
|
|
478
|
+
author,
|
|
479
|
+
atomic,
|
|
480
|
+
strictTargets: true,
|
|
481
|
+
validate: true,
|
|
482
|
+
generateRedlines,
|
|
483
|
+
existingRevisions: flags.existingRevisions || 'merge-same-author',
|
|
484
|
+
requireComplete
|
|
485
|
+
};
|
|
486
|
+
const result = await document.applyOperations(operations, {
|
|
487
|
+
author,
|
|
488
|
+
atomic,
|
|
297
489
|
validate: true,
|
|
298
490
|
strictTargets: true,
|
|
299
491
|
generateRedlines,
|
|
@@ -301,22 +493,29 @@ export async function executeCli(argv) {
|
|
|
301
493
|
...(expectedRevision ? { expectedRevision } : {})
|
|
302
494
|
});
|
|
303
495
|
const mutationResult = await writeMutation(command, input, flags, result);
|
|
304
|
-
return {
|
|
305
|
-
command,
|
|
306
|
-
input,
|
|
307
|
-
...serializable(mutationResult),
|
|
308
|
-
...(
|
|
309
|
-
|
|
496
|
+
return compactMutationResult({
|
|
497
|
+
command,
|
|
498
|
+
input,
|
|
499
|
+
...serializable(mutationResult),
|
|
500
|
+
...(profile ? { executionProfile: profile, effectiveOptions } : {}),
|
|
501
|
+
...(result.status === 'error'
|
|
502
|
+
? { exitCode: 2 }
|
|
503
|
+
: (result.status === 'partial' && requireComplete
|
|
504
|
+
? { exitCode: 3 }
|
|
505
|
+
: {}))
|
|
506
|
+
});
|
|
310
507
|
}
|
|
311
508
|
const filter = flags.allAuthors ? { allAuthors: true } : flags.author ? { author: String(flags.author) } : null;
|
|
312
509
|
if (!filter) return cliError('AUTHOR_REQUIRED', 'Use --author <name> or --all-authors.');
|
|
313
510
|
const result = command === 'delete-comments' ? await document.deleteComments(filter) : await document.resolveRevisions(command, filter);
|
|
314
|
-
return { command, input, ...serializable(await writeMutation(command, input, flags, result)) };
|
|
511
|
+
return compactMutationResult({ command, input, ...serializable(await writeMutation(command, input, flags, result)) });
|
|
315
512
|
} catch (error) { return cliError(error.code || 'CLI_FAILED', error.message); }
|
|
316
513
|
}
|
|
317
514
|
|
|
318
|
-
export async function runCli(argv = process.argv.slice(2), io = process) {
|
|
319
|
-
configureLogger({}, { level: 'silent' });
|
|
320
|
-
const result = await executeCli(argv); io.stdout.write(`${JSON.stringify(serializable(result), null, 2)}\n`);
|
|
321
|
-
return result.
|
|
322
|
-
|
|
515
|
+
export async function runCli(argv = process.argv.slice(2), io = process) {
|
|
516
|
+
configureLogger({}, { level: 'silent' });
|
|
517
|
+
const result = await executeCli(argv, io); io.stdout.write(`${JSON.stringify(serializable(result), null, 2)}\n`);
|
|
518
|
+
return Number.isInteger(result.exitCode) && result.exitCode !== 0
|
|
519
|
+
? result.exitCode
|
|
520
|
+
: (result.status === 'error' ? 1 : 0);
|
|
521
|
+
}
|
package/node/docx-document.js
CHANGED
|
@@ -5,16 +5,48 @@ import { inspectDocumentParts } from '../services/document-inspection.js';
|
|
|
5
5
|
import { applyOperationsToDocumentXml, preflightOperations } from '../services/standalone-operation-runner.js';
|
|
6
6
|
import { createDynamicNumberingIdState, mergeNumberingXmlBySchemaOrder } from '../services/numbering-helpers.js';
|
|
7
7
|
import { ensureCommentsArtifactsInZip, ensureCommentsExtendedArtifactsInZip, ensureNumberingArtifactsInZip, validateDocxPackage } from '../services/standalone-docx-plumbing.js';
|
|
8
|
-
import { validateRedlineOoxml } from '../core/redline-validation.js';
|
|
8
|
+
import { validateRedlineOoxml } from '../core/redline-validation.js';
|
|
9
|
+
import { subtractValidationIssueMultiset, validationErrors } from '../core/validation-delta.js';
|
|
9
10
|
import { acceptTrackedChangesInOoxml, rejectTrackedChangesInOoxml, deleteCommentsByAuthorInOoxml } from '../services/revision-comment-management.js';
|
|
10
11
|
import { createSerializer, parseOoxmlSafe } from '../adapters/xml-adapter.js';
|
|
11
12
|
import { createHash } from 'node:crypto';
|
|
12
13
|
import { MemoryZip, unzipDocx, zipDocx } from './zip-archive.js';
|
|
13
|
-
import { computeRevisionTokenSync, validateRevisionToken, areRevisionTokensEqual } from '../services/revision-token.js';
|
|
14
|
+
import { computeRevisionTokenSync, validateRevisionToken, areRevisionTokensEqual } from '../services/revision-token.js';
|
|
15
|
+
import { createRetryPlan, normalizeErrorWithRecovery } from '../services/error-recovery.js';
|
|
14
16
|
|
|
15
17
|
configureXmlProvider({ DOMParser, XMLSerializer });
|
|
16
|
-
const text = (entries, path) => entries.get(path)?.toString('utf8') || null;
|
|
17
|
-
const cloneEntries = entries => new Map([...entries].map(([name, data]) => [name, Buffer.from(data)]));
|
|
18
|
+
const text = (entries, path) => entries.get(path)?.toString('utf8') || null;
|
|
19
|
+
const cloneEntries = entries => new Map([...entries].map(([name, data]) => [name, Buffer.from(data)]));
|
|
20
|
+
|
|
21
|
+
function rolledBackOperationPayload(operationResult, operationCount) {
|
|
22
|
+
const rollbackReceipt = receipt => {
|
|
23
|
+
if (!receipt || typeof receipt !== 'object') return receipt;
|
|
24
|
+
if (receipt.attemptedDisposition !== 'applied' && receipt.committed !== true) return { ...receipt };
|
|
25
|
+
return { ...receipt, committed: false, finalDisposition: 'rolled_back' };
|
|
26
|
+
};
|
|
27
|
+
const receipts = (operationResult?.receipts || []).map(rollbackReceipt);
|
|
28
|
+
const receiptByIndex = new Map(receipts.map(receipt => [receipt.operationIndex, receipt]));
|
|
29
|
+
const results = (operationResult?.results || []).map(result => ({
|
|
30
|
+
...result,
|
|
31
|
+
...(result.receipt ? {
|
|
32
|
+
receipt: receiptByIndex.get(result.index) || rollbackReceipt(result.receipt)
|
|
33
|
+
} : {})
|
|
34
|
+
}));
|
|
35
|
+
return {
|
|
36
|
+
results,
|
|
37
|
+
receipts,
|
|
38
|
+
executionOrder: operationResult?.executionOrder || [],
|
|
39
|
+
authorsUsed: [],
|
|
40
|
+
rolledBack: true,
|
|
41
|
+
retryPlan: createRetryPlan({
|
|
42
|
+
atomic: true,
|
|
43
|
+
rolledBack: true,
|
|
44
|
+
results,
|
|
45
|
+
receipts,
|
|
46
|
+
operationCount
|
|
47
|
+
})
|
|
48
|
+
};
|
|
49
|
+
}
|
|
18
50
|
|
|
19
51
|
/**
|
|
20
52
|
* Computes a package-scoped revision token over all uncompressed entries in a DOCX archive.
|
|
@@ -83,60 +115,55 @@ export class DocxDocument {
|
|
|
83
115
|
get revisionToken() { return this.getRevisionToken(); }
|
|
84
116
|
preflight(operations, author = getDefaultAuthor(), options = {}) { return preflightOperations(text(this.entries, 'word/document.xml'), operations, author || getDefaultAuthor(), { ...options, _existingCommentDetails: existingCommentDetails(this.entries) }); }
|
|
85
117
|
toBuffer() { return zipDocx(this.entries); }
|
|
86
|
-
async applyOperations(operations, options = {}) {
|
|
87
|
-
|
|
118
|
+
async applyOperations(operations, options = {}) {
|
|
119
|
+
const failedApply = error => {
|
|
120
|
+
const results = [];
|
|
121
|
+
const receipts = [];
|
|
122
|
+
return {
|
|
123
|
+
status: 'error',
|
|
124
|
+
hasChanges: false,
|
|
125
|
+
written: false,
|
|
126
|
+
rolledBack: true,
|
|
127
|
+
results,
|
|
128
|
+
receipts,
|
|
129
|
+
artifactsChanged: [],
|
|
130
|
+
error: normalizeErrorWithRecovery(error),
|
|
131
|
+
retryPlan: createRetryPlan({
|
|
132
|
+
atomic: true,
|
|
133
|
+
rolledBack: true,
|
|
134
|
+
results,
|
|
135
|
+
receipts,
|
|
136
|
+
operationCount: Array.isArray(operations) ? operations.length : 0
|
|
137
|
+
}),
|
|
138
|
+
validation: { originalIssues: [], generatedIssues: [] },
|
|
139
|
+
buffer: Buffer.from(this.originalBuffer),
|
|
140
|
+
toBuffer: () => Buffer.from(this.originalBuffer)
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
if (options?.expectedRevision) {
|
|
88
144
|
const tokenValidation = validateRevisionToken(options.expectedRevision);
|
|
89
145
|
if (!tokenValidation.valid) {
|
|
90
|
-
return {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
rolledBack: true,
|
|
95
|
-
results: [],
|
|
96
|
-
artifactsChanged: [],
|
|
97
|
-
error: {
|
|
98
|
-
code: tokenValidation.error?.code || 'INVALID_REVISION_TOKEN',
|
|
99
|
-
message: tokenValidation.error?.message || 'Invalid revision token.'
|
|
100
|
-
},
|
|
101
|
-
validation: { originalIssues: [], generatedIssues: [] },
|
|
102
|
-
buffer: Buffer.from(this.originalBuffer),
|
|
103
|
-
toBuffer: () => Buffer.from(this.originalBuffer)
|
|
104
|
-
};
|
|
146
|
+
return failedApply({
|
|
147
|
+
code: tokenValidation.error?.code || 'INVALID_REVISION_TOKEN',
|
|
148
|
+
message: tokenValidation.error?.message || 'Invalid revision token.'
|
|
149
|
+
});
|
|
105
150
|
}
|
|
106
151
|
if (options.expectedRevision.scope !== 'package') {
|
|
107
|
-
return {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
artifactsChanged: [],
|
|
114
|
-
error: {
|
|
115
|
-
code: 'REVISION_TOKEN_SCOPE_MISMATCH',
|
|
116
|
-
message: `Revision token scope mismatch: expected 'package', got '${options.expectedRevision.scope}'.`
|
|
117
|
-
},
|
|
118
|
-
validation: { originalIssues: [], generatedIssues: [] },
|
|
119
|
-
buffer: Buffer.from(this.originalBuffer),
|
|
120
|
-
toBuffer: () => Buffer.from(this.originalBuffer)
|
|
121
|
-
};
|
|
152
|
+
return failedApply({
|
|
153
|
+
code: 'REVISION_TOKEN_SCOPE_MISMATCH',
|
|
154
|
+
message: `Revision token scope mismatch: expected 'package', got '${options.expectedRevision.scope}'.`,
|
|
155
|
+
expectedScope: 'package',
|
|
156
|
+
actualScope: options.expectedRevision.scope
|
|
157
|
+
});
|
|
122
158
|
}
|
|
123
159
|
const currentToken = computePackageRevisionToken(this.entries);
|
|
124
160
|
if (!areRevisionTokensEqual(currentToken.value, options.expectedRevision.value)) {
|
|
125
|
-
return {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
artifactsChanged: [],
|
|
132
|
-
error: {
|
|
133
|
-
code: 'REVISION_MISMATCH',
|
|
134
|
-
message: `Document revision mismatch: expected '${options.expectedRevision.value}', current is '${currentToken.value}'.`
|
|
135
|
-
},
|
|
136
|
-
validation: { originalIssues: [], generatedIssues: [] },
|
|
137
|
-
buffer: Buffer.from(this.originalBuffer),
|
|
138
|
-
toBuffer: () => Buffer.from(this.originalBuffer)
|
|
139
|
-
};
|
|
161
|
+
return failedApply({
|
|
162
|
+
code: 'REVISION_MISMATCH',
|
|
163
|
+
message: `Document revision mismatch: expected '${options.expectedRevision.value}', current is '${currentToken.value}'.`,
|
|
164
|
+
expectedRevision: options.expectedRevision,
|
|
165
|
+
currentRevision: currentToken
|
|
166
|
+
});
|
|
140
167
|
}
|
|
141
168
|
}
|
|
142
169
|
|
|
@@ -162,7 +189,17 @@ export class DocxDocument {
|
|
|
162
189
|
_existingCommentDetails: existingCommentDetails(working),
|
|
163
190
|
commentIdAllocator: nextCommentId(working)
|
|
164
191
|
});
|
|
165
|
-
if (result.rolledBack || result.status === 'error')
|
|
192
|
+
if (result.rolledBack || result.status === 'error') {
|
|
193
|
+
return {
|
|
194
|
+
...result,
|
|
195
|
+
...rolledBackOperationPayload(result, Array.isArray(operations) ? operations.length : 0),
|
|
196
|
+
written: false,
|
|
197
|
+
artifactsChanged: [],
|
|
198
|
+
validation: { originalIssues, generatedIssues: [] },
|
|
199
|
+
buffer: this.originalBuffer,
|
|
200
|
+
toBuffer: () => Buffer.from(this.originalBuffer)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
166
203
|
if (!result.hasChanges) return { ...result, status: result.status || 'ok', written: false, artifactsChanged: [], validation: { originalIssues, generatedIssues: [] }, buffer: Buffer.from(this.originalBuffer), toBuffer: () => Buffer.from(this.originalBuffer) };
|
|
167
204
|
working.set('word/document.xml', Buffer.from(result.documentXml));
|
|
168
205
|
await ensureNumberingArtifactsInZip(zip, result.numberingXmlParts, { mergeNumberingXml: mergeNumberingXmlBySchemaOrder });
|
|
@@ -176,41 +213,46 @@ export class DocxDocument {
|
|
|
176
213
|
await ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXmlForPackaging, {
|
|
177
214
|
replaceExisting: result.commentsExtendedXmlMode === 'replace' || (!result.commentsExtendedXml && !!existingCommentsExtendedXml)
|
|
178
215
|
});
|
|
179
|
-
if (options.validate !== false) {
|
|
180
|
-
const generated = validateRedlineOoxml(result.documentXml);
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
216
|
+
if (options.validate !== false) {
|
|
217
|
+
const generated = validateRedlineOoxml(result.documentXml);
|
|
218
|
+
const outputIssues = generated.issues.map(issue => ({ source: 'word/document.xml', ...issue }));
|
|
219
|
+
try { await validateDocxPackage(zip); }
|
|
220
|
+
catch (error) { outputIssues.push({ source: 'package', code: 'PACKAGE_VALIDATION', severity: 'error', message: error.message }); }
|
|
221
|
+
const introduced = subtractValidationIssueMultiset(outputIssues, originalIssues);
|
|
222
|
+
const introducedErrors = validationErrors(introduced);
|
|
223
|
+
if (introducedErrors.length) {
|
|
224
|
+
const codes = [...new Set(introducedErrors.map(issue => issue.code))].join(', ');
|
|
225
|
+
throw Object.assign(
|
|
226
|
+
new Error(`Applied operations introduced invalid revision markup (${codes}); these are generated-output issues, not pre-existing input issues.`),
|
|
227
|
+
{ issues: introducedErrors }
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
192
231
|
this.entries = working;
|
|
193
232
|
const output = this.toBuffer();
|
|
194
233
|
this.originalBuffer = Buffer.from(output);
|
|
195
234
|
const artifactsChanged = [...working].filter(([name, data]) => !originalEntries.has(name) || !data.equals(originalEntries.get(name))).map(([name]) => name);
|
|
196
235
|
return { ...result, status: result.status || 'ok', written: true, artifactsChanged, validation: { originalIssues, generatedIssues: [] }, buffer: output, inspection: this.inspect(), toBuffer: () => Buffer.from(output) };
|
|
197
|
-
} catch (error) {
|
|
198
|
-
this.entries = originalEntries;
|
|
199
|
-
const generatedIssues = error.issues || [{ source: 'package', code: 'PACKAGE_OPERATION_FAILED', severity: 'error', message: error.message }];
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
236
|
+
} catch (error) {
|
|
237
|
+
this.entries = originalEntries;
|
|
238
|
+
const generatedIssues = error.issues || [{ source: 'package', code: 'PACKAGE_OPERATION_FAILED', severity: 'error', message: error.message }];
|
|
239
|
+
const rollbackPayload = rolledBackOperationPayload(
|
|
240
|
+
operationResult,
|
|
241
|
+
Array.isArray(operations) ? operations.length : 0
|
|
242
|
+
);
|
|
243
|
+
return {
|
|
244
|
+
...rollbackPayload,
|
|
245
|
+
status: 'error',
|
|
246
|
+
hasChanges: false,
|
|
247
|
+
written: false,
|
|
248
|
+
artifactsChanged: [],
|
|
249
|
+
error: normalizeErrorWithRecovery({
|
|
250
|
+
code: 'PACKAGE_OPERATION_FAILED',
|
|
251
|
+
message: error.message,
|
|
252
|
+
stage: 'package',
|
|
253
|
+
issues: generatedIssues
|
|
254
|
+
}),
|
|
255
|
+
validation: { originalIssues, generatedIssues },
|
|
214
256
|
issues: generatedIssues,
|
|
215
257
|
buffer: Buffer.from(this.originalBuffer),
|
|
216
258
|
toBuffer: () => Buffer.from(this.originalBuffer)
|
|
@@ -298,5 +340,17 @@ export class DocxDocument {
|
|
|
298
340
|
}
|
|
299
341
|
}
|
|
300
342
|
|
|
301
|
-
function packageFailure(source, code, message) {
|
|
343
|
+
function packageFailure(source, code, message) {
|
|
344
|
+
return {
|
|
345
|
+
status: 'error',
|
|
346
|
+
hasChanges: false,
|
|
347
|
+
written: false,
|
|
348
|
+
rolledBack: true,
|
|
349
|
+
error: normalizeErrorWithRecovery({ code, message }),
|
|
350
|
+
retryPlan: createRetryPlan({ atomic: true, rolledBack: true }),
|
|
351
|
+
artifactsChanged: [],
|
|
352
|
+
buffer: Buffer.from(source),
|
|
353
|
+
toBuffer: () => Buffer.from(source)
|
|
354
|
+
};
|
|
355
|
+
}
|
|
302
356
|
export function openDocx(input) { return new DocxDocument(input); }
|
package/node/index.d.ts
CHANGED
|
@@ -27,5 +27,9 @@ export class DocxDocument {
|
|
|
27
27
|
}
|
|
28
28
|
export function computePackageRevisionToken(input: unknown): RevisionToken;
|
|
29
29
|
export function openDocx(input: Uint8Array): DocxDocument;
|
|
30
|
-
export
|
|
31
|
-
|
|
30
|
+
export interface CliIo {
|
|
31
|
+
stdin?: AsyncIterable<string | Uint8Array>;
|
|
32
|
+
stdout: { write(value: string): unknown };
|
|
33
|
+
}
|
|
34
|
+
export function executeCli(argv: string[], io?: Partial<CliIo>): Promise<Record<string, unknown>>;
|
|
35
|
+
export function runCli(argv?: string[], io?: CliIo): Promise<number>;
|