@ansonlai/docx-redline-js 0.5.3 → 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 +82 -667
- package/ARCHITECTURE.md +51 -4
- package/CHANGELOG.md +11 -0
- package/README.md +176 -39
- package/core/paragraph-revision-safety.js +10 -8
- package/core/paragraph-targeting.js +14 -2
- package/core/redline-validation.js +7 -4
- package/core/revision-cloning.js +21 -0
- package/core/validation-delta.js +23 -0
- package/dist/docx-redline-js.esm.js +275 -45
- package/dist/docx-redline-js.esm.js.map +3 -3
- package/dist/docx-redline-js.esm.min.js +82 -82
- 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 +16 -2
- 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/engine/surgical-mode.js +148 -3
- package/engine/surgical-run-splitting.js +19 -7
- package/engine/surgical-spans.js +2 -1
- package/index.d.ts +17 -1
- package/node/cli.js +235 -36
- package/node/docx-document.js +137 -83
- package/node/index.d.ts +6 -2
- package/package.json +10 -3
- package/pipeline/diff-engine.js +15 -0
- 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 +99 -36
- package/services/document-operation-contract.js +50 -6
- package/services/document-operation-mutations.js +404 -41
- 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 +35 -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 -856
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getDefaultAuthor } from '../adapters/config.js';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
3
|
normalizeDocumentOperation,
|
|
4
4
|
resolveDocumentOperationAuthor,
|
|
5
5
|
validateDocumentOperation
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
applyFormattingToParagraphByExactText,
|
|
16
16
|
applyHighlightToParagraphByExactText,
|
|
17
17
|
applyParagraphFormatToParagraphByExactText,
|
|
18
|
+
insertIntoRejectedDeletedText,
|
|
18
19
|
restoreDeletedParagraphByExactText,
|
|
19
20
|
applyToParagraphByExactText
|
|
20
21
|
} from './document-operation-mutations.js';
|
|
@@ -26,22 +27,25 @@ import {
|
|
|
26
27
|
import {
|
|
27
28
|
createEmptyReceipt,
|
|
28
29
|
reconcileReceiptsAgainstOutput
|
|
29
|
-
} from './receipt-collector.js';
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
30
|
+
} from './receipt-collector.js';
|
|
31
|
+
import { validateRedlineOoxml } from '../core/redline-validation.js';
|
|
32
|
+
import { subtractValidationIssueMultiset, validationErrors } from '../core/validation-delta.js';
|
|
33
|
+
import { normalizeErrorWithRecovery } from './error-recovery.js';
|
|
34
|
+
|
|
35
|
+
export function normalizeOperationError(error, context = {}) {
|
|
36
|
+
return normalizeErrorWithRecovery(error, context);
|
|
37
|
+
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Validates and dispatches one structured operation against full document XML.
|
|
41
41
|
* Result metadata is assembled here so every mutation path exposes the same
|
|
42
42
|
* */
|
|
43
|
-
export async function applyOperationToDocumentXml(documentXml, op, author, runtimeContext = null, options = {}) {
|
|
44
|
-
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
|
+
};
|
|
45
49
|
const validation = validateDocumentOperation(op);
|
|
46
50
|
if (!validation.valid) {
|
|
47
51
|
const authorUsed = resolveDocumentOperationAuthor(op, author, getDefaultAuthor());
|
|
@@ -49,7 +53,7 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
49
53
|
documentXml,
|
|
50
54
|
hasChanges: false,
|
|
51
55
|
status: 'error',
|
|
52
|
-
error: validation.error,
|
|
56
|
+
error: normalizeOperationError(validation.error, errorContext),
|
|
53
57
|
operationType: normalizeDocumentOperation(op).operationKind,
|
|
54
58
|
authorUsed,
|
|
55
59
|
receipt: createEmptyReceipt(operationIndex, op?.operationId, authorUsed, 'refused')
|
|
@@ -61,6 +65,8 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
61
65
|
|
|
62
66
|
if (
|
|
63
67
|
operation.operationKind !== 'comment_reply'
|
|
68
|
+
&& operation.operationKind !== 'rejected-insert'
|
|
69
|
+
&& operation.operationKind !== 'restore'
|
|
64
70
|
&& (
|
|
65
71
|
operation.targetDescriptor?.revisionView === 'rejected'
|
|
66
72
|
|| operation.targetEndDescriptor?.revisionView === 'rejected'
|
|
@@ -70,10 +76,10 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
70
76
|
documentXml,
|
|
71
77
|
hasChanges: false,
|
|
72
78
|
status: 'error',
|
|
73
|
-
error: {
|
|
74
|
-
code: 'UNSUPPORTED_REVISION_VIEW_MUTATION',
|
|
75
|
-
message: 'Targeting rejected revision view for mutation is not supported yet.'
|
|
76
|
-
},
|
|
79
|
+
error: normalizeOperationError({
|
|
80
|
+
code: 'UNSUPPORTED_REVISION_VIEW_MUTATION',
|
|
81
|
+
message: 'Targeting rejected revision view for mutation is not supported yet.'
|
|
82
|
+
}, errorContext),
|
|
77
83
|
operationType: operation.operationKind,
|
|
78
84
|
authorUsed,
|
|
79
85
|
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
@@ -87,10 +93,10 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
87
93
|
documentXml,
|
|
88
94
|
hasChanges: false,
|
|
89
95
|
status: 'error',
|
|
90
|
-
error: {
|
|
91
|
-
code: tokenValidation.error?.code || 'INVALID_REVISION_TOKEN',
|
|
92
|
-
message: tokenValidation.error?.message || 'Invalid revision token.'
|
|
93
|
-
},
|
|
96
|
+
error: normalizeOperationError({
|
|
97
|
+
code: tokenValidation.error?.code || 'INVALID_REVISION_TOKEN',
|
|
98
|
+
message: tokenValidation.error?.message || 'Invalid revision token.'
|
|
99
|
+
}, errorContext),
|
|
94
100
|
operationType: operation.operationKind,
|
|
95
101
|
authorUsed,
|
|
96
102
|
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
@@ -101,10 +107,10 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
101
107
|
documentXml,
|
|
102
108
|
hasChanges: false,
|
|
103
109
|
status: 'error',
|
|
104
|
-
error: {
|
|
105
|
-
code: 'REVISION_TOKEN_SCOPE_MISMATCH',
|
|
106
|
-
message: `Revision token scope mismatch: expected 'document-parts', got '${options.expectedRevision.scope}'.`
|
|
107
|
-
},
|
|
110
|
+
error: normalizeOperationError({
|
|
111
|
+
code: 'REVISION_TOKEN_SCOPE_MISMATCH',
|
|
112
|
+
message: `Revision token scope mismatch: expected 'document-parts', got '${options.expectedRevision.scope}'.`
|
|
113
|
+
}, errorContext),
|
|
108
114
|
operationType: operation.operationKind,
|
|
109
115
|
authorUsed,
|
|
110
116
|
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
@@ -122,10 +128,12 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
122
128
|
documentXml,
|
|
123
129
|
hasChanges: false,
|
|
124
130
|
status: 'error',
|
|
125
|
-
error: {
|
|
126
|
-
code: 'REVISION_MISMATCH',
|
|
127
|
-
message: `Document revision mismatch: expected '${options.expectedRevision.value}', current is '${currentToken.value}'
|
|
128
|
-
|
|
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),
|
|
129
137
|
operationType: operation.operationKind,
|
|
130
138
|
authorUsed,
|
|
131
139
|
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
@@ -141,7 +149,7 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
141
149
|
documentXml,
|
|
142
150
|
hasChanges: false,
|
|
143
151
|
status: 'error',
|
|
144
|
-
error: session.parseResult.error,
|
|
152
|
+
error: normalizeOperationError(session.parseResult.error, errorContext),
|
|
145
153
|
warnings: session.parseResult.warnings,
|
|
146
154
|
operationType: operation.operationKind,
|
|
147
155
|
authorUsed,
|
|
@@ -156,7 +164,9 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
156
164
|
operation.operationId,
|
|
157
165
|
authorUsed
|
|
158
166
|
);
|
|
159
|
-
const operationWarnings =
|
|
167
|
+
const operationWarnings = Array.isArray(operation._compiledWarnings)
|
|
168
|
+
? [...operation._compiledWarnings]
|
|
169
|
+
: [];
|
|
160
170
|
const operationOptions = {
|
|
161
171
|
...options,
|
|
162
172
|
...(typeof operation.generateRedlines === 'boolean' ? { generateRedlines: operation.generateRedlines } : {}),
|
|
@@ -168,6 +178,10 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
168
178
|
...(operation.formattingRevisionPolicy ? { formattingRevisionPolicy: operation.formattingRevisionPolicy } : {}),
|
|
169
179
|
targetDescriptor: operation.targetDescriptor,
|
|
170
180
|
targetEndDescriptor: operation.targetEndDescriptor,
|
|
181
|
+
_compiledSourceId: operation._compiledSourceId || null,
|
|
182
|
+
_compiledSourceEndId: operation._compiledSourceEndId || null,
|
|
183
|
+
_compiledResolvedBy: operation._compiledResolvedBy || null,
|
|
184
|
+
_sourceTargetRegistry: session.sourceTargetRegistry || null,
|
|
171
185
|
_resolutionCapture: resolutionCapture,
|
|
172
186
|
_revisionIdAllocator: session.revisionIdAllocator,
|
|
173
187
|
_documentOperationSession: session,
|
|
@@ -250,6 +264,17 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
250
264
|
runtimeContext,
|
|
251
265
|
operationOptions
|
|
252
266
|
);
|
|
267
|
+
} else if (operation.operationKind === 'rejected-insert') {
|
|
268
|
+
result = await insertIntoRejectedDeletedText(
|
|
269
|
+
documentXml,
|
|
270
|
+
operation.target,
|
|
271
|
+
operation.anchor,
|
|
272
|
+
operation.modified,
|
|
273
|
+
authorUsed,
|
|
274
|
+
operation.targetRef,
|
|
275
|
+
runtimeContext,
|
|
276
|
+
operationOptions
|
|
277
|
+
);
|
|
253
278
|
} else if (operation.operationKind === 'restore') {
|
|
254
279
|
result = await restoreDeletedParagraphByExactText(
|
|
255
280
|
documentXml,
|
|
@@ -311,8 +336,45 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
311
336
|
operationReceipt.warnings.push(String(w));
|
|
312
337
|
}
|
|
313
338
|
}
|
|
314
|
-
} else {
|
|
315
|
-
|
|
339
|
+
} else {
|
|
340
|
+
const beforeValidation = validateRedlineOoxml(savepoint.document);
|
|
341
|
+
const afterValidation = validateRedlineOoxml(session.document);
|
|
342
|
+
const generatedIssues = subtractValidationIssueMultiset(afterValidation.issues, beforeValidation.issues);
|
|
343
|
+
const generatedErrors = validationErrors(generatedIssues);
|
|
344
|
+
if (generatedErrors.length > 0) {
|
|
345
|
+
session.restoreSavepoint(savepoint);
|
|
346
|
+
operationReceipt = createEmptyReceipt(
|
|
347
|
+
operationIndex,
|
|
348
|
+
operation.operationId,
|
|
349
|
+
authorUsed,
|
|
350
|
+
'refused'
|
|
351
|
+
);
|
|
352
|
+
const codes = [...new Set(generatedErrors.map(issue => issue.code))].join(', ');
|
|
353
|
+
return {
|
|
354
|
+
documentXml,
|
|
355
|
+
hasChanges: false,
|
|
356
|
+
status: 'error',
|
|
357
|
+
error: normalizeOperationError({
|
|
358
|
+
code: 'GENERATED_OOXML_INVALID',
|
|
359
|
+
stage: 'validation',
|
|
360
|
+
message: `Operation introduced invalid OOXML (${codes}).`,
|
|
361
|
+
generatedIssues: generatedErrors
|
|
362
|
+
}, errorContext),
|
|
363
|
+
operationType: operation.operationKind,
|
|
364
|
+
authorUsed,
|
|
365
|
+
receipt: operationReceipt,
|
|
366
|
+
...resolutionCapture
|
|
367
|
+
};
|
|
368
|
+
}
|
|
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
|
+
}
|
|
316
378
|
if (operation.captureKey && session.captureTable) {
|
|
317
379
|
session.captureTable.set(
|
|
318
380
|
operation.captureKey,
|
|
@@ -349,7 +411,7 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
349
411
|
documentXml,
|
|
350
412
|
hasChanges: false,
|
|
351
413
|
status: 'error',
|
|
352
|
-
error: reconciliation.error,
|
|
414
|
+
error: normalizeOperationError(reconciliation.error, errorContext),
|
|
353
415
|
warnings: [reconciliation.error.message],
|
|
354
416
|
operationType: operation.operationKind,
|
|
355
417
|
authorUsed,
|
|
@@ -359,7 +421,8 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
359
421
|
}
|
|
360
422
|
}
|
|
361
423
|
}
|
|
362
|
-
|
|
424
|
+
if (result?.error) result.error = normalizeOperationError(result.error, errorContext);
|
|
425
|
+
return {
|
|
363
426
|
...result,
|
|
364
427
|
operationType: operation.operationKind,
|
|
365
428
|
authorUsed,
|
|
@@ -368,7 +431,7 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
368
431
|
};
|
|
369
432
|
} catch (error) {
|
|
370
433
|
session.restoreSavepoint(savepoint);
|
|
371
|
-
const normalizedError = normalizeOperationError(error);
|
|
434
|
+
const normalizedError = normalizeOperationError(error, errorContext);
|
|
372
435
|
const operationReceipt = createEmptyReceipt(
|
|
373
436
|
operationIndex,
|
|
374
437
|
operation.operationId,
|
|
@@ -43,6 +43,7 @@ function nonEmptyString(value) {
|
|
|
43
43
|
|
|
44
44
|
export function getCanonicalOperationType(operation) {
|
|
45
45
|
const type = operation?.type;
|
|
46
|
+
if (type === 'insert' && operation?.target?.revisionView === 'rejected') return 'rejected-insert';
|
|
46
47
|
if (type === 'restore') return 'restore';
|
|
47
48
|
if (type === 'comment' || type === 'comment_reply' || type === 'highlight') return type;
|
|
48
49
|
if (type === 'paragraph-format') return 'paragraph-format';
|
|
@@ -50,7 +51,8 @@ export function getCanonicalOperationType(operation) {
|
|
|
50
51
|
return 'redline';
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
export function normalizeTargetDescriptor(target, legacyTargetRef = null) {
|
|
54
|
+
export function normalizeTargetDescriptor(target, legacyTargetRef = null, defaultRevisionView = 'accepted') {
|
|
55
|
+
const fallbackRevisionView = defaultRevisionView === 'rejected' ? 'rejected' : 'accepted';
|
|
54
56
|
if (!isRecord(target)) {
|
|
55
57
|
return {
|
|
56
58
|
text: typeof target === 'string' ? target : '',
|
|
@@ -59,7 +61,7 @@ export function normalizeTargetDescriptor(target, legacyTargetRef = null) {
|
|
|
59
61
|
occurrence: null,
|
|
60
62
|
inTable: null,
|
|
61
63
|
fingerprint: null,
|
|
62
|
-
revisionView:
|
|
64
|
+
revisionView: fallbackRevisionView
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -76,7 +78,9 @@ export function normalizeTargetDescriptor(target, legacyTargetRef = null) {
|
|
|
76
78
|
fingerprint: nonEmptyString(target.fingerprint)
|
|
77
79
|
? target.fingerprint.trim()
|
|
78
80
|
: (nonEmptyString(target.sourceFingerprint) ? target.sourceFingerprint.trim() : null),
|
|
79
|
-
revisionView: target.revisionView === 'rejected'
|
|
81
|
+
revisionView: target.revisionView === 'rejected'
|
|
82
|
+
? 'rejected'
|
|
83
|
+
: (target.revisionView === 'accepted' ? 'accepted' : fallbackRevisionView),
|
|
80
84
|
captureRef: nonEmptyString(target.captureRef) ? target.captureRef.trim() : null,
|
|
81
85
|
select: typeof target.select === 'string' ? target.select : null
|
|
82
86
|
};
|
|
@@ -84,17 +88,24 @@ export function normalizeTargetDescriptor(target, legacyTargetRef = null) {
|
|
|
84
88
|
|
|
85
89
|
export function normalizeDocumentOperation(operation) {
|
|
86
90
|
const source = isRecord(operation) ? operation : {};
|
|
87
|
-
const
|
|
91
|
+
const kind = getCanonicalOperationType(source);
|
|
92
|
+
const defaultRevisionView = kind === 'restore' ? 'rejected' : 'accepted';
|
|
93
|
+
const targetDescriptor = normalizeTargetDescriptor(source.target, source.targetRef, defaultRevisionView);
|
|
88
94
|
const targetEndDescriptor = source.targetEnd != null
|
|
89
|
-
? normalizeTargetDescriptor(source.targetEnd, source.targetEndRef)
|
|
95
|
+
? normalizeTargetDescriptor(source.targetEnd, source.targetEndRef, defaultRevisionView)
|
|
90
96
|
: null;
|
|
91
|
-
const kind = getCanonicalOperationType(source);
|
|
92
97
|
|
|
93
98
|
return {
|
|
94
99
|
...source,
|
|
95
100
|
operationId: nonEmptyString(source.operationId) ? source.operationId.trim() : null,
|
|
96
101
|
captureKey: nonEmptyString(source.captureKey) ? source.captureKey.trim() : null,
|
|
97
102
|
operationKind: kind,
|
|
103
|
+
anchor: isRecord(source.anchor) ? {
|
|
104
|
+
exactText: typeof source.anchor.exactText === 'string' ? source.anchor.exactText : '',
|
|
105
|
+
occurrence: Number.isInteger(source.anchor.occurrence) && source.anchor.occurrence > 0 ? source.anchor.occurrence : 1,
|
|
106
|
+
occurrenceExplicit: Number.isInteger(source.anchor.occurrence) && source.anchor.occurrence > 0,
|
|
107
|
+
offset: Number.isInteger(source.anchor.offset) ? source.anchor.offset : null
|
|
108
|
+
} : null,
|
|
98
109
|
targetDescriptor,
|
|
99
110
|
targetEndDescriptor,
|
|
100
111
|
target: targetDescriptor.text,
|
|
@@ -211,6 +222,39 @@ export function validateDocumentOperation(operation) {
|
|
|
211
222
|
};
|
|
212
223
|
}
|
|
213
224
|
|
|
225
|
+
if (normalized.operationKind === 'rejected-insert') {
|
|
226
|
+
if (!nonEmptyString(normalized.modified)) {
|
|
227
|
+
return {
|
|
228
|
+
valid: false,
|
|
229
|
+
error: { code: 'INVALID_OPERATION', message: 'Rejected-view insert operations require non-empty string "modified" text.' }
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
if (
|
|
233
|
+
!normalized.anchor
|
|
234
|
+
|| !nonEmptyString(normalized.anchor.exactText)
|
|
235
|
+
|| !Number.isInteger(normalized.anchor.offset)
|
|
236
|
+
|| normalized.anchor.offset < 0
|
|
237
|
+
|| normalized.anchor.offset > normalized.anchor.exactText.length
|
|
238
|
+
) {
|
|
239
|
+
return {
|
|
240
|
+
valid: false,
|
|
241
|
+
error: {
|
|
242
|
+
code: 'INVALID_OPERATION',
|
|
243
|
+
message: 'Rejected-view insert operations require anchor.exactText, a positive occurrence, and an offset within the anchor text.'
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (normalized.existingRevisions !== 'slice-cross-author') {
|
|
248
|
+
return {
|
|
249
|
+
valid: false,
|
|
250
|
+
error: {
|
|
251
|
+
code: 'INVALID_OPERATION',
|
|
252
|
+
message: 'Rejected-view insert operations require existingRevisions: "slice-cross-author".'
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
214
258
|
if (normalized.operationKind === 'restore') {
|
|
215
259
|
const validSingle = nonEmptyString(normalized.modified);
|
|
216
260
|
const validRange = Array.isArray(normalized.modified)
|