@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
|
@@ -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:
|
|
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'
|
|
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
|
|
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 (
|
|
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
|
-
{
|
|
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'}).
|
|
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
|
+
}
|