@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
@@ -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
+ }