@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.
Files changed (51) hide show
  1. package/AGENTS.md +78 -697
  2. package/ARCHITECTURE.md +13 -1
  3. package/CHANGELOG.md +5 -0
  4. package/README.md +140 -30
  5. package/core/paragraph-targeting.js +14 -2
  6. package/dist/docx-redline-js.esm.js +113 -33
  7. package/dist/docx-redline-js.esm.js.map +3 -3
  8. package/dist/docx-redline-js.esm.min.js +75 -75
  9. package/dist/docx-redline-js.esm.min.js.map +4 -4
  10. package/docs/AGENT_FAST_START.md +59 -0
  11. package/docs/AGENT_KNOWLEDGE_BASE.md +868 -0
  12. package/docs/TESTING.md +20 -1
  13. package/docs/schemas/document-operations.schema.json +5 -1
  14. package/docs/validation-reports/2026-09-12-agent-protocol-rollout.md +82 -0
  15. package/engine/oxml-engine.js +80 -13
  16. package/engine/run-builders.js +5 -15
  17. package/index.d.ts +17 -1
  18. package/node/cli.js +111 -27
  19. package/node/docx-document.js +120 -69
  20. package/node/index.d.ts +6 -2
  21. package/package.json +10 -3
  22. package/scripts/generate-cross-author-slicing-fixtures.ps1 +25 -25
  23. package/services/batch-operation-orchestrator.js +215 -120
  24. package/services/document-inspection.js +5 -3
  25. package/services/document-operation-applier.js +52 -34
  26. package/services/document-operation-contract.js +10 -6
  27. package/services/document-operation-mutations.js +51 -5
  28. package/services/document-operation-session.js +4 -0
  29. package/services/error-recovery.js +174 -0
  30. package/services/operation-batch-compiler.js +394 -0
  31. package/services/operation-preflight.js +91 -72
  32. package/services/standalone-operation-runner.d.ts +17 -1
  33. package/docs/plans/2026-09-05-structural-revisions-and-fidelity-oracles.md +0 -1669
  34. package/docs/plans/2026-09-08-cross-author-revision-slicing.md +0 -1399
  35. package/docs/plans/completed/2026-03-01-release-0.1.4-design.md +0 -33
  36. package/docs/plans/completed/2026-03-01-release-0.1.4.md +0 -110
  37. package/docs/plans/completed/2026-05-31-architectural changes.md +0 -593
  38. package/docs/plans/completed/2026-08-02-reliability-improvements.md +0 -1155
  39. package/docs/plans/completed/2026-08-30-reliability-testing-improvements.md +0 -488
  40. package/docs/plans/completed/2026-09-01-performance-and-complexity-reduction.md +0 -669
  41. package/docs/plans/completed/2026-09-03-agent-friendly-document-workflows.md +0 -427
  42. package/docs/plans/completed/2026-09-04-comment-anchor-and-cli-reliability.md +0 -519
  43. package/docs/plans/completed/PERFORMANCE-CONSOLIDATION.md +0 -69
  44. package/docs/plans/completed/structural-revision-capability-matrix.md +0 -115
  45. package/docs/test-comparison-dashboard.html +0 -4338
  46. package/docs/validation-reports/2026-08-30-phase-1-word-visual-preflight.md +0 -22
  47. package/docs/validation-reports/2026-08-30-phase-2-word-visual-preflight.md +0 -24
  48. package/docs/validation-reports/2026-08-30-phase-3-coverage.md +0 -73
  49. package/docs/validation-reports/2026-09-02-multilevel-bullets-visual-review.md +0 -82
  50. package/docs/validation-reports/2026-09-02-multimodal-visual-samples.md +0 -114
  51. 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
- if (options.existingRevisions != null && !isExistingRevisionsPolicy(options.existingRevisions)) {
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: emptyReceipts(),
234
+ receipts,
205
235
  executionOrder: [],
206
236
  authorsUsed: [],
237
+ ...(options.atomic === true ? { rolledBack: true } : {}),
207
238
  status: 'error',
208
- error: {
209
- code: 'INVALID_OPERATION',
210
- message: `Unsupported existingRevisions policy: "${String(options.existingRevisions)}".`
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
- documentXml,
220
- hasChanges: false,
221
- commentsXml: null,
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
- documentXml,
237
- hasChanges: false,
238
- commentsXml: null,
239
- numberingXmlParts: [],
240
- results: [],
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
- documentXml,
261
- hasChanges: false,
262
- commentsXml: null,
263
- numberingXmlParts: [],
264
- results: [],
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 dependencyPlan = buildOperationDependencyPlan(sourceOperations);
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
- try {
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
- return {
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 commentAnchors = collectDocumentCommentAnchors(paragraphNodes, options.revisionView || 'accepted');
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: options.revisionView || 'accepted' });
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), fingerprint: createParagraphFingerprint(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
- export function normalizeOperationError(error) {
35
- return {
36
- code: typeof error?.code === 'string' && error.code ? error.code : 'OPERATION_ERROR',
37
- message: error?.message || String(error),
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
- return {
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,