@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.
- package/AGENTS.md +82 -697
- package/ARCHITECTURE.md +13 -1
- package/CHANGELOG.md +8 -0
- package/README.md +177 -45
- package/core/paragraph-targeting.js +14 -2
- package/dist/docx-redline-js.esm.js +184 -51
- package/dist/docx-redline-js.esm.js.map +3 -3
- package/dist/docx-redline-js.esm.min.js +77 -77
- 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 +878 -0
- package/docs/SKILL_AUTHORING.md +126 -0
- package/docs/TESTING.md +35 -1
- package/docs/schemas/document-operations.schema.json +5 -1
- package/docs/validation-reports/2026-09-12-agent-cli-discovery-baseline.md +56 -0
- package/docs/validation-reports/2026-09-12-agent-protocol-rollout.md +86 -0
- package/docs/validation-reports/2026-09-13-agent-cli-efficiency-rollout.md +86 -0
- package/engine/oxml-engine.js +80 -13
- package/engine/run-builders.js +5 -15
- package/index.d.ts +28 -3
- package/node/cli-help.js +209 -0
- package/node/cli.js +323 -65
- package/node/docx-document.js +120 -69
- package/node/index.d.ts +6 -2
- package/package.json +15 -3
- package/scripts/generate-cross-author-slicing-fixtures.ps1 +25 -25
- package/services/batch-operation-orchestrator.js +215 -120
- package/services/document-inspection.js +89 -11
- 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
|
@@ -30,21 +30,22 @@ import {
|
|
|
30
30
|
} from './receipt-collector.js';
|
|
31
31
|
import { validateRedlineOoxml } from '../core/redline-validation.js';
|
|
32
32
|
import { subtractValidationIssueMultiset, validationErrors } from '../core/validation-delta.js';
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
...(Array.isArray(error?.candidates) ? { candidates: error.candidates } : {})
|
|
39
|
-
};
|
|
40
|
-
}
|
|
33
|
+
import { normalizeErrorWithRecovery } from './error-recovery.js';
|
|
34
|
+
|
|
35
|
+
export function normalizeOperationError(error, context = {}) {
|
|
36
|
+
return normalizeErrorWithRecovery(error, context);
|
|
37
|
+
}
|
|
41
38
|
|
|
42
39
|
/**
|
|
43
40
|
* Validates and dispatches one structured operation against full document XML.
|
|
44
41
|
* Result metadata is assembled here so every mutation path exposes the same
|
|
45
42
|
* */
|
|
46
|
-
export async function applyOperationToDocumentXml(documentXml, op, author, runtimeContext = null, options = {}) {
|
|
47
|
-
const operationIndex = typeof options._operationIndex === 'number' ? options._operationIndex : 1;
|
|
43
|
+
export async function applyOperationToDocumentXml(documentXml, op, author, runtimeContext = null, options = {}) {
|
|
44
|
+
const operationIndex = typeof options._operationIndex === 'number' ? options._operationIndex : 1;
|
|
45
|
+
const errorContext = {
|
|
46
|
+
operationIndex,
|
|
47
|
+
...(typeof op?.operationId === 'string' ? { operationId: op.operationId } : {})
|
|
48
|
+
};
|
|
48
49
|
const validation = validateDocumentOperation(op);
|
|
49
50
|
if (!validation.valid) {
|
|
50
51
|
const authorUsed = resolveDocumentOperationAuthor(op, author, getDefaultAuthor());
|
|
@@ -52,7 +53,7 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
52
53
|
documentXml,
|
|
53
54
|
hasChanges: false,
|
|
54
55
|
status: 'error',
|
|
55
|
-
error: validation.error,
|
|
56
|
+
error: normalizeOperationError(validation.error, errorContext),
|
|
56
57
|
operationType: normalizeDocumentOperation(op).operationKind,
|
|
57
58
|
authorUsed,
|
|
58
59
|
receipt: createEmptyReceipt(operationIndex, op?.operationId, authorUsed, 'refused')
|
|
@@ -75,10 +76,10 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
75
76
|
documentXml,
|
|
76
77
|
hasChanges: false,
|
|
77
78
|
status: 'error',
|
|
78
|
-
error: {
|
|
79
|
-
code: 'UNSUPPORTED_REVISION_VIEW_MUTATION',
|
|
80
|
-
message: 'Targeting rejected revision view for mutation is not supported yet.'
|
|
81
|
-
},
|
|
79
|
+
error: normalizeOperationError({
|
|
80
|
+
code: 'UNSUPPORTED_REVISION_VIEW_MUTATION',
|
|
81
|
+
message: 'Targeting rejected revision view for mutation is not supported yet.'
|
|
82
|
+
}, errorContext),
|
|
82
83
|
operationType: operation.operationKind,
|
|
83
84
|
authorUsed,
|
|
84
85
|
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
@@ -92,10 +93,10 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
92
93
|
documentXml,
|
|
93
94
|
hasChanges: false,
|
|
94
95
|
status: 'error',
|
|
95
|
-
error: {
|
|
96
|
-
code: tokenValidation.error?.code || 'INVALID_REVISION_TOKEN',
|
|
97
|
-
message: tokenValidation.error?.message || 'Invalid revision token.'
|
|
98
|
-
},
|
|
96
|
+
error: normalizeOperationError({
|
|
97
|
+
code: tokenValidation.error?.code || 'INVALID_REVISION_TOKEN',
|
|
98
|
+
message: tokenValidation.error?.message || 'Invalid revision token.'
|
|
99
|
+
}, errorContext),
|
|
99
100
|
operationType: operation.operationKind,
|
|
100
101
|
authorUsed,
|
|
101
102
|
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
@@ -106,10 +107,10 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
106
107
|
documentXml,
|
|
107
108
|
hasChanges: false,
|
|
108
109
|
status: 'error',
|
|
109
|
-
error: {
|
|
110
|
-
code: 'REVISION_TOKEN_SCOPE_MISMATCH',
|
|
111
|
-
message: `Revision token scope mismatch: expected 'document-parts', got '${options.expectedRevision.scope}'.`
|
|
112
|
-
},
|
|
110
|
+
error: normalizeOperationError({
|
|
111
|
+
code: 'REVISION_TOKEN_SCOPE_MISMATCH',
|
|
112
|
+
message: `Revision token scope mismatch: expected 'document-parts', got '${options.expectedRevision.scope}'.`
|
|
113
|
+
}, errorContext),
|
|
113
114
|
operationType: operation.operationKind,
|
|
114
115
|
authorUsed,
|
|
115
116
|
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
@@ -127,10 +128,12 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
127
128
|
documentXml,
|
|
128
129
|
hasChanges: false,
|
|
129
130
|
status: 'error',
|
|
130
|
-
error: {
|
|
131
|
-
code: 'REVISION_MISMATCH',
|
|
132
|
-
message: `Document revision mismatch: expected '${options.expectedRevision.value}', current is '${currentToken.value}'
|
|
133
|
-
|
|
131
|
+
error: normalizeOperationError({
|
|
132
|
+
code: 'REVISION_MISMATCH',
|
|
133
|
+
message: `Document revision mismatch: expected '${options.expectedRevision.value}', current is '${currentToken.value}'.`,
|
|
134
|
+
expectedRevision: options.expectedRevision,
|
|
135
|
+
currentRevision: currentToken
|
|
136
|
+
}, errorContext),
|
|
134
137
|
operationType: operation.operationKind,
|
|
135
138
|
authorUsed,
|
|
136
139
|
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
@@ -146,7 +149,7 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
146
149
|
documentXml,
|
|
147
150
|
hasChanges: false,
|
|
148
151
|
status: 'error',
|
|
149
|
-
error: session.parseResult.error,
|
|
152
|
+
error: normalizeOperationError(session.parseResult.error, errorContext),
|
|
150
153
|
warnings: session.parseResult.warnings,
|
|
151
154
|
operationType: operation.operationKind,
|
|
152
155
|
authorUsed,
|
|
@@ -161,7 +164,9 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
161
164
|
operation.operationId,
|
|
162
165
|
authorUsed
|
|
163
166
|
);
|
|
164
|
-
const operationWarnings =
|
|
167
|
+
const operationWarnings = Array.isArray(operation._compiledWarnings)
|
|
168
|
+
? [...operation._compiledWarnings]
|
|
169
|
+
: [];
|
|
165
170
|
const operationOptions = {
|
|
166
171
|
...options,
|
|
167
172
|
...(typeof operation.generateRedlines === 'boolean' ? { generateRedlines: operation.generateRedlines } : {}),
|
|
@@ -173,6 +178,10 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
173
178
|
...(operation.formattingRevisionPolicy ? { formattingRevisionPolicy: operation.formattingRevisionPolicy } : {}),
|
|
174
179
|
targetDescriptor: operation.targetDescriptor,
|
|
175
180
|
targetEndDescriptor: operation.targetEndDescriptor,
|
|
181
|
+
_compiledSourceId: operation._compiledSourceId || null,
|
|
182
|
+
_compiledSourceEndId: operation._compiledSourceEndId || null,
|
|
183
|
+
_compiledResolvedBy: operation._compiledResolvedBy || null,
|
|
184
|
+
_sourceTargetRegistry: session.sourceTargetRegistry || null,
|
|
176
185
|
_resolutionCapture: resolutionCapture,
|
|
177
186
|
_revisionIdAllocator: session.revisionIdAllocator,
|
|
178
187
|
_documentOperationSession: session,
|
|
@@ -345,12 +354,12 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
345
354
|
documentXml,
|
|
346
355
|
hasChanges: false,
|
|
347
356
|
status: 'error',
|
|
348
|
-
error: {
|
|
357
|
+
error: normalizeOperationError({
|
|
349
358
|
code: 'GENERATED_OOXML_INVALID',
|
|
350
359
|
stage: 'validation',
|
|
351
360
|
message: `Operation introduced invalid OOXML (${codes}).`,
|
|
352
361
|
generatedIssues: generatedErrors
|
|
353
|
-
},
|
|
362
|
+
}, errorContext),
|
|
354
363
|
operationType: operation.operationKind,
|
|
355
364
|
authorUsed,
|
|
356
365
|
receipt: operationReceipt,
|
|
@@ -358,6 +367,14 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
358
367
|
};
|
|
359
368
|
}
|
|
360
369
|
session.markMutationCommitted(operation.operationKind !== 'comment_reply');
|
|
370
|
+
if (operation._compiledSourceId && session.sourceTargetRegistry) {
|
|
371
|
+
session.sourceTargetRegistry.commitMutation(
|
|
372
|
+
operation._compiledSourceId,
|
|
373
|
+
operationOptions._mutationRemovedNodes,
|
|
374
|
+
operationOptions._mutationLiveNodes,
|
|
375
|
+
operationIndex
|
|
376
|
+
);
|
|
377
|
+
}
|
|
361
378
|
if (operation.captureKey && session.captureTable) {
|
|
362
379
|
session.captureTable.set(
|
|
363
380
|
operation.captureKey,
|
|
@@ -394,7 +411,7 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
394
411
|
documentXml,
|
|
395
412
|
hasChanges: false,
|
|
396
413
|
status: 'error',
|
|
397
|
-
error: reconciliation.error,
|
|
414
|
+
error: normalizeOperationError(reconciliation.error, errorContext),
|
|
398
415
|
warnings: [reconciliation.error.message],
|
|
399
416
|
operationType: operation.operationKind,
|
|
400
417
|
authorUsed,
|
|
@@ -404,7 +421,8 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
404
421
|
}
|
|
405
422
|
}
|
|
406
423
|
}
|
|
407
|
-
|
|
424
|
+
if (result?.error) result.error = normalizeOperationError(result.error, errorContext);
|
|
425
|
+
return {
|
|
408
426
|
...result,
|
|
409
427
|
operationType: operation.operationKind,
|
|
410
428
|
authorUsed,
|
|
@@ -413,7 +431,7 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
413
431
|
};
|
|
414
432
|
} catch (error) {
|
|
415
433
|
session.restoreSavepoint(savepoint);
|
|
416
|
-
const normalizedError = normalizeOperationError(error);
|
|
434
|
+
const normalizedError = normalizeOperationError(error, errorContext);
|
|
417
435
|
const operationReceipt = createEmptyReceipt(
|
|
418
436
|
operationIndex,
|
|
419
437
|
operation.operationId,
|
|
@@ -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
|
+
}
|