@ansonlai/docx-redline-js 0.5.4 → 0.6.1

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 (55) hide show
  1. package/AGENTS.md +82 -697
  2. package/ARCHITECTURE.md +13 -1
  3. package/CHANGELOG.md +8 -0
  4. package/README.md +177 -45
  5. package/core/paragraph-targeting.js +14 -2
  6. package/dist/docx-redline-js.esm.js +184 -51
  7. package/dist/docx-redline-js.esm.js.map +3 -3
  8. package/dist/docx-redline-js.esm.min.js +77 -77
  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 +878 -0
  12. package/docs/SKILL_AUTHORING.md +126 -0
  13. package/docs/TESTING.md +35 -1
  14. package/docs/schemas/document-operations.schema.json +5 -1
  15. package/docs/validation-reports/2026-09-12-agent-cli-discovery-baseline.md +56 -0
  16. package/docs/validation-reports/2026-09-12-agent-protocol-rollout.md +86 -0
  17. package/docs/validation-reports/2026-09-13-agent-cli-efficiency-rollout.md +86 -0
  18. package/engine/oxml-engine.js +80 -13
  19. package/engine/run-builders.js +5 -15
  20. package/index.d.ts +28 -3
  21. package/node/cli-help.js +209 -0
  22. package/node/cli.js +323 -65
  23. package/node/docx-document.js +120 -69
  24. package/node/index.d.ts +6 -2
  25. package/package.json +15 -3
  26. package/scripts/generate-cross-author-slicing-fixtures.ps1 +25 -25
  27. package/services/batch-operation-orchestrator.js +215 -120
  28. package/services/document-inspection.js +89 -11
  29. package/services/document-operation-applier.js +52 -34
  30. package/services/document-operation-contract.js +10 -6
  31. package/services/document-operation-mutations.js +51 -5
  32. package/services/document-operation-session.js +4 -0
  33. package/services/error-recovery.js +174 -0
  34. package/services/operation-batch-compiler.js +394 -0
  35. package/services/operation-preflight.js +91 -72
  36. package/services/standalone-operation-runner.d.ts +17 -1
  37. package/docs/plans/2026-09-05-structural-revisions-and-fidelity-oracles.md +0 -1669
  38. package/docs/plans/2026-09-08-cross-author-revision-slicing.md +0 -1399
  39. package/docs/plans/completed/2026-03-01-release-0.1.4-design.md +0 -33
  40. package/docs/plans/completed/2026-03-01-release-0.1.4.md +0 -110
  41. package/docs/plans/completed/2026-05-31-architectural changes.md +0 -593
  42. package/docs/plans/completed/2026-08-02-reliability-improvements.md +0 -1155
  43. package/docs/plans/completed/2026-08-30-reliability-testing-improvements.md +0 -488
  44. package/docs/plans/completed/2026-09-01-performance-and-complexity-reduction.md +0 -669
  45. package/docs/plans/completed/2026-09-03-agent-friendly-document-workflows.md +0 -427
  46. package/docs/plans/completed/2026-09-04-comment-anchor-and-cli-reliability.md +0 -519
  47. package/docs/plans/completed/PERFORMANCE-CONSOLIDATION.md +0 -69
  48. package/docs/plans/completed/structural-revision-capability-matrix.md +0 -115
  49. package/docs/test-comparison-dashboard.html +0 -4338
  50. package/docs/validation-reports/2026-08-30-phase-1-word-visual-preflight.md +0 -22
  51. package/docs/validation-reports/2026-08-30-phase-2-word-visual-preflight.md +0 -24
  52. package/docs/validation-reports/2026-08-30-phase-3-coverage.md +0 -73
  53. package/docs/validation-reports/2026-09-02-multilevel-bullets-visual-review.md +0 -82
  54. package/docs/validation-reports/2026-09-02-multimodal-visual-samples.md +0 -114
  55. package/docs/validation-reports/2026-09-02-visual-failures-preflight.md +0 -79
@@ -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,
@@ -51,7 +51,8 @@ export function getCanonicalOperationType(operation) {
51
51
  return 'redline';
52
52
  }
53
53
 
54
- export function normalizeTargetDescriptor(target, legacyTargetRef = null) {
54
+ export function normalizeTargetDescriptor(target, legacyTargetRef = null, defaultRevisionView = 'accepted') {
55
+ const fallbackRevisionView = defaultRevisionView === 'rejected' ? 'rejected' : 'accepted';
55
56
  if (!isRecord(target)) {
56
57
  return {
57
58
  text: typeof target === 'string' ? target : '',
@@ -60,7 +61,7 @@ export function normalizeTargetDescriptor(target, legacyTargetRef = null) {
60
61
  occurrence: null,
61
62
  inTable: null,
62
63
  fingerprint: null,
63
- revisionView: 'accepted'
64
+ revisionView: fallbackRevisionView
64
65
  };
65
66
  }
66
67
 
@@ -77,7 +78,9 @@ export function normalizeTargetDescriptor(target, legacyTargetRef = null) {
77
78
  fingerprint: nonEmptyString(target.fingerprint)
78
79
  ? target.fingerprint.trim()
79
80
  : (nonEmptyString(target.sourceFingerprint) ? target.sourceFingerprint.trim() : null),
80
- revisionView: target.revisionView === 'rejected' ? 'rejected' : 'accepted',
81
+ revisionView: target.revisionView === 'rejected'
82
+ ? 'rejected'
83
+ : (target.revisionView === 'accepted' ? 'accepted' : fallbackRevisionView),
81
84
  captureRef: nonEmptyString(target.captureRef) ? target.captureRef.trim() : null,
82
85
  select: typeof target.select === 'string' ? target.select : null
83
86
  };
@@ -85,11 +88,12 @@ export function normalizeTargetDescriptor(target, legacyTargetRef = null) {
85
88
 
86
89
  export function normalizeDocumentOperation(operation) {
87
90
  const source = isRecord(operation) ? operation : {};
88
- const targetDescriptor = normalizeTargetDescriptor(source.target, source.targetRef);
91
+ const kind = getCanonicalOperationType(source);
92
+ const defaultRevisionView = kind === 'restore' ? 'rejected' : 'accepted';
93
+ const targetDescriptor = normalizeTargetDescriptor(source.target, source.targetRef, defaultRevisionView);
89
94
  const targetEndDescriptor = source.targetEnd != null
90
- ? normalizeTargetDescriptor(source.targetEnd, source.targetEndRef)
95
+ ? normalizeTargetDescriptor(source.targetEnd, source.targetEndRef, defaultRevisionView)
91
96
  : null;
92
- const kind = getCanonicalOperationType(source);
93
97
 
94
98
  return {
95
99
  ...source,
@@ -224,11 +224,36 @@ function describeTargetTextMatch(actualText, requestedText) {
224
224
  };
225
225
  }
226
226
 
227
- function resolveTargetParagraph(xmlDoc, targetText, targetRef, opType, runtimeContext = null, options = {}) {
227
+ function resolveTargetParagraph(xmlDoc, targetText, targetRef, opType, runtimeContext = null, options = {}) {
228
228
  const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => { };
229
229
  const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => { };
230
230
  const session = options?._documentOperationSession || null;
231
- const paragraphMetadataIndex = session?.getParagraphMetadataIndex?.() || null;
231
+ const paragraphMetadataIndex = session?.getParagraphMetadataIndex?.() || null;
232
+
233
+ if (options?._compiledSourceId && options?._sourceTargetRegistry) {
234
+ const resolved = options._sourceTargetRegistry.resolve(options._compiledSourceId, xmlDoc);
235
+ if (resolved?.error || !resolved?.paragraph) return resolved;
236
+ resolved.resolvedBy = options._compiledResolvedBy || resolved.resolvedBy;
237
+ if (options?._resolutionCapture) {
238
+ const paragraph = resolved.paragraph;
239
+ const metadata = paragraphMetadataIndex?.byParagraph?.get(paragraph) || null;
240
+ Object.assign(options._resolutionCapture, {
241
+ resolvedBy: resolved.resolvedBy,
242
+ resolvedTarget: {
243
+ index: metadata?.index ?? getDocumentParagraphNodes(xmlDoc).indexOf(paragraph) + 1,
244
+ paragraphId: metadata?.paragraphId ?? getParagraphId(paragraph),
245
+ text: metadata?.text ?? getParagraphText(paragraph),
246
+ fingerprint: metadata?.fingerprint ?? createParagraphFingerprint(paragraph),
247
+ inTable: metadata?.inTable ?? !!findContainingWordElement(paragraph, 'tbl'),
248
+ targetTextMatch: describeTargetTextMatch(
249
+ metadata?.text ?? getParagraphText(paragraph),
250
+ options?.targetDescriptor?.exactText ?? targetText
251
+ )
252
+ }
253
+ });
254
+ }
255
+ return resolved;
256
+ }
232
257
 
233
258
  if (options?.targetDescriptor?.captureRef) {
234
259
  try {
@@ -1106,7 +1131,22 @@ export async function restoreDeletedParagraphByExactText(
1106
1131
 
1107
1132
  const firstSource = resolved.paragraph;
1108
1133
  let sourceParagraphs = [firstSource];
1109
- if (targetEndRef) {
1134
+ if (options._compiledSourceEndId) {
1135
+ const endResolved = resolveTargetParagraph(
1136
+ xmlDoc,
1137
+ options.targetEndDescriptor?.text || '',
1138
+ targetEndRef,
1139
+ 'restore',
1140
+ runtimeContext,
1141
+ { ...options, _compiledSourceId: options._compiledSourceEndId }
1142
+ );
1143
+ const allParagraphs = endResolved?.paragraph ? getDocumentParagraphNodes(xmlDoc) : [];
1144
+ const startIndex = allParagraphs.indexOf(firstSource);
1145
+ const endIndex = allParagraphs.indexOf(endResolved?.paragraph);
1146
+ sourceParagraphs = startIndex >= 0 && endIndex >= startIndex
1147
+ ? allParagraphs.slice(startIndex, endIndex + 1)
1148
+ : null;
1149
+ } else if (targetEndRef) {
1110
1150
  sourceParagraphs = resolveParagraphRangeByRefs(xmlDoc, targetRef, targetEndRef, {
1111
1151
  opType: 'restore',
1112
1152
  targetRefSnapshot: runtimeContext?.targetRefSnapshot || null,
@@ -1120,7 +1160,11 @@ export async function restoreDeletedParagraphByExactText(
1120
1160
  options.targetEndDescriptor.index,
1121
1161
  'restore',
1122
1162
  runtimeContext,
1123
- { ...options, targetDescriptor: options.targetEndDescriptor }
1163
+ {
1164
+ ...options,
1165
+ targetDescriptor: options.targetEndDescriptor,
1166
+ _compiledSourceId: options._compiledSourceEndId || null
1167
+ }
1124
1168
  );
1125
1169
  const allParagraphs = endResolved?.paragraph ? getDocumentParagraphNodes(xmlDoc) : [];
1126
1170
  const startIndex = allParagraphs.indexOf(firstSource);
@@ -1669,7 +1713,9 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
1669
1713
  status: 'error',
1670
1714
  error: {
1671
1715
  code: 'EXISTING_REVISIONS',
1672
- message: `Target paragraph contains tracked changes from another author (${authors.length ? authors.join(', ') : 'unattributed'}). Pass existingRevisions: "accept-all-first" or resolve revisions first.`
1716
+ message: `Target paragraph contains tracked changes from another author (${authors.length ? authors.join(', ') : 'unattributed'}). Use existingRevisions: "slice-cross-author" for a surgical edit that preserves reviewer history; accepting or rejecting revisions requires separate authorization.`,
1717
+ revisionAuthors: authors,
1718
+ currentPolicy: existingPolicy
1673
1719
  }
1674
1720
  };
1675
1721
  }
@@ -42,6 +42,7 @@ export class DocumentOperationSession {
42
42
  this.executionOrder = [];
43
43
  this.authorsUsed = new Set();
44
44
  this.captureTable = new Map();
45
+ this.sourceTargetRegistry = null;
45
46
  this.nextCaptureParaId = 1;
46
47
  this.receiptCollector = new ReceiptCollector();
47
48
 
@@ -111,6 +112,7 @@ export class DocumentOperationSession {
111
112
  commentsXmlMode: this.commentsXmlMode,
112
113
  commentsExtendedXmlMode: this.commentsExtendedXmlMode,
113
114
  captureTable: cloneCaptureTable(this.captureTable),
115
+ sourceTargetRegistry: this.sourceTargetRegistry?.createSavepoint?.(this.document) || null,
114
116
  nextCaptureParaId: this.nextCaptureParaId,
115
117
  receiptCollector: this.receiptCollector ? this.receiptCollector.createSavepoint() : null
116
118
  };
@@ -127,6 +129,7 @@ export class DocumentOperationSession {
127
129
  this.commentsXmlMode = savepoint.commentsXmlMode || 'merge';
128
130
  this.commentsExtendedXmlMode = savepoint.commentsExtendedXmlMode || 'merge';
129
131
  this.captureTable = savepoint.captureTable ? cloneCaptureTable(savepoint.captureTable) : new Map();
132
+ this.sourceTargetRegistry?.restoreSavepoint?.(this.document, savepoint.sourceTargetRegistry);
130
133
  if (typeof savepoint.nextCaptureParaId === 'number') {
131
134
  this.nextCaptureParaId = savepoint.nextCaptureParaId;
132
135
  }
@@ -160,6 +163,7 @@ export class DocumentOperationSession {
160
163
  this.hasChanges = false;
161
164
  this.documentHasChanges = false;
162
165
  this.captureTable.clear();
166
+ this.sourceTargetRegistry = null;
163
167
  this.receiptCollector?.clear();
164
168
  return this.originalDocumentXml;
165
169
  }
@@ -0,0 +1,174 @@
1
+ export const ERROR_RECOVERY_VERSION = 1;
2
+
3
+ const RULES = Object.freeze({
4
+ INVALID_OPERATION: ['request', 'request_fixable', 'change_request'],
5
+ INVALID_AGENT_REQUEST: ['request', 'request_fixable', 'change_request'],
6
+ INVALID_FILTER: ['request', 'request_fixable', 'change_request'],
7
+ INVALID_OPERATIONS_FILE: ['request', 'request_fixable', 'change_request'],
8
+ OPERATIONS_REQUIRED: ['request', 'request_fixable', 'change_request'],
9
+ UNKNOWN_OPTION: ['request', 'request_fixable', 'change_request'],
10
+ UNEXPECTED_ARGUMENT: ['request', 'request_fixable', 'change_request'],
11
+ INVALID_ACTION: ['request', 'request_fixable', 'change_request'],
12
+ INVALID_PROFILE: ['request', 'request_fixable', 'change_request'],
13
+ INVALID_REVISION_TOKEN: ['revision-check', 'request_fixable', 'change_request'],
14
+ BATCH_OPERATION_FAILED: ['batch-execution', 'request_fixable', 'inspect_failed_operations'],
15
+ REVISION_TOKEN_SCOPE_MISMATCH: ['revision-check', 'request_fixable', 'change_request'],
16
+ REVISION_MISMATCH: ['revision-check', 'target_refresh_required', 'reinspect'],
17
+ TARGET_NOT_FOUND: ['target-resolution', 'target_refresh_required', 'reinspect'],
18
+ SOURCE_TARGET_NOT_FOUND: ['target-resolution', 'target_refresh_required', 'reinspect'],
19
+ TARGET_TEXT_MISMATCH: ['target-resolution', 'target_refresh_required', 'reinspect'],
20
+ TARGET_FINGERPRINT_MISMATCH: ['target-resolution', 'target_refresh_required', 'reinspect'],
21
+ TARGET_INDEX_MISMATCH: ['target-resolution', 'target_refresh_required', 'reinspect'],
22
+ TARGET_OCCURRENCE_MISMATCH: ['target-resolution', 'candidate_selection_required', 'choose_candidate'],
23
+ STALE_TARGET_HANDLE: ['target-resolution', 'target_refresh_required', 'reinspect'],
24
+ TARGET_HANDLE_NOT_FOUND: ['target-resolution', 'target_refresh_required', 'reinspect'],
25
+ AMBIGUOUS_TARGET: ['target-resolution', 'candidate_selection_required', 'choose_candidate'],
26
+ TARGET_RANGE_INVALID: ['target-resolution', 'request_fixable', 'change_target_range'],
27
+ ANCHOR_NOT_FOUND: ['anchor-resolution', 'request_fixable', 'change_anchor'],
28
+ AMBIGUOUS_ANCHOR: ['anchor-resolution', 'candidate_selection_required', 'choose_candidate'],
29
+ PATCH_SOURCE_NOT_FOUND: ['patch-compilation', 'request_fixable', 'change_patch'],
30
+ AMBIGUOUS_PATCH_SOURCE: ['patch-compilation', 'candidate_selection_required', 'choose_occurrence'],
31
+ OVERLAPPING_PATCHES: ['patch-compilation', 'source_conflict', 'combine_patches'],
32
+ CONFLICTING_PATCHES: ['patch-compilation', 'source_conflict', 'combine_patches'],
33
+ PATCH_ROUNDTRIP_MISMATCH: ['patch-validation', 'request_fixable', 'reinspect_and_narrow'],
34
+ DIFF_TOKEN_LIMIT: ['patch-compilation', 'request_fixable', 'narrow_operation'],
35
+ STRUCTURED_CONTENT_INVALID: ['request', 'request_fixable', 'change_request'],
36
+ UNSUPPORTED_INSERTION_AFFINITY: ['request', 'request_fixable', 'change_request'],
37
+ EXISTING_REVISIONS: ['target-safety', 'policy_choice_required', 'set_option'],
38
+ COMMENTED_CONTENT_MERGE: ['target-safety', 'user_authorization_required', 'resolve_comments'],
39
+ COMMENTED_CONTENT_DELETE: ['target-safety', 'user_authorization_required', 'resolve_comments'],
40
+ OVERLAPPING_SOURCE_TARGETS: ['batch-compilation', 'source_conflict', 'consolidate_operations'],
41
+ OVERLAPPING_TEXT_EDITS: ['batch-compilation', 'source_conflict', 'consolidate_operations'],
42
+ REVISION_ORDER_CONFLICT: ['batch-compilation', 'source_conflict', 'split_or_consolidate_operations'],
43
+ TARGET_CONSUMED_BY_OPERATION: ['batch-execution', 'source_conflict', 'use_created_content_dependency'],
44
+ CAPTURE_FANOUT_CONFLICT: ['batch-compilation', 'source_conflict', 'split_or_chain_capture_consumers'],
45
+ DUPLICATE_CAPTURE_KEY: ['batch-compilation', 'request_fixable', 'rename_capture'],
46
+ CAPTURE_NOT_FOUND: ['batch-compilation', 'request_fixable', 'add_or_correct_capture'],
47
+ CAPTURE_DEPENDENCY_CYCLE: ['batch-compilation', 'source_conflict', 'replan_batch'],
48
+ AMBIGUOUS_CAPTURE_SELECTION: ['batch-compilation', 'candidate_selection_required', 'choose_candidate'],
49
+ CAPTURE_STALE: ['batch-execution', 'source_conflict', 'replan_batch'],
50
+ GENERATED_OOXML_INVALID: ['validation', 'library_or_builder_failure', 'report_library_failure'],
51
+ DOCUMENT_SERIALIZATION_FAILED: ['serialization', 'library_or_builder_failure', 'report_library_failure'],
52
+ PACKAGE_OPERATION_FAILED: ['package', 'library_or_builder_failure', 'report_library_failure'],
53
+ PACKAGE_VALIDATION: ['package-validation', 'library_or_builder_failure', 'report_library_failure'],
54
+ MUTATION_RECEIPT_MISMATCH: ['receipt-validation', 'library_or_builder_failure', 'report_library_failure'],
55
+ FOREIGN_PARAGRAPH_MARK_DELETION: ['target-safety', 'policy_choice_required', 'use_restore_or_leave_deleted'],
56
+ RESTORATION_STATE_REQUIRED: ['target-safety', 'target_refresh_required', 'reinspect'],
57
+ RESTORATION_COUNT_MISMATCH: ['request', 'request_fixable', 'change_request'],
58
+ REJECTED_INSERTION_STATE_REQUIRED: ['target-safety', 'target_refresh_required', 'reinspect'],
59
+ UNSAFE_REVISION_NESTING: ['target-safety', 'manual_document_resolution', 'manual_resolution'],
60
+ UNSAFE_REVISION_BOUNDARY: ['target-safety', 'manual_document_resolution', 'manual_resolution'],
61
+ UNSAFE_PARAGRAPH_BOUNDARY: ['target-safety', 'manual_document_resolution', 'manual_resolution'],
62
+ UNSAFE_DELETED_TABLE_ROW: ['target-safety', 'manual_document_resolution', 'manual_resolution'],
63
+ UNSUPPORTED_MOVE_REVISION: ['target-safety', 'manual_document_resolution', 'manual_resolution'],
64
+ SECTION_BREAK_PARAGRAPH: ['target-safety', 'manual_document_resolution', 'manual_resolution'],
65
+ UNSAFE_PARAGRAPH_PLACEMENT: ['target-safety', 'manual_document_resolution', 'manual_resolution'],
66
+ UNSUPPORTED_REVISION_VIEW_MUTATION: ['target-safety', 'manual_document_resolution', 'manual_resolution']
67
+ });
68
+
69
+ function issueSummary(error) {
70
+ const issues = Array.isArray(error?.generatedIssues)
71
+ ? error.generatedIssues
72
+ : (Array.isArray(error?.issues) ? error.issues : null);
73
+ if (!issues) return null;
74
+ const byCode = new Map();
75
+ for (const issue of issues) {
76
+ const code = issue?.code || 'UNKNOWN';
77
+ byCode.set(code, (byCode.get(code) || 0) + 1);
78
+ }
79
+ return {
80
+ total: issues.length,
81
+ byCode: Array.from(byCode, ([code, count]) => ({ code, count }))
82
+ };
83
+ }
84
+
85
+ function ruleFor(code) {
86
+ return RULES[code] || ['operation', 'manual_document_resolution', 'inspect_error'];
87
+ }
88
+
89
+ /** Add stable machine recovery metadata while preserving code-specific details. */
90
+ export function normalizeErrorWithRecovery(error, context = {}) {
91
+ const source = error && typeof error === 'object' ? error : {};
92
+ const code = typeof source.code === 'string' && source.code ? source.code : 'OPERATION_ERROR';
93
+ const [defaultStage, category, action] = ruleFor(code);
94
+ const details = {};
95
+ for (const [key, value] of Object.entries(source)) {
96
+ if (key === 'name' || key === 'stack' || key === 'message' || key === 'code') continue;
97
+ details[key] = value;
98
+ }
99
+ const requiresAuthorization = category === 'user_authorization_required';
100
+ const requiresReinspection = category === 'target_refresh_required'
101
+ || code === 'PATCH_ROUNDTRIP_MISMATCH';
102
+ const recovery = {
103
+ action,
104
+ sameArgumentsSafe: false,
105
+ requiresReinspection,
106
+ requiresUserAuthorization: requiresAuthorization,
107
+ ...(code === 'EXISTING_REVISIONS' ? {
108
+ field: 'existingRevisions',
109
+ recommendedValue: 'slice-cross-author'
110
+ } : {}),
111
+ ...(source.recovery && typeof source.recovery === 'object' ? source.recovery : {})
112
+ };
113
+ const summary = issueSummary(source);
114
+ const derivedContext = {
115
+ ...context,
116
+ ...(Array.isArray(source.revisionAuthors) ? { revisionAuthors: source.revisionAuthors } : {}),
117
+ ...(source.currentPolicy ? { currentPolicy: source.currentPolicy } : {})
118
+ };
119
+ if (
120
+ ['TARGET_TEXT_MISMATCH', 'TARGET_FINGERPRINT_MISMATCH'].includes(code)
121
+ && Array.isArray(source.candidates)
122
+ && source.candidates.length === 1
123
+ ) {
124
+ derivedContext.currentTarget = source.candidates[0];
125
+ derivedContext.requiresRecomposeModified = true;
126
+ }
127
+ return {
128
+ recoveryVersion: ERROR_RECOVERY_VERSION,
129
+ code,
130
+ message: source.message || String(error),
131
+ stage: source.stage || defaultStage,
132
+ category: source.category || category,
133
+ ...details,
134
+ ...(Object.keys(derivedContext).length > 0 || source.context ? {
135
+ context: { ...(source.context || {}), ...derivedContext }
136
+ } : {}),
137
+ ...(summary ? { issueSummary: summary } : {}),
138
+ recovery
139
+ };
140
+ }
141
+
142
+ export function createRetryPlan({
143
+ atomic = false,
144
+ rolledBack = false,
145
+ results = [],
146
+ receipts = [],
147
+ operationCount = null
148
+ } = {}) {
149
+ const count = Number.isInteger(operationCount)
150
+ ? operationCount
151
+ : Math.max(results.length, receipts.length);
152
+ const attempted = new Set(results.map(result => result?.index).filter(Number.isInteger));
153
+ const failedIndexes = results
154
+ .filter(result => result?.status === 'error')
155
+ .map(result => result.index)
156
+ .filter(Number.isInteger);
157
+ const committedIndexes = receipts
158
+ .filter(receipt => receipt?.committed === true)
159
+ .map(receipt => receipt.operationIndex)
160
+ .filter(Number.isInteger);
161
+ const unattemptedIndexes = [];
162
+ for (let index = 1; index <= count; index += 1) {
163
+ if (!attempted.has(index)) unattemptedIndexes.push(index);
164
+ }
165
+ const base = rolledBack || atomic || committedIndexes.length === 0 ? 'original' : 'output';
166
+ return {
167
+ base,
168
+ committedIndexes,
169
+ failedIndexes,
170
+ unattemptedIndexes,
171
+ replayWholeBatch: base === 'original',
172
+ sameArgumentsSafe: false
173
+ };
174
+ }