@ansonlai/docx-redline-js 0.8.4 → 0.9.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 (68) hide show
  1. package/ARCHITECTURE.md +221 -12
  2. package/CHANGELOG.md +176 -0
  3. package/README.md +125 -12
  4. package/core/current-properties.js +136 -0
  5. package/core/paragraph-text.js +211 -209
  6. package/core/types.js +9 -3
  7. package/core/word-revision-semantics.js +256 -0
  8. package/dist/docx-redline-js.esm.js +4189 -767
  9. package/dist/docx-redline-js.esm.js.map +4 -4
  10. package/dist/docx-redline-js.esm.min.js +111 -108
  11. package/dist/docx-redline-js.esm.min.js.map +4 -4
  12. package/dist/docx-redline.bundle.cjs +4292 -870
  13. package/dist/docx-redline.bundle.cjs.map +4 -4
  14. package/dist/docx-redline.bundle.js +4292 -870
  15. package/dist/docx-redline.bundle.js.map +4 -4
  16. package/docs/AGENT_FAST_START.md +2 -0
  17. package/docs/AGENT_KNOWLEDGE_BASE.md +19 -3
  18. package/docs/SKILL_AUTHORING.md +165 -150
  19. package/docs/TESTING.md +1142 -1059
  20. package/docs/schemas/document-operations.schema.json +54 -1
  21. package/document/docx-document.js +1022 -1010
  22. package/engine/oxml-engine.js +736 -749
  23. package/engine/paragraph-mark-revisions.js +292 -0
  24. package/engine/reconstruction-mapper.js +312 -311
  25. package/engine/reconstruction-writer.js +67 -10
  26. package/engine/rpr-helpers.js +42 -0
  27. package/engine/run-builders.js +0 -73
  28. package/engine/surgical-diff-application.js +604 -605
  29. package/engine/table-mode.js +63 -63
  30. package/index.d.ts +47 -1
  31. package/index.js +11 -2
  32. package/node/cli-help.js +1 -0
  33. package/node/cli.js +992 -975
  34. package/orchestration/list-structural-fallback.js +67 -9
  35. package/package.json +1 -1
  36. package/pipeline/diff-engine.js +130 -1
  37. package/pipeline/ingestion-export.js +227 -243
  38. package/pipeline/ingestion-paragraph.js +93 -98
  39. package/pipeline/list-generation.js +415 -419
  40. package/pipeline/pipeline.js +206 -204
  41. package/scripts/export-validation-fixtures.mjs +19 -6
  42. package/scripts/generate-word-revision-semantics-matrix.mjs +134 -0
  43. package/scripts/lib/word-coverage-catalogue.mjs +7 -2
  44. package/scripts/render-multilevel-bullet-visual.ps1 +2 -2
  45. package/scripts/word-com-corpus-visual-suite.ps1 +2 -2
  46. package/scripts/word-com-differential.ps1 +443 -335
  47. package/scripts/word-com-visual-suite.ps1 +2 -2
  48. package/scripts/word-revision-semantics-matrix.ps1 +108 -0
  49. package/services/batch-operation-orchestrator.js +23 -3
  50. package/services/capture-engine.js +228 -226
  51. package/services/document-operation-applier.js +213 -0
  52. package/services/document-operation-contract.js +75 -0
  53. package/services/document-operation-mutations.js +3346 -2721
  54. package/services/error-recovery.js +17 -0
  55. package/services/list-conversion-diagnostics.js +296 -0
  56. package/services/list-format-change.js +586 -0
  57. package/services/list-format-mutation.js +416 -0
  58. package/services/list-format.js +305 -0
  59. package/services/numbering-allocation.js +642 -0
  60. package/services/numbering-helpers.js +354 -524
  61. package/services/operation-batch-compiler.js +183 -11
  62. package/services/operation-preflight.js +1 -0
  63. package/services/receipt-collector.js +288 -288
  64. package/services/revision-comment-management.js +38 -5
  65. package/services/revision-fidelity-verifier.js +498 -0
  66. package/services/standalone-docx-plumbing.js +627 -631
  67. package/services/standalone-operation-runner.d.ts +182 -5
  68. package/services/table-reconciliation.js +59 -7
@@ -2,30 +2,30 @@
2
2
  * Table-specific reconciliation and transformation flows.
3
3
  */
4
4
 
5
- import {
6
- NS_W,
7
- createRevisionMetadata,
8
- getRevisionIdAllocatorForDocument,
9
- setRevisionIdAllocatorForDocument
10
- } from '../core/types.js';
11
- import {
12
- getElementsByTagNS,
13
- getElementsByTagNSOrTag,
14
- getFirstElementByTagNS,
15
- getFirstElementByTagNSOrTag,
16
- getXmlParseError
17
- } from '../core/xml-query.js';
18
- import { parseOoxmlSafe } from '../adapters/xml-adapter.js';
19
- import { log, error } from '../adapters/logger.js';
20
- import { diffTablesWithVirtualGrid, serializeVirtualGridToOoxml, generateTableOoxml } from '../services/table-reconciliation.js';
21
- import { parseTable } from '../pipeline/pipeline.js';
22
- import { ingestTableToVirtualGrid } from '../pipeline/ingestion.js';
23
- import { createWordElement, withOoxmlSourceType } from '../core/word-xml.js';
24
- import { markParagraphMarkDeleted } from './run-builders.js';
25
-
26
- function noChanges(serializer, xmlDoc) {
27
- return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: false });
28
- }
5
+ import {
6
+ NS_W,
7
+ createRevisionMetadata,
8
+ getRevisionIdAllocatorForDocument,
9
+ setRevisionIdAllocatorForDocument
10
+ } from '../core/types.js';
11
+ import {
12
+ getElementsByTagNS,
13
+ getElementsByTagNSOrTag,
14
+ getFirstElementByTagNS,
15
+ getFirstElementByTagNSOrTag,
16
+ getXmlParseError
17
+ } from '../core/xml-query.js';
18
+ import { parseOoxmlSafe } from '../adapters/xml-adapter.js';
19
+ import { log, error } from '../adapters/logger.js';
20
+ import { diffTablesWithVirtualGrid, serializeVirtualGridToOoxml, generateTableOoxml } from '../services/table-reconciliation.js';
21
+ import { parseTable } from '../pipeline/pipeline.js';
22
+ import { ingestTableToVirtualGrid } from '../pipeline/ingestion.js';
23
+ import { createWordElement, withOoxmlSourceType } from '../core/word-xml.js';
24
+ import { markParagraphDeleted } from './paragraph-mark-revisions.js';
25
+
26
+ function noChanges(serializer, xmlDoc) {
27
+ return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: false });
28
+ }
29
29
 
30
30
  /**
31
31
  * Applies structural reconciliation to tables using Virtual Grid.
@@ -38,8 +38,8 @@ function noChanges(serializer, xmlDoc) {
38
38
  * @param {boolean} [generateRedlines=true] - Track change toggle
39
39
  * @returns {{ oxml: string, hasChanges: boolean }}
40
40
  */
41
- export function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines = true) {
42
- const tableNodes = getElementsByTagNSOrTag(xmlDoc, NS_W, 'tbl');
41
+ export function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines = true) {
42
+ const tableNodes = getElementsByTagNSOrTag(xmlDoc, NS_W, 'tbl');
43
43
  const newTableData = parseTable(modifiedText);
44
44
  const hasNewContent = newTableData.rows.length > 0 || newTableData.headers.length > 0;
45
45
 
@@ -55,15 +55,15 @@ export function applyTableReconciliation(xmlDoc, modifiedText, serializer, parse
55
55
  return noChanges(serializer, xmlDoc);
56
56
  }
57
57
 
58
- const options = {
59
- generateRedlines,
60
- author,
61
- revisionIdAllocator: getRevisionIdAllocatorForDocument(xmlDoc)
62
- };
58
+ const options = {
59
+ generateRedlines,
60
+ author,
61
+ revisionIdAllocator: getRevisionIdAllocatorForDocument(xmlDoc)
62
+ };
63
63
  const reconciledOxml = serializeVirtualGridToOoxml(oldGrid, operations, options);
64
64
  const wrappedOxml = `<root xmlns:w="${NS_W}">${reconciledOxml}</root>`;
65
- const reconciledDoc = parseOoxmlSafe(wrappedOxml, 'application/xml').doc;
66
- if (!reconciledDoc) return noChanges(serializer, xmlDoc);
65
+ const reconciledDoc = parseOoxmlSafe(wrappedOxml, 'application/xml').doc;
66
+ if (!reconciledDoc) return noChanges(serializer, xmlDoc);
67
67
 
68
68
  const parseError = getXmlParseError(reconciledDoc);
69
69
  if (parseError) {
@@ -71,7 +71,7 @@ export function applyTableReconciliation(xmlDoc, modifiedText, serializer, parse
71
71
  return noChanges(serializer, xmlDoc);
72
72
  }
73
73
 
74
- const newTableNode = getFirstElementByTagNSOrTag(reconciledDoc, NS_W, 'tbl');
74
+ const newTableNode = getFirstElementByTagNSOrTag(reconciledDoc, NS_W, 'tbl');
75
75
  if (!newTableNode) {
76
76
  error('[OxmlEngine] No table found in reconciled OOXML');
77
77
  return noChanges(serializer, xmlDoc);
@@ -80,7 +80,7 @@ export function applyTableReconciliation(xmlDoc, modifiedText, serializer, parse
80
80
  const importedTable = xmlDoc.importNode(newTableNode, true);
81
81
  targetTable.parentNode.replaceChild(importedTable, targetTable);
82
82
 
83
- return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: true });
83
+ return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: true });
84
84
  }
85
85
 
86
86
  /**
@@ -94,17 +94,17 @@ export function applyTableReconciliation(xmlDoc, modifiedText, serializer, parse
94
94
  * @param {boolean} generateRedlines - Track change toggle
95
95
  * @returns {{ oxml: string, hasChanges: boolean }}
96
96
  */
97
- export function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines) {
98
- const revisionIdAllocator = getRevisionIdAllocatorForDocument(xmlDoc);
97
+ export function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines) {
98
+ const revisionIdAllocator = getRevisionIdAllocatorForDocument(xmlDoc);
99
99
  const tableData = parseTable(modifiedText);
100
100
  if (!tableData || (tableData.rows.length === 0 && tableData.headers.length === 0)) {
101
101
  log('[OxmlEngine] Failed to parse table data from Markdown');
102
102
  return noChanges(serializer, xmlDoc);
103
103
  }
104
104
 
105
- const tableOoxml = generateTableOoxml(tableData, { generateRedlines, author, revisionIdAllocator });
106
- const tableDoc = parseOoxmlSafe(`<root xmlns:w="${NS_W}">${tableOoxml}</root>`, 'application/xml').doc;
107
- if (!tableDoc) return noChanges(serializer, xmlDoc);
105
+ const tableOoxml = generateTableOoxml(tableData, { generateRedlines, author, revisionIdAllocator, trackAsBlock: true });
106
+ const tableDoc = parseOoxmlSafe(`<root xmlns:w="${NS_W}">${tableOoxml}</root>`, 'application/xml').doc;
107
+ if (!tableDoc) return noChanges(serializer, xmlDoc);
108
108
 
109
109
  const tableParseError = getXmlParseError(tableDoc);
110
110
  if (tableParseError) {
@@ -132,16 +132,16 @@ export function applyTextToTableTransformation(xmlDoc, modifiedText, serializer,
132
132
  let parent = firstParagraph.parentNode;
133
133
 
134
134
  if (parent && parent.nodeType === 9) {
135
- const wrappedDoc = parseOoxmlSafe(
136
- `<w:document xmlns:w="${NS_W}"><w:body/></w:document>`,
137
- 'application/xml'
138
- ).doc;
139
- if (!wrappedDoc) return noChanges(serializer, workingDoc);
135
+ const wrappedDoc = parseOoxmlSafe(
136
+ `<w:document xmlns:w="${NS_W}"><w:body/></w:document>`,
137
+ 'application/xml'
138
+ ).doc;
139
+ if (!wrappedDoc) return noChanges(serializer, workingDoc);
140
140
  const wrappedBody = getFirstElementByTagNS(wrappedDoc, NS_W, 'body');
141
141
  paragraphs.forEach(p => wrappedBody.appendChild(wrappedDoc.importNode(p, true)));
142
142
 
143
- workingDoc = wrappedDoc;
144
- setRevisionIdAllocatorForDocument(workingDoc, revisionIdAllocator);
143
+ workingDoc = wrappedDoc;
144
+ setRevisionIdAllocatorForDocument(workingDoc, revisionIdAllocator);
145
145
  paragraphs = getElementsByTagNS(workingDoc, NS_W, 'p');
146
146
  firstParagraph = paragraphs[0];
147
147
  parent = firstParagraph.parentNode;
@@ -149,26 +149,26 @@ export function applyTextToTableTransformation(xmlDoc, modifiedText, serializer,
149
149
 
150
150
  const importedTable = workingDoc.importNode(newTableElement, true);
151
151
 
152
- if (generateRedlines) {
153
- paragraphs.forEach(p => {
154
- markParagraphMarkDeleted(workingDoc, p, author);
155
- const runs = getElementsByTagNS(p, NS_W, 'r');
156
- runs.forEach(run => {
152
+ if (generateRedlines) {
153
+ paragraphs.forEach(p => {
154
+ markParagraphDeleted(p, { author, revisionIdAllocator: workingDoc });
155
+ const runs = getElementsByTagNS(p, NS_W, 'r');
156
+ runs.forEach(run => {
157
157
  const textNodes = getElementsByTagNS(run, NS_W, 't');
158
158
  textNodes.forEach(t => {
159
159
  const text = t.textContent || '';
160
160
  if (text.trim()) {
161
- const delText = createWordElement(workingDoc, 'w:delText');
162
- delText.textContent = text;
163
- t.parentNode.replaceChild(delText, t);
161
+ const delText = createWordElement(workingDoc, 'w:delText');
162
+ delText.textContent = text;
163
+ t.parentNode.replaceChild(delText, t);
164
164
  }
165
165
  });
166
-
167
- const del = createWordElement(workingDoc, 'w:del');
168
- const metadata = createRevisionMetadata(author, workingDoc, 'del');
169
- del.setAttribute('w:id', String(metadata.id));
170
- del.setAttribute('w:author', metadata.author);
171
- del.setAttribute('w:date', metadata.date);
166
+
167
+ const del = createWordElement(workingDoc, 'w:del');
168
+ const metadata = createRevisionMetadata(author, workingDoc, 'del');
169
+ del.setAttribute('w:id', String(metadata.id));
170
+ del.setAttribute('w:author', metadata.author);
171
+ del.setAttribute('w:date', metadata.date);
172
172
  run.parentNode.insertBefore(del, run);
173
173
  del.appendChild(run);
174
174
  });
@@ -183,5 +183,5 @@ export function applyTextToTableTransformation(xmlDoc, modifiedText, serializer,
183
183
  }
184
184
 
185
185
  log('[OxmlEngine] Text-to-table transformation complete');
186
- return withOoxmlSourceType({ oxml: serializer.serializeToString(workingDoc), hasChanges: true });
187
- }
186
+ return withOoxmlSourceType({ oxml: serializer.serializeToString(workingDoc), hasChanges: true });
187
+ }
package/index.d.ts CHANGED
@@ -30,6 +30,16 @@ export type {
30
30
  DocumentOperationBatchResult,
31
31
  HighlightDocumentOperation,
32
32
  InsertionAffinity,
33
+ ListConversionDiagnostic,
34
+ ListFormatDocumentOperation,
35
+ ListFormatRequest,
36
+ ListFormatConvertPlainRequest,
37
+ ListFormatChangeFormatRequest,
38
+ ListFormatJoinRequest,
39
+ ListFormatResult,
40
+ ListFormatConvertPlainResult,
41
+ ListFormatRebindResult,
42
+ ListNumberFormat,
33
43
  LocalizedReplacement,
34
44
  LocalizedReplacementChange,
35
45
  OperationConflict,
@@ -52,6 +62,13 @@ export interface RedlineError {
52
62
  | 'DIFF_TOKEN_LIMIT'
53
63
  | 'UNSUPPORTED_REVISION_VIEW_MUTATION'
54
64
  | 'UNSUPPORTED_INSERTION_AFFINITY'
65
+ | 'UNSUPPORTED_LIST_CONVERSION'
66
+ | 'UNSUPPORTED_LIST_FORMAT_CHANGE'
67
+ | 'LIST_MARKER_NOT_FOUND'
68
+ | 'LIST_MARKER_AMBIGUOUS'
69
+ | 'LIST_MARKER_UNSUPPORTED'
70
+ | 'LIST_FORMAT_TARGET_IS_LIST'
71
+ | 'LIST_FORMAT_STYLE_NUMBERED'
55
72
  | 'UNSAFE_PARAGRAPH_BOUNDARY'
56
73
  | 'FOREIGN_PARAGRAPH_MARK_DELETION'
57
74
  | 'RESTORATION_STATE_REQUIRED'
@@ -80,6 +97,11 @@ export interface RedlineError {
80
97
  recoveryVersion?: number;
81
98
  category?: 'request_fixable' | 'target_refresh_required' | 'candidate_selection_required' | 'policy_choice_required' | 'user_authorization_required' | 'source_conflict' | 'library_or_builder_failure' | 'manual_document_resolution' | string;
82
99
  context?: Record<string, unknown>;
100
+ /** List-conversion errors: the literal marker, the requested list kind/format and the kept source definition. */
101
+ listMarker?: string;
102
+ requestedList?: { kind: 'bullet' | 'numbered'; format: string };
103
+ sourceList?: { numId: string; level: number; kind: 'bullet' | 'numbered'; format: string };
104
+ targetExcerpt?: string;
83
105
  recovery?: {
84
106
  action: string;
85
107
  sameArgumentsSafe: boolean;
@@ -87,6 +109,8 @@ export interface RedlineError {
87
109
  requiresUserAuthorization: boolean;
88
110
  field?: string;
89
111
  recommendedValue?: unknown;
112
+ allowedValue?: unknown;
113
+ hint?: string;
90
114
  };
91
115
  expectedRevision?: RevisionToken;
92
116
  currentRevision?: RevisionToken;
@@ -437,7 +461,29 @@ export function overwriteParagraphNumIds(oxml: string, numId: string | number):
437
461
  export function extractFirstParagraphNumId(oxml: string): string | null;
438
462
  export function buildExplicitDecimalMultilevelNumberingXml(options?: Record<string, unknown>): string;
439
463
  export function remapNumberingPayloadForDocument(numberingXml: string, state: unknown): unknown;
440
- export function mergeNumberingXmlBySchemaOrder(baseXml: string, incomingXml: string): string;
464
+ export interface NumberingIdConflict { kind: 'abstractNum' | 'num'; id: number }
465
+ export interface NumberingMergeOptions { onConflict?: (conflict: NumberingIdConflict) => void }
466
+ export interface NumberingIntegrityIssue { code: string; severity: 'error'; message: string; numId?: number; abstractNumId?: number; ilvl?: number }
467
+ export function mergeNumberingXmlBySchemaOrder(baseXml: string, incomingXml: string, options?: NumberingMergeOptions): string;
468
+ export function mergeNumberingParts(existingXml: string | null | undefined, incomingParts: string | Array<string | null | undefined> | null | undefined, options?: NumberingMergeOptions): string;
469
+ export function allocateNumberingDefinition(params: {
470
+ numberingIdState: unknown;
471
+ sourceNumberingXml?: string | null;
472
+ sourceNumId?: number | string | null;
473
+ levels?: Array<{ ilvl: number; numFmt?: string; lvlText?: string; start?: number }>;
474
+ startOverrides?: Record<number | string, number>;
475
+ }): { numId: number; abstractNumId: number; numberingXml: string } | null;
476
+ export function allocateNumberingForReplacement(params: {
477
+ numberingXml: string | null;
478
+ replacementNodes: unknown[];
479
+ sourceParagraphs?: unknown[];
480
+ numberingIdState?: unknown;
481
+ allowContinueSource?: boolean;
482
+ }): { policy: 'continue-source' | 'allocate' | 'passthrough'; numberingXml: string | null; replacementNodes: unknown[]; numId?: number };
483
+ export function applyStartOverridesToNum(numElement: unknown, startOverrides: Record<number | string, number>): void;
484
+ export function copyLevelGeometry(sourceLvl: unknown, targetLvl: unknown): void;
485
+ export function canonicalNumberingDefinition(numbering: unknown, numId: string | number): string | null;
486
+ export function validateNumberingIntegrity(documentXml: unknown, numberingXml?: unknown): NumberingIntegrityIssue[];
441
487
 
442
488
  export function buildListMarkdown(items: unknown[], options?: Record<string, unknown>): string;
443
489
  export function inferNumberingStyleFromMarker(marker: string): string;
package/index.js CHANGED
@@ -260,9 +260,18 @@ export {
260
260
  overwriteParagraphNumIds,
261
261
  extractFirstParagraphNumId,
262
262
  buildExplicitDecimalMultilevelNumberingXml,
263
- remapNumberingPayloadForDocument,
264
- mergeNumberingXmlBySchemaOrder
263
+ remapNumberingPayloadForDocument
265
264
  } from './services/numbering-helpers.js';
265
+ export {
266
+ allocateNumberingDefinition,
267
+ allocateNumberingForReplacement,
268
+ applyStartOverridesToNum,
269
+ canonicalNumberingDefinition,
270
+ copyLevelGeometry,
271
+ mergeNumberingParts,
272
+ mergeNumberingXmlBySchemaOrder,
273
+ validateNumberingIntegrity
274
+ } from './services/numbering-allocation.js';
266
275
 
267
276
  // Pipeline components
268
277
  export { ReconciliationPipeline } from './pipeline/pipeline.js';
package/node/cli-help.js CHANGED
@@ -144,6 +144,7 @@ export const CLI_COMMAND_HELP = Object.freeze({
144
144
  option('textToComment', '--text-to-comment <text>', 'Anchor an inline comment to an exact subspan.'),
145
145
  option('targetRef', '--target-ref <N>', 'Disambiguate an inline target with a 1-based machine index.'),
146
146
  option('existingRevisions', '--existing-revisions <policy>', 'Select revision handling; cross-author slicing must be deliberate.'),
147
+ option('revisionFidelity', '--revision-fidelity <enforce|warn|off>', 'Reject All verification of each tracked operation (default enforce: refuse with REVISION_FIDELITY_VIOLATION); the agent profile rejects off.'),
147
148
  option('atomic', '--atomic[=true|false]', 'Choose all-or-nothing or progressive batch execution.'),
148
149
  option('generateRedlines', '--generate-redlines[=true|false]', 'Control tracked-change generation.'),
149
150
  option('noRedlines', '--no-redlines', 'Apply clean text without tracked-change markup.'),