@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.
Files changed (59) hide show
  1. package/AGENTS.md +82 -667
  2. package/ARCHITECTURE.md +51 -4
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +176 -39
  5. package/core/paragraph-revision-safety.js +10 -8
  6. package/core/paragraph-targeting.js +14 -2
  7. package/core/redline-validation.js +7 -4
  8. package/core/revision-cloning.js +21 -0
  9. package/core/validation-delta.js +23 -0
  10. package/dist/docx-redline-js.esm.js +275 -45
  11. package/dist/docx-redline-js.esm.js.map +3 -3
  12. package/dist/docx-redline-js.esm.min.js +82 -82
  13. package/dist/docx-redline-js.esm.min.js.map +4 -4
  14. package/docs/AGENT_FAST_START.md +59 -0
  15. package/docs/AGENT_KNOWLEDGE_BASE.md +868 -0
  16. package/docs/TESTING.md +20 -1
  17. package/docs/schemas/document-operations.schema.json +16 -2
  18. package/docs/validation-reports/2026-09-12-agent-protocol-rollout.md +82 -0
  19. package/engine/oxml-engine.js +80 -13
  20. package/engine/run-builders.js +5 -15
  21. package/engine/surgical-mode.js +148 -3
  22. package/engine/surgical-run-splitting.js +19 -7
  23. package/engine/surgical-spans.js +2 -1
  24. package/index.d.ts +17 -1
  25. package/node/cli.js +235 -36
  26. package/node/docx-document.js +137 -83
  27. package/node/index.d.ts +6 -2
  28. package/package.json +10 -3
  29. package/pipeline/diff-engine.js +15 -0
  30. package/scripts/generate-cross-author-slicing-fixtures.ps1 +25 -25
  31. package/services/batch-operation-orchestrator.js +215 -120
  32. package/services/document-inspection.js +5 -3
  33. package/services/document-operation-applier.js +99 -36
  34. package/services/document-operation-contract.js +50 -6
  35. package/services/document-operation-mutations.js +404 -41
  36. package/services/document-operation-session.js +4 -0
  37. package/services/error-recovery.js +174 -0
  38. package/services/operation-batch-compiler.js +394 -0
  39. package/services/operation-preflight.js +91 -72
  40. package/services/standalone-operation-runner.d.ts +35 -1
  41. package/docs/plans/2026-09-05-structural-revisions-and-fidelity-oracles.md +0 -1669
  42. package/docs/plans/2026-09-08-cross-author-revision-slicing.md +0 -856
  43. package/docs/plans/completed/2026-03-01-release-0.1.4-design.md +0 -33
  44. package/docs/plans/completed/2026-03-01-release-0.1.4.md +0 -110
  45. package/docs/plans/completed/2026-05-31-architectural changes.md +0 -593
  46. package/docs/plans/completed/2026-08-02-reliability-improvements.md +0 -1155
  47. package/docs/plans/completed/2026-08-30-reliability-testing-improvements.md +0 -488
  48. package/docs/plans/completed/2026-09-01-performance-and-complexity-reduction.md +0 -669
  49. package/docs/plans/completed/2026-09-03-agent-friendly-document-workflows.md +0 -427
  50. package/docs/plans/completed/2026-09-04-comment-anchor-and-cli-reliability.md +0 -519
  51. package/docs/plans/completed/PERFORMANCE-CONSOLIDATION.md +0 -69
  52. package/docs/plans/completed/structural-revision-capability-matrix.md +0 -115
  53. package/docs/test-comparison-dashboard.html +0 -4338
  54. package/docs/validation-reports/2026-08-30-phase-1-word-visual-preflight.md +0 -22
  55. package/docs/validation-reports/2026-08-30-phase-2-word-visual-preflight.md +0 -24
  56. package/docs/validation-reports/2026-08-30-phase-3-coverage.md +0 -73
  57. package/docs/validation-reports/2026-09-02-multilevel-bullets-visual-review.md +0 -82
  58. package/docs/validation-reports/2026-09-02-multimodal-visual-samples.md +0 -114
  59. package/docs/validation-reports/2026-09-02-visual-failures-preflight.md +0 -79
@@ -29,14 +29,12 @@ import {
29
29
  resolveDocumentOperationAuthor,
30
30
  validateDocumentOperation
31
31
  } from './document-operation-contract.js';
32
- import { buildOperationDependencyPlan } from './batch-operation-orchestrator.js';
33
-
34
- function normalizedError(error) {
35
- return {
36
- code: typeof error?.code === 'string' && error.code ? error.code : 'OPERATION_ERROR',
37
- message: error?.message || String(error),
38
- ...(Array.isArray(error?.candidates) ? { candidates: error.candidates } : {})
39
- };
32
+ import { buildOperationDependencyPlan } from './batch-operation-orchestrator.js';
33
+ import { compileOperationBatch } from './operation-batch-compiler.js';
34
+ import { normalizeErrorWithRecovery } from './error-recovery.js';
35
+
36
+ function normalizedError(error, context = {}) {
37
+ return normalizeErrorWithRecovery(error, context);
40
38
  }
41
39
 
42
40
  function operationNeedsNumbering(operation) {
@@ -70,10 +68,6 @@ function targetMetadata(xmlDoc, paragraph, resolvedBy, suppliedText, paragraphMe
70
68
  };
71
69
  }
72
70
 
73
- function buildConflict(code, message, operationIndexes, target) {
74
- return { code, message, operationIndexes, target };
75
- }
76
-
77
71
  function getCommentIdsInParagraph(paragraph) {
78
72
  const ids = new Set();
79
73
  for (const localName of ['commentRangeStart', 'commentRangeEnd', 'commentReference']) {
@@ -90,10 +84,10 @@ export function preflightOperations(documentXml, operations, author, options = {
90
84
  return {
91
85
  valid: false,
92
86
  status: 'error',
93
- error: {
87
+ error: normalizedError({
94
88
  code: 'INVALID_OPERATION',
95
89
  message: `Unsupported existingRevisions policy: "${String(options.existingRevisions)}".`
96
- },
90
+ }),
97
91
  results: [],
98
92
  conflicts: [],
99
93
  authorsUsed: [],
@@ -105,7 +99,7 @@ export function preflightOperations(documentXml, operations, author, options = {
105
99
  return {
106
100
  valid: false,
107
101
  status: 'error',
108
- error: parsed.error,
102
+ error: normalizedError(parsed.error),
109
103
  results: [],
110
104
  conflicts: [],
111
105
  authorsUsed: [],
@@ -119,19 +113,23 @@ export function preflightOperations(documentXml, operations, author, options = {
119
113
  rejected: null
120
114
  };
121
115
  const sourceOperations = Array.isArray(operations) ? operations : [];
122
- const dependencyPlan = buildOperationDependencyPlan(sourceOperations);
123
- if (!dependencyPlan.valid) {
116
+ const batchCompilation = compileOperationBatch(xmlDoc, sourceOperations, {
117
+ strictTargets: options.strictTargets !== false,
118
+ onInfo: options.onInfo,
119
+ onWarn: options.onWarn
120
+ });
121
+ const dependencyPlan = buildOperationDependencyPlan(batchCompilation.compiledOperations);
122
+ if (!dependencyPlan.valid) {
124
123
  return {
125
124
  valid: false,
126
125
  status: 'error',
127
- error: dependencyPlan.error,
126
+ error: normalizedError(dependencyPlan.error),
128
127
  results: [],
129
128
  conflicts: [],
130
129
  authorsUsed: [],
131
130
  requiredArtifacts: { comments: false, numbering: false }
132
131
  };
133
- }
134
-
132
+ }
135
133
  const strictTargets = options.strictTargets !== false;
136
134
  const results = [];
137
135
  const authorsUsed = new Set();
@@ -175,7 +173,7 @@ export function preflightOperations(documentXml, operations, author, options = {
175
173
  continue;
176
174
  }
177
175
 
178
- if (operation.targetDescriptor?.captureRef) {
176
+ if (operation.targetDescriptor?.captureRef) {
179
177
  results.push({
180
178
  index: index + 1,
181
179
  type: sourceOperation?.type || 'redline',
@@ -186,8 +184,35 @@ export function preflightOperations(documentXml, operations, author, options = {
186
184
  captureRef: operation.targetDescriptor.captureRef,
187
185
  ...(operation.targetDescriptor.select ? { select: operation.targetDescriptor.select } : {})
188
186
  });
189
- continue;
190
- }
187
+ continue;
188
+ }
189
+
190
+ const compiledBinding = batchCompilation.bindings[index];
191
+ if (compiledBinding?.error) {
192
+ results.push({
193
+ index: index + 1,
194
+ type: sourceOperation?.type || 'redline',
195
+ operationType: operation.operationKind,
196
+ status: 'error',
197
+ authorUsed,
198
+ error: compiledBinding.error
199
+ });
200
+ continue;
201
+ }
202
+ if (compiledBinding?.createdByOperation) {
203
+ results.push({
204
+ index: index + 1,
205
+ type: sourceOperation?.type || 'redline',
206
+ operationType: operation.operationKind,
207
+ status: 'deferred',
208
+ authorUsed,
209
+ resolvedBy: 'created_content_dependency',
210
+ captureRef: compiledBinding.captureRef,
211
+ select: operation.targetDescriptor.text,
212
+ createdByOperation: compiledBinding.createdByOperation
213
+ });
214
+ continue;
215
+ }
191
216
 
192
217
  const targetView = operation.targetDescriptor?.revisionView === 'rejected' ? 'rejected' : 'accepted';
193
218
  let currentMetadataIndex = targetView === 'rejected'
@@ -195,17 +220,28 @@ export function preflightOperations(documentXml, operations, author, options = {
195
220
  : metadataIndices.accepted;
196
221
 
197
222
  try {
198
- const resolved = resolveTargetParagraph(xmlDoc, {
199
- targetText: operation.target,
200
- targetRef: operation.targetRef,
201
- targetDescriptor: operation.targetDescriptor,
202
- opType: operation.operationKind,
203
- strictAmbiguity: strictTargets,
204
- paragraphMetadataIndex: currentMetadataIndex,
205
- metadataIndices,
206
- onInfo: options.onInfo,
207
- onWarn: options.onWarn
208
- });
223
+ let resolved;
224
+ if (compiledBinding.dynamic) {
225
+ resolved = resolveTargetParagraph(xmlDoc, {
226
+ targetText: operation.target,
227
+ targetRef: operation.targetRef,
228
+ targetDescriptor: operation.targetDescriptor,
229
+ opType: operation.operationKind,
230
+ strictAmbiguity: strictTargets,
231
+ paragraphMetadataIndex: currentMetadataIndex,
232
+ metadataIndices,
233
+ onInfo: options.onInfo,
234
+ onWarn: options.onWarn
235
+ });
236
+ } else {
237
+ const bound = batchCompilation.registry.resolve(compiledBinding.sourceIds[0], xmlDoc);
238
+ if (bound.error) throw Object.assign(new Error(bound.error.message), bound.error);
239
+ resolved = {
240
+ paragraph: bound.paragraph,
241
+ resolvedBy: compiledBinding.resolvedBy,
242
+ warnings: compiledBinding.warnings
243
+ };
244
+ }
209
245
  const paragraph = resolved.paragraph;
210
246
  const metadata = targetMetadata(xmlDoc, paragraph, resolved.resolvedBy, operation.target, currentMetadataIndex, targetView);
211
247
  const paragraphText = metadata.resolvedTarget.text;
@@ -313,8 +349,10 @@ export function preflightOperations(documentXml, operations, author, options = {
313
349
  const allSame = authors.length > 0 && authors.every(a => a.trim().toLowerCase() === opAuthor);
314
350
  if (!allSame && existingPolicy === 'merge-same-author') {
315
351
  error = {
316
- code: 'EXISTING_REVISIONS',
317
- message: `Target paragraph contains tracked changes from another author (${authors.length ? authors.join(', ') : 'unattributed'}). Pass existingRevisions: "accept-all-first" or resolve revisions first.`
352
+ code: 'EXISTING_REVISIONS',
353
+ 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.`,
354
+ revisionAuthors: authors,
355
+ currentPolicy: existingPolicy
318
356
  };
319
357
  } else if (allSame) {
320
358
  const mergeCommentIds = getCommentIdsInParagraph(paragraph);
@@ -408,43 +446,24 @@ export function preflightOperations(documentXml, operations, author, options = {
408
446
  }
409
447
  }
410
448
 
411
- const conflicts = [];
412
- const byTarget = new Map();
413
- for (const result of results) {
414
- const targetIndex = result.resolvedTarget?.index;
415
- if (!targetIndex) continue;
416
- if (!byTarget.has(targetIndex)) byTarget.set(targetIndex, []);
417
- byTarget.get(targetIndex).push(result);
418
- }
419
-
420
- for (const [targetIndex, targetResults] of byTarget) {
421
- const redlines = targetResults.filter(result => ['redline', 'restore'].includes(result.operationType));
422
- const highlights = targetResults.filter(result => result.operationType === 'highlight');
423
- const target = targetResults[0].resolvedTarget;
424
- if (redlines.length > 1) {
425
- conflicts.push(buildConflict(
426
- 'OVERLAPPING_TEXT_EDITS',
427
- `Multiple text edits target paragraph ${targetIndex}; later operations may use a stale anchor.`,
428
- redlines.map(result => result.index),
429
- target
430
- ));
431
- }
432
- if (redlines.length > 0 && highlights.length > 0) {
433
- conflicts.push(buildConflict(
434
- 'REVISION_ORDER_CONFLICT',
435
- `A text edit and highlight target paragraph ${targetIndex}; operation order can invalidate the target or existing-revision policy.`,
436
- [...redlines, ...highlights].map(result => result.index).sort((a, b) => a - b),
437
- target
438
- ));
439
- }
440
- }
449
+ const conflicts = batchCompilation.conflicts;
441
450
 
442
- const hasErrors = results.some(result => result.status === 'error');
443
- return {
444
- valid: !hasErrors && conflicts.length === 0,
445
- status: !hasErrors && conflicts.length === 0 ? 'ok' : 'error',
446
- results,
447
- conflicts,
451
+ const enrichedResults = results.map(result => result.error ? {
452
+ ...result,
453
+ error: normalizedError(result.error, {
454
+ operationIndex: result.index,
455
+ ...(sourceOperations[result.index - 1]?.operationId
456
+ ? { operationId: sourceOperations[result.index - 1].operationId }
457
+ : {})
458
+ })
459
+ } : result);
460
+ const enrichedConflicts = conflicts.map(conflict => normalizedError(conflict));
461
+ const hasErrors = enrichedResults.some(result => result.status === 'error');
462
+ return {
463
+ valid: !hasErrors && enrichedConflicts.length === 0,
464
+ status: !hasErrors && enrichedConflicts.length === 0 ? 'ok' : 'error',
465
+ results: enrichedResults,
466
+ conflicts: enrichedConflicts,
448
467
  authorsUsed: Array.from(authorsUsed),
449
468
  requiredArtifacts: {
450
469
  comments: commentsRequired,
@@ -10,9 +10,11 @@ export interface ParagraphTargetDescriptor {
10
10
  inTable?: boolean;
11
11
  fingerprint?: string;
12
12
  sourceFingerprint?: string;
13
+ /** Defaults to rejected for restore operations and accepted for all other operations. */
13
14
  revisionView?: 'accepted' | 'rejected';
14
15
  captureRef?: string;
15
16
  select?: string;
17
+ createdByOperation?: number;
16
18
  }
17
19
 
18
20
  export interface InsertionAffinity {
@@ -23,6 +25,12 @@ export interface InsertionAffinity {
23
25
  comment?: 'inside' | 'outside';
24
26
  }
25
27
 
28
+ export interface RejectedTextInsertionAnchor {
29
+ exactText: string;
30
+ occurrence?: number;
31
+ offset: number;
32
+ }
33
+
26
34
  export interface DocumentOperationBase {
27
35
  operationId?: string;
28
36
  captureKey?: string;
@@ -41,6 +49,8 @@ export interface RedlineDocumentOperation extends DocumentOperationBase {
41
49
  structuredContent?: boolean;
42
50
  targetEnd?: ParagraphTargetDescriptor;
43
51
  targetEndRef?: number | string | null;
52
+ /** Required when type is insert and target.revisionView is rejected. */
53
+ anchor?: RejectedTextInsertionAnchor;
44
54
  }
45
55
 
46
56
  export interface RestoreDocumentOperation extends DocumentOperationBase {
@@ -124,6 +134,16 @@ export interface ResolvedDocumentTarget {
124
134
  text: string;
125
135
  fingerprint?: string;
126
136
  inTable?: boolean;
137
+ targetTextMatch?: {
138
+ mode: 'exact' | 'space_equivalent' | 'normalized';
139
+ differences?: Array<{
140
+ offset: number;
141
+ sourceCodePoint: string;
142
+ requestedCodePoint: string;
143
+ }>;
144
+ sourceExcerpt?: string;
145
+ requestedExcerpt?: string;
146
+ };
127
147
  }
128
148
 
129
149
  export interface ResolvedCommentAnchor {
@@ -220,6 +240,17 @@ export interface DocumentOperationBatchResult {
220
240
  status?: RedlineStatus;
221
241
  error?: RedlineError;
222
242
  warnings?: string[];
243
+ conflicts?: OperationConflict[];
244
+ retryPlan?: MutationRetryPlan;
245
+ }
246
+
247
+ export interface MutationRetryPlan {
248
+ base: 'original' | 'output';
249
+ committedIndexes: number[];
250
+ failedIndexes: number[];
251
+ unattemptedIndexes: number[];
252
+ replayWholeBatch: boolean;
253
+ sameArgumentsSafe: false;
223
254
  }
224
255
 
225
256
  export interface OperationPreflightItemResult {
@@ -253,10 +284,13 @@ export interface OperationPreflightItemResult {
253
284
  }
254
285
 
255
286
  export interface OperationConflict {
256
- code: 'OVERLAPPING_TEXT_EDITS' | 'REVISION_ORDER_CONFLICT' | string;
287
+ code: 'OVERLAPPING_SOURCE_TARGETS' | 'OVERLAPPING_TEXT_EDITS' | 'REVISION_ORDER_CONFLICT' | 'CAPTURE_FANOUT_CONFLICT' | string;
257
288
  message: string;
258
289
  operationIndexes: number[];
259
290
  target: ResolvedDocumentTarget;
291
+ recoveryVersion?: number;
292
+ category?: string;
293
+ recovery?: RedlineError['recovery'];
260
294
  }
261
295
 
262
296
  export interface OperationPreflightResult {