@ansonlai/docx-redline-js 0.5.4 → 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 +78 -697
- package/ARCHITECTURE.md +13 -1
- package/CHANGELOG.md +5 -0
- package/README.md +140 -30
- package/core/paragraph-targeting.js +14 -2
- package/dist/docx-redline-js.esm.js +113 -33
- package/dist/docx-redline-js.esm.js.map +3 -3
- package/dist/docx-redline-js.esm.min.js +75 -75
- 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 +5 -1
- 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/index.d.ts +17 -1
- package/node/cli.js +111 -27
- package/node/docx-document.js +120 -69
- package/node/index.d.ts +6 -2
- package/package.json +10 -3
- 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 +52 -34
- package/services/document-operation-contract.js +10 -6
- package/services/document-operation-mutations.js +51 -5
- 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 +17 -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 -1399
- 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
|
@@ -19,10 +19,12 @@ import {
|
|
|
19
19
|
computeDocumentPartsRevisionToken,
|
|
20
20
|
areRevisionTokensEqual
|
|
21
21
|
} from './revision-token.js';
|
|
22
|
-
import {
|
|
23
|
-
createEmptyReceipt,
|
|
24
|
-
reconcileReceiptsAgainstOutput
|
|
25
|
-
} from './receipt-collector.js';
|
|
22
|
+
import {
|
|
23
|
+
createEmptyReceipt,
|
|
24
|
+
reconcileReceiptsAgainstOutput
|
|
25
|
+
} from './receipt-collector.js';
|
|
26
|
+
import { compileOperationBatch } from './operation-batch-compiler.js';
|
|
27
|
+
import { createRetryPlan } from './error-recovery.js';
|
|
26
28
|
|
|
27
29
|
const NS_W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
|
|
28
30
|
|
|
@@ -70,9 +72,10 @@ export function buildOperationDependencyPlan(operations = []) {
|
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
// 2. Build dependency edges
|
|
73
|
-
const inDegrees = new Array(list.length).fill(0);
|
|
74
|
-
const dependents = Array.from({ length: list.length }, () => new Set());
|
|
75
|
-
const dependencies = Array.from({ length: list.length }, () => new Set());
|
|
75
|
+
const inDegrees = new Array(list.length).fill(0);
|
|
76
|
+
const dependents = Array.from({ length: list.length }, () => new Set());
|
|
77
|
+
const dependencies = Array.from({ length: list.length }, () => new Set());
|
|
78
|
+
const mutatingCaptureConsumers = new Map();
|
|
76
79
|
|
|
77
80
|
for (let i = 0; i < list.length; i++) {
|
|
78
81
|
const op = list[i];
|
|
@@ -107,13 +110,40 @@ export function buildOperationDependencyPlan(operations = []) {
|
|
|
107
110
|
}
|
|
108
111
|
};
|
|
109
112
|
}
|
|
110
|
-
if (!dependencies[i].has(producerIndex)) {
|
|
111
|
-
dependencies[i].add(producerIndex);
|
|
112
|
-
dependents[producerIndex].add(i);
|
|
113
|
-
inDegrees[i]++;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
if (!dependencies[i].has(producerIndex)) {
|
|
114
|
+
dependencies[i].add(producerIndex);
|
|
115
|
+
dependents[producerIndex].add(i);
|
|
116
|
+
inDegrees[i]++;
|
|
117
|
+
}
|
|
118
|
+
const kind = normalizeDocumentOperation(op).operationKind;
|
|
119
|
+
if (kind !== 'comment' && kind !== 'comment_reply') {
|
|
120
|
+
if (!mutatingCaptureConsumers.has(ref)) mutatingCaptureConsumers.set(ref, []);
|
|
121
|
+
mutatingCaptureConsumers.get(ref).push({
|
|
122
|
+
index: i,
|
|
123
|
+
select: op?.target?.select ?? op?.targetDescriptor?.select ?? null
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const [captureRef, consumers] of mutatingCaptureConsumers) {
|
|
130
|
+
if (consumers.length < 2) continue;
|
|
131
|
+
const selectors = consumers.map(consumer => consumer.select);
|
|
132
|
+
const distinctSelectors = new Set(selectors);
|
|
133
|
+
if (selectors.every(selector => typeof selector === 'string' && selector.length > 0)
|
|
134
|
+
&& distinctSelectors.size === selectors.length) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
valid: false,
|
|
139
|
+
error: {
|
|
140
|
+
code: 'CAPTURE_FANOUT_CONFLICT',
|
|
141
|
+
message: `Capture "${captureRef}" has overlapping or unscoped mutating consumers at operation indices ${consumers.map(consumer => consumer.index + 1).join(', ')}.`,
|
|
142
|
+
operationIndexes: consumers.map(consumer => consumer.index + 1),
|
|
143
|
+
captureRef
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
117
147
|
|
|
118
148
|
// 3. Stable topological sort with comment priority among ready nodes
|
|
119
149
|
const ready = [];
|
|
@@ -191,62 +221,56 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
|
|
|
191
221
|
i + 1,
|
|
192
222
|
op?.operationId,
|
|
193
223
|
resolveDocumentOperationAuthor(op, author, defaultAuthor),
|
|
194
|
-
'not_attempted'
|
|
224
|
+
'not_attempted'
|
|
195
225
|
));
|
|
196
|
-
|
|
197
|
-
|
|
226
|
+
const failedBeforeExecution = (error, extra = {}) => {
|
|
227
|
+
const receipts = emptyReceipts();
|
|
198
228
|
return {
|
|
199
229
|
documentXml,
|
|
200
230
|
hasChanges: false,
|
|
201
231
|
commentsXml: null,
|
|
202
232
|
numberingXmlParts: [],
|
|
203
233
|
results: [],
|
|
204
|
-
receipts
|
|
234
|
+
receipts,
|
|
205
235
|
executionOrder: [],
|
|
206
236
|
authorsUsed: [],
|
|
237
|
+
...(options.atomic === true ? { rolledBack: true } : {}),
|
|
207
238
|
status: 'error',
|
|
208
|
-
error:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
239
|
+
error: normalizeOperationError(error),
|
|
240
|
+
retryPlan: createRetryPlan({
|
|
241
|
+
atomic: options.atomic === true,
|
|
242
|
+
rolledBack: options.atomic === true,
|
|
243
|
+
results: [],
|
|
244
|
+
receipts,
|
|
245
|
+
operationCount: sourceOperations.length
|
|
246
|
+
}),
|
|
247
|
+
...extra
|
|
212
248
|
};
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
if (options.existingRevisions != null && !isExistingRevisionsPolicy(options.existingRevisions)) {
|
|
252
|
+
return failedBeforeExecution({
|
|
253
|
+
code: 'INVALID_OPERATION',
|
|
254
|
+
message: `Unsupported existingRevisions policy: "${String(options.existingRevisions)}".`,
|
|
255
|
+
field: 'existingRevisions'
|
|
256
|
+
});
|
|
213
257
|
}
|
|
214
258
|
|
|
215
259
|
if (options?.expectedRevision) {
|
|
216
260
|
const tokenValidation = validateRevisionToken(options.expectedRevision);
|
|
217
261
|
if (!tokenValidation.valid) {
|
|
218
|
-
return {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
numberingXmlParts: [],
|
|
223
|
-
results: [],
|
|
224
|
-
receipts: emptyReceipts(),
|
|
225
|
-
executionOrder: [],
|
|
226
|
-
authorsUsed: [],
|
|
227
|
-
status: 'error',
|
|
228
|
-
error: {
|
|
229
|
-
code: tokenValidation.error?.code || 'INVALID_REVISION_TOKEN',
|
|
230
|
-
message: tokenValidation.error?.message || 'Invalid revision token.'
|
|
231
|
-
}
|
|
232
|
-
};
|
|
262
|
+
return failedBeforeExecution({
|
|
263
|
+
code: tokenValidation.error?.code || 'INVALID_REVISION_TOKEN',
|
|
264
|
+
message: tokenValidation.error?.message || 'Invalid revision token.'
|
|
265
|
+
});
|
|
233
266
|
}
|
|
234
267
|
if (options.expectedRevision.scope !== 'document-parts') {
|
|
235
|
-
return {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
receipts: emptyReceipts(),
|
|
242
|
-
executionOrder: [],
|
|
243
|
-
authorsUsed: [],
|
|
244
|
-
status: 'error',
|
|
245
|
-
error: {
|
|
246
|
-
code: 'REVISION_TOKEN_SCOPE_MISMATCH',
|
|
247
|
-
message: `Revision token scope mismatch: expected 'document-parts', got '${options.expectedRevision.scope}'.`
|
|
248
|
-
}
|
|
249
|
-
};
|
|
268
|
+
return failedBeforeExecution({
|
|
269
|
+
code: 'REVISION_TOKEN_SCOPE_MISMATCH',
|
|
270
|
+
message: `Revision token scope mismatch: expected 'document-parts', got '${options.expectedRevision.scope}'.`,
|
|
271
|
+
expectedScope: 'document-parts',
|
|
272
|
+
actualScope: options.expectedRevision.scope
|
|
273
|
+
});
|
|
250
274
|
}
|
|
251
275
|
const currentToken = await computeDocumentPartsRevisionToken({
|
|
252
276
|
documentXml,
|
|
@@ -256,21 +280,12 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
|
|
|
256
280
|
stylesXml: runtimeContext?.stylesXml || options.stylesXml
|
|
257
281
|
}, options);
|
|
258
282
|
if (!areRevisionTokensEqual(currentToken.value, options.expectedRevision.value)) {
|
|
259
|
-
return {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
receipts: emptyReceipts(),
|
|
266
|
-
executionOrder: [],
|
|
267
|
-
authorsUsed: [],
|
|
268
|
-
status: 'error',
|
|
269
|
-
error: {
|
|
270
|
-
code: 'REVISION_MISMATCH',
|
|
271
|
-
message: `Document revision mismatch: expected '${options.expectedRevision.value}', current is '${currentToken.value}'.`
|
|
272
|
-
}
|
|
273
|
-
};
|
|
283
|
+
return failedBeforeExecution({
|
|
284
|
+
code: 'REVISION_MISMATCH',
|
|
285
|
+
message: `Document revision mismatch: expected '${options.expectedRevision.value}', current is '${currentToken.value}'.`,
|
|
286
|
+
expectedRevision: options.expectedRevision,
|
|
287
|
+
currentRevision: currentToken
|
|
288
|
+
});
|
|
274
289
|
}
|
|
275
290
|
}
|
|
276
291
|
|
|
@@ -281,34 +296,73 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
|
|
|
281
296
|
_deferDocumentSerialization: true
|
|
282
297
|
});
|
|
283
298
|
if (!session.valid) {
|
|
284
|
-
return {
|
|
285
|
-
documentXml,
|
|
286
|
-
hasChanges: false,
|
|
287
|
-
commentsXml: null,
|
|
288
|
-
numberingXmlParts: [],
|
|
289
|
-
results: [],
|
|
290
|
-
receipts: emptyReceipts(),
|
|
291
|
-
executionOrder: [],
|
|
292
|
-
authorsUsed: [],
|
|
293
|
-
status: 'error',
|
|
294
|
-
error: session.parseResult.error,
|
|
295
|
-
warnings: session.parseResult.warnings
|
|
296
|
-
};
|
|
299
|
+
return failedBeforeExecution(session.parseResult.error, { warnings: session.parseResult.warnings });
|
|
297
300
|
}
|
|
298
|
-
const
|
|
301
|
+
const compilation = compileOperationBatch(session.document, sourceOperations, {
|
|
302
|
+
strictTargets: options.strictTargets !== false,
|
|
303
|
+
onInfo: options.onInfo,
|
|
304
|
+
onWarn: options.onWarn
|
|
305
|
+
});
|
|
306
|
+
session.sourceTargetRegistry = compilation.registry;
|
|
307
|
+
if (compilation.conflicts.length > 0) {
|
|
308
|
+
const conflictByIndex = new Map();
|
|
309
|
+
for (const conflict of compilation.conflicts) {
|
|
310
|
+
for (const index of conflict.operationIndexes) {
|
|
311
|
+
if (!conflictByIndex.has(index)) conflictByIndex.set(index, conflict);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const conflictResults = Array.from(conflictByIndex, ([index, conflict]) => {
|
|
315
|
+
const operation = sourceOperations[index - 1];
|
|
316
|
+
const authorUsed = resolveDocumentOperationAuthor(operation, author, defaultAuthor);
|
|
317
|
+
const normalizedConflict = normalizeOperationError(conflict, {
|
|
318
|
+
operationIndex: index,
|
|
319
|
+
...(operation?.operationId ? { operationId: operation.operationId } : {})
|
|
320
|
+
});
|
|
321
|
+
return {
|
|
322
|
+
index,
|
|
323
|
+
type: operation?.type || 'redline',
|
|
324
|
+
operationType: normalizeDocumentOperation(operation).operationKind,
|
|
325
|
+
status: 'error',
|
|
326
|
+
authorUsed,
|
|
327
|
+
error: normalizedConflict,
|
|
328
|
+
receipt: createEmptyReceipt(index, operation?.operationId, authorUsed, 'refused')
|
|
329
|
+
};
|
|
330
|
+
}).sort((left, right) => left.index - right.index);
|
|
331
|
+
const receipts = sourceOperations.map((operation, index) => {
|
|
332
|
+
const conflictResult = conflictResults.find(result => result.index === index + 1);
|
|
333
|
+
return conflictResult?.receipt || createEmptyReceipt(
|
|
334
|
+
index + 1,
|
|
335
|
+
operation?.operationId,
|
|
336
|
+
resolveDocumentOperationAuthor(operation, author, defaultAuthor),
|
|
337
|
+
'not_attempted'
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
return {
|
|
341
|
+
documentXml,
|
|
342
|
+
hasChanges: false,
|
|
343
|
+
commentsXml: null,
|
|
344
|
+
numberingXmlParts: [],
|
|
345
|
+
results: conflictResults,
|
|
346
|
+
receipts,
|
|
347
|
+
executionOrder: [],
|
|
348
|
+
authorsUsed: [],
|
|
349
|
+
rolledBack: options.atomic === true,
|
|
350
|
+
status: 'error',
|
|
351
|
+
error: normalizeOperationError(compilation.conflicts[0]),
|
|
352
|
+
conflicts: compilation.conflicts.map(conflict => normalizeOperationError(conflict)),
|
|
353
|
+
retryPlan: createRetryPlan({
|
|
354
|
+
atomic: options.atomic === true,
|
|
355
|
+
rolledBack: options.atomic === true,
|
|
356
|
+
results: conflictResults,
|
|
357
|
+
receipts,
|
|
358
|
+
operationCount: sourceOperations.length
|
|
359
|
+
})
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const executableOperations = compilation.compiledOperations;
|
|
363
|
+
const dependencyPlan = buildOperationDependencyPlan(executableOperations);
|
|
299
364
|
if (!dependencyPlan.valid) {
|
|
300
|
-
return
|
|
301
|
-
documentXml,
|
|
302
|
-
hasChanges: false,
|
|
303
|
-
commentsXml: null,
|
|
304
|
-
numberingXmlParts: [],
|
|
305
|
-
results: [],
|
|
306
|
-
receipts: emptyReceipts(),
|
|
307
|
-
executionOrder: [],
|
|
308
|
-
authorsUsed: [],
|
|
309
|
-
status: 'error',
|
|
310
|
-
error: dependencyPlan.error
|
|
311
|
-
};
|
|
365
|
+
return failedBeforeExecution(dependencyPlan.error);
|
|
312
366
|
}
|
|
313
367
|
const scheduled = dependencyPlan.scheduled;
|
|
314
368
|
|
|
@@ -332,9 +386,37 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
|
|
|
332
386
|
const authorsUsed = session.authorsUsed;
|
|
333
387
|
let operationFailed = false;
|
|
334
388
|
|
|
335
|
-
for (const { operation, index } of scheduled) {
|
|
336
|
-
executionOrder.push(index + 1);
|
|
337
|
-
|
|
389
|
+
for (const { operation, index } of scheduled) {
|
|
390
|
+
executionOrder.push(index + 1);
|
|
391
|
+
const sourceBinding = compilation.bindings[index];
|
|
392
|
+
if (sourceBinding?.error) {
|
|
393
|
+
operationFailed = true;
|
|
394
|
+
const authorUsed = resolveDocumentOperationAuthor(operation, author, defaultAuthor);
|
|
395
|
+
const errorReceipt = createEmptyReceipt(
|
|
396
|
+
index + 1,
|
|
397
|
+
operation?.operationId,
|
|
398
|
+
authorUsed,
|
|
399
|
+
'refused'
|
|
400
|
+
);
|
|
401
|
+
const normalizedBindingError = normalizeOperationError(sourceBinding.error, {
|
|
402
|
+
operationIndex: index + 1,
|
|
403
|
+
...(operation?.operationId ? { operationId: operation.operationId } : {})
|
|
404
|
+
});
|
|
405
|
+
errorReceipt.warnings.push(normalizedBindingError.message);
|
|
406
|
+
results.push({
|
|
407
|
+
index: index + 1,
|
|
408
|
+
type: operation?.type || 'redline',
|
|
409
|
+
status: 'error',
|
|
410
|
+
operationType: normalizeDocumentOperation(operation).operationKind,
|
|
411
|
+
authorUsed,
|
|
412
|
+
warnings: [normalizedBindingError.message],
|
|
413
|
+
error: normalizedBindingError,
|
|
414
|
+
receipt: errorReceipt
|
|
415
|
+
});
|
|
416
|
+
if (!continueOnError) break;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
338
420
|
const result = await applyOperationToDocumentXml(
|
|
339
421
|
session.currentDocumentXml,
|
|
340
422
|
operation,
|
|
@@ -374,8 +456,11 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
|
|
|
374
456
|
...(result.receipt ? { receipt: result.receipt } : {})
|
|
375
457
|
});
|
|
376
458
|
if (isError && !continueOnError) break;
|
|
377
|
-
} catch (error) {
|
|
378
|
-
const normalizedError = normalizeOperationError(error
|
|
459
|
+
} catch (error) {
|
|
460
|
+
const normalizedError = normalizeOperationError(error, {
|
|
461
|
+
operationIndex: index + 1,
|
|
462
|
+
...(operation?.operationId ? { operationId: operation.operationId } : {})
|
|
463
|
+
});
|
|
379
464
|
operationFailed = true;
|
|
380
465
|
const authorUsed = resolveDocumentOperationAuthor(operation, author, getDefaultAuthor());
|
|
381
466
|
const errorReceipt = createEmptyReceipt(
|
|
@@ -476,8 +561,8 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
|
|
|
476
561
|
|
|
477
562
|
if (!rolledBack) commitBatchRuntimeContext(runtimeContext, context);
|
|
478
563
|
|
|
479
|
-
|
|
480
|
-
documentXml: rolledBack ? session.rollback() : outputDocumentXml,
|
|
564
|
+
const batchResult = {
|
|
565
|
+
documentXml: rolledBack ? session.rollback() : outputDocumentXml,
|
|
481
566
|
hasChanges: rolledBack ? false : hasChanges,
|
|
482
567
|
commentsXml: rolledBack ? null : session.commentsXml,
|
|
483
568
|
commentsExtendedXml: rolledBack ? null : session.commentsExtendedXml,
|
|
@@ -491,23 +576,33 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
|
|
|
491
576
|
...(rolledBack ? {
|
|
492
577
|
rolledBack: true,
|
|
493
578
|
status: 'error',
|
|
494
|
-
error: {
|
|
495
|
-
code: reconciliationError ? reconciliationError.code : (serializationError ? 'DOCUMENT_SERIALIZATION_FAILED' : 'BATCH_OPERATION_FAILED'),
|
|
496
|
-
message: reconciliationError?.message
|
|
497
|
-
|| serializationError?.message
|
|
498
|
-
|| 'Atomic batch rolled back because one or more operations failed.'
|
|
499
|
-
}
|
|
500
|
-
} : (reconciliationError ? {
|
|
501
|
-
status: 'error',
|
|
502
|
-
error: reconciliationError
|
|
503
|
-
} : (operationFailed ? {
|
|
504
|
-
status: hasChanges ? 'partial' : 'error',
|
|
505
|
-
error: {
|
|
506
|
-
code: 'BATCH_OPERATION_FAILED',
|
|
507
|
-
message: 'One or more operations failed.'
|
|
508
|
-
}
|
|
509
|
-
} : {
|
|
510
|
-
status: 'ok'
|
|
511
|
-
})))
|
|
512
|
-
};
|
|
513
|
-
|
|
579
|
+
error: normalizeOperationError({
|
|
580
|
+
code: reconciliationError ? reconciliationError.code : (serializationError ? 'DOCUMENT_SERIALIZATION_FAILED' : 'BATCH_OPERATION_FAILED'),
|
|
581
|
+
message: reconciliationError?.message
|
|
582
|
+
|| serializationError?.message
|
|
583
|
+
|| 'Atomic batch rolled back because one or more operations failed.'
|
|
584
|
+
})
|
|
585
|
+
} : (reconciliationError ? {
|
|
586
|
+
status: 'error',
|
|
587
|
+
error: normalizeOperationError(reconciliationError)
|
|
588
|
+
} : (operationFailed ? {
|
|
589
|
+
status: hasChanges ? 'partial' : 'error',
|
|
590
|
+
error: normalizeOperationError({
|
|
591
|
+
code: 'BATCH_OPERATION_FAILED',
|
|
592
|
+
message: 'One or more operations failed.'
|
|
593
|
+
})
|
|
594
|
+
} : {
|
|
595
|
+
status: 'ok'
|
|
596
|
+
})))
|
|
597
|
+
};
|
|
598
|
+
if (batchResult.status === 'error' || batchResult.status === 'partial') {
|
|
599
|
+
batchResult.retryPlan = createRetryPlan({
|
|
600
|
+
atomic,
|
|
601
|
+
rolledBack,
|
|
602
|
+
results: batchResult.results,
|
|
603
|
+
receipts: batchResult.receipts,
|
|
604
|
+
operationCount: sourceOperations.length
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
return batchResult;
|
|
608
|
+
}
|
|
@@ -199,9 +199,10 @@ export function inspectDocumentParts(parts, options = {}) {
|
|
|
199
199
|
const resolveNumbering = createNumberingResolver(numberingPart.doc);
|
|
200
200
|
let nearestHeading = null;
|
|
201
201
|
const paragraphNodes = getDocumentParagraphNodes(documentPart.doc);
|
|
202
|
-
const
|
|
202
|
+
const revisionView = options.revisionView === 'rejected' ? 'rejected' : 'accepted';
|
|
203
|
+
const commentAnchors = collectDocumentCommentAnchors(paragraphNodes, revisionView);
|
|
203
204
|
let paragraphs = paragraphNodes.map((paragraph, zeroIndex) => {
|
|
204
|
-
const text = extractCanonicalParagraphText(paragraph, { revisionView
|
|
205
|
+
const text = extractCanonicalParagraphText(paragraph, { revisionView });
|
|
205
206
|
const level = headingLevel(paragraph);
|
|
206
207
|
if (level) nearestHeading = { level, text };
|
|
207
208
|
const ids = [...new Set([...descendants(paragraph, 'commentRangeStart'), ...descendants(paragraph, 'commentReference')].map(node => attr(node, 'id')).filter(Boolean))];
|
|
@@ -215,7 +216,8 @@ export function inspectDocumentParts(parts, options = {}) {
|
|
|
215
216
|
const humanReference = [provision, headingText, text.slice(0, options.excerptLength || 120)].filter(Boolean).join(' — ');
|
|
216
217
|
const segments = extractParagraphRevisionSegments(paragraph);
|
|
217
218
|
return {
|
|
218
|
-
index, ref: `P${index}`, paragraphId: getParagraphId(paragraph),
|
|
219
|
+
index, ref: `P${index}`, paragraphId: getParagraphId(paragraph),
|
|
220
|
+
fingerprint: createParagraphFingerprint(paragraph, { text, index, revisionView }), revisionView,
|
|
219
221
|
text, exactText: text, excerpt: text.slice(0, options.excerptLength || 120), humanReference, inTable: hasAncestor(paragraph, 'tc'), table: structure.table,
|
|
220
222
|
styleId, headingLevel: level, nearestHeading, list, structuralReferences: structure.references, hasRevisions: authors.length > 0, revisionAuthors: authors, commentIds: ids,
|
|
221
223
|
segments
|
|
@@ -30,21 +30,22 @@ import {
|
|
|
30
30
|
} from './receipt-collector.js';
|
|
31
31
|
import { validateRedlineOoxml } from '../core/redline-validation.js';
|
|
32
32
|
import { subtractValidationIssueMultiset, validationErrors } from '../core/validation-delta.js';
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
...(Array.isArray(error?.candidates) ? { candidates: error.candidates } : {})
|
|
39
|
-
};
|
|
40
|
-
}
|
|
33
|
+
import { normalizeErrorWithRecovery } from './error-recovery.js';
|
|
34
|
+
|
|
35
|
+
export function normalizeOperationError(error, context = {}) {
|
|
36
|
+
return normalizeErrorWithRecovery(error, context);
|
|
37
|
+
}
|
|
41
38
|
|
|
42
39
|
/**
|
|
43
40
|
* Validates and dispatches one structured operation against full document XML.
|
|
44
41
|
* Result metadata is assembled here so every mutation path exposes the same
|
|
45
42
|
* */
|
|
46
|
-
export async function applyOperationToDocumentXml(documentXml, op, author, runtimeContext = null, options = {}) {
|
|
47
|
-
const operationIndex = typeof options._operationIndex === 'number' ? options._operationIndex : 1;
|
|
43
|
+
export async function applyOperationToDocumentXml(documentXml, op, author, runtimeContext = null, options = {}) {
|
|
44
|
+
const operationIndex = typeof options._operationIndex === 'number' ? options._operationIndex : 1;
|
|
45
|
+
const errorContext = {
|
|
46
|
+
operationIndex,
|
|
47
|
+
...(typeof op?.operationId === 'string' ? { operationId: op.operationId } : {})
|
|
48
|
+
};
|
|
48
49
|
const validation = validateDocumentOperation(op);
|
|
49
50
|
if (!validation.valid) {
|
|
50
51
|
const authorUsed = resolveDocumentOperationAuthor(op, author, getDefaultAuthor());
|
|
@@ -52,7 +53,7 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
52
53
|
documentXml,
|
|
53
54
|
hasChanges: false,
|
|
54
55
|
status: 'error',
|
|
55
|
-
error: validation.error,
|
|
56
|
+
error: normalizeOperationError(validation.error, errorContext),
|
|
56
57
|
operationType: normalizeDocumentOperation(op).operationKind,
|
|
57
58
|
authorUsed,
|
|
58
59
|
receipt: createEmptyReceipt(operationIndex, op?.operationId, authorUsed, 'refused')
|
|
@@ -75,10 +76,10 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
75
76
|
documentXml,
|
|
76
77
|
hasChanges: false,
|
|
77
78
|
status: 'error',
|
|
78
|
-
error: {
|
|
79
|
-
code: 'UNSUPPORTED_REVISION_VIEW_MUTATION',
|
|
80
|
-
message: 'Targeting rejected revision view for mutation is not supported yet.'
|
|
81
|
-
},
|
|
79
|
+
error: normalizeOperationError({
|
|
80
|
+
code: 'UNSUPPORTED_REVISION_VIEW_MUTATION',
|
|
81
|
+
message: 'Targeting rejected revision view for mutation is not supported yet.'
|
|
82
|
+
}, errorContext),
|
|
82
83
|
operationType: operation.operationKind,
|
|
83
84
|
authorUsed,
|
|
84
85
|
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
@@ -92,10 +93,10 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
92
93
|
documentXml,
|
|
93
94
|
hasChanges: false,
|
|
94
95
|
status: 'error',
|
|
95
|
-
error: {
|
|
96
|
-
code: tokenValidation.error?.code || 'INVALID_REVISION_TOKEN',
|
|
97
|
-
message: tokenValidation.error?.message || 'Invalid revision token.'
|
|
98
|
-
},
|
|
96
|
+
error: normalizeOperationError({
|
|
97
|
+
code: tokenValidation.error?.code || 'INVALID_REVISION_TOKEN',
|
|
98
|
+
message: tokenValidation.error?.message || 'Invalid revision token.'
|
|
99
|
+
}, errorContext),
|
|
99
100
|
operationType: operation.operationKind,
|
|
100
101
|
authorUsed,
|
|
101
102
|
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
@@ -106,10 +107,10 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
106
107
|
documentXml,
|
|
107
108
|
hasChanges: false,
|
|
108
109
|
status: 'error',
|
|
109
|
-
error: {
|
|
110
|
-
code: 'REVISION_TOKEN_SCOPE_MISMATCH',
|
|
111
|
-
message: `Revision token scope mismatch: expected 'document-parts', got '${options.expectedRevision.scope}'.`
|
|
112
|
-
},
|
|
110
|
+
error: normalizeOperationError({
|
|
111
|
+
code: 'REVISION_TOKEN_SCOPE_MISMATCH',
|
|
112
|
+
message: `Revision token scope mismatch: expected 'document-parts', got '${options.expectedRevision.scope}'.`
|
|
113
|
+
}, errorContext),
|
|
113
114
|
operationType: operation.operationKind,
|
|
114
115
|
authorUsed,
|
|
115
116
|
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
@@ -127,10 +128,12 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
127
128
|
documentXml,
|
|
128
129
|
hasChanges: false,
|
|
129
130
|
status: 'error',
|
|
130
|
-
error: {
|
|
131
|
-
code: 'REVISION_MISMATCH',
|
|
132
|
-
message: `Document revision mismatch: expected '${options.expectedRevision.value}', current is '${currentToken.value}'
|
|
133
|
-
|
|
131
|
+
error: normalizeOperationError({
|
|
132
|
+
code: 'REVISION_MISMATCH',
|
|
133
|
+
message: `Document revision mismatch: expected '${options.expectedRevision.value}', current is '${currentToken.value}'.`,
|
|
134
|
+
expectedRevision: options.expectedRevision,
|
|
135
|
+
currentRevision: currentToken
|
|
136
|
+
}, errorContext),
|
|
134
137
|
operationType: operation.operationKind,
|
|
135
138
|
authorUsed,
|
|
136
139
|
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
@@ -146,7 +149,7 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
146
149
|
documentXml,
|
|
147
150
|
hasChanges: false,
|
|
148
151
|
status: 'error',
|
|
149
|
-
error: session.parseResult.error,
|
|
152
|
+
error: normalizeOperationError(session.parseResult.error, errorContext),
|
|
150
153
|
warnings: session.parseResult.warnings,
|
|
151
154
|
operationType: operation.operationKind,
|
|
152
155
|
authorUsed,
|
|
@@ -161,7 +164,9 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
161
164
|
operation.operationId,
|
|
162
165
|
authorUsed
|
|
163
166
|
);
|
|
164
|
-
const operationWarnings =
|
|
167
|
+
const operationWarnings = Array.isArray(operation._compiledWarnings)
|
|
168
|
+
? [...operation._compiledWarnings]
|
|
169
|
+
: [];
|
|
165
170
|
const operationOptions = {
|
|
166
171
|
...options,
|
|
167
172
|
...(typeof operation.generateRedlines === 'boolean' ? { generateRedlines: operation.generateRedlines } : {}),
|
|
@@ -173,6 +178,10 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
173
178
|
...(operation.formattingRevisionPolicy ? { formattingRevisionPolicy: operation.formattingRevisionPolicy } : {}),
|
|
174
179
|
targetDescriptor: operation.targetDescriptor,
|
|
175
180
|
targetEndDescriptor: operation.targetEndDescriptor,
|
|
181
|
+
_compiledSourceId: operation._compiledSourceId || null,
|
|
182
|
+
_compiledSourceEndId: operation._compiledSourceEndId || null,
|
|
183
|
+
_compiledResolvedBy: operation._compiledResolvedBy || null,
|
|
184
|
+
_sourceTargetRegistry: session.sourceTargetRegistry || null,
|
|
176
185
|
_resolutionCapture: resolutionCapture,
|
|
177
186
|
_revisionIdAllocator: session.revisionIdAllocator,
|
|
178
187
|
_documentOperationSession: session,
|
|
@@ -345,12 +354,12 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
345
354
|
documentXml,
|
|
346
355
|
hasChanges: false,
|
|
347
356
|
status: 'error',
|
|
348
|
-
error: {
|
|
357
|
+
error: normalizeOperationError({
|
|
349
358
|
code: 'GENERATED_OOXML_INVALID',
|
|
350
359
|
stage: 'validation',
|
|
351
360
|
message: `Operation introduced invalid OOXML (${codes}).`,
|
|
352
361
|
generatedIssues: generatedErrors
|
|
353
|
-
},
|
|
362
|
+
}, errorContext),
|
|
354
363
|
operationType: operation.operationKind,
|
|
355
364
|
authorUsed,
|
|
356
365
|
receipt: operationReceipt,
|
|
@@ -358,6 +367,14 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
358
367
|
};
|
|
359
368
|
}
|
|
360
369
|
session.markMutationCommitted(operation.operationKind !== 'comment_reply');
|
|
370
|
+
if (operation._compiledSourceId && session.sourceTargetRegistry) {
|
|
371
|
+
session.sourceTargetRegistry.commitMutation(
|
|
372
|
+
operation._compiledSourceId,
|
|
373
|
+
operationOptions._mutationRemovedNodes,
|
|
374
|
+
operationOptions._mutationLiveNodes,
|
|
375
|
+
operationIndex
|
|
376
|
+
);
|
|
377
|
+
}
|
|
361
378
|
if (operation.captureKey && session.captureTable) {
|
|
362
379
|
session.captureTable.set(
|
|
363
380
|
operation.captureKey,
|
|
@@ -394,7 +411,7 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
394
411
|
documentXml,
|
|
395
412
|
hasChanges: false,
|
|
396
413
|
status: 'error',
|
|
397
|
-
error: reconciliation.error,
|
|
414
|
+
error: normalizeOperationError(reconciliation.error, errorContext),
|
|
398
415
|
warnings: [reconciliation.error.message],
|
|
399
416
|
operationType: operation.operationKind,
|
|
400
417
|
authorUsed,
|
|
@@ -404,7 +421,8 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
404
421
|
}
|
|
405
422
|
}
|
|
406
423
|
}
|
|
407
|
-
|
|
424
|
+
if (result?.error) result.error = normalizeOperationError(result.error, errorContext);
|
|
425
|
+
return {
|
|
408
426
|
...result,
|
|
409
427
|
operationType: operation.operationKind,
|
|
410
428
|
authorUsed,
|
|
@@ -413,7 +431,7 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
413
431
|
};
|
|
414
432
|
} catch (error) {
|
|
415
433
|
session.restoreSavepoint(savepoint);
|
|
416
|
-
const normalizedError = normalizeOperationError(error);
|
|
434
|
+
const normalizedError = normalizeOperationError(error, errorContext);
|
|
417
435
|
const operationReceipt = createEmptyReceipt(
|
|
418
436
|
operationIndex,
|
|
419
437
|
operation.operationId,
|