@ansonlai/docx-redline-js 0.4.0 → 0.5.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 (100) hide show
  1. package/AGENTS.md +589 -287
  2. package/ARCHITECTURE.md +215 -9
  3. package/CHANGELOG.md +319 -0
  4. package/README.md +604 -360
  5. package/adapters/config.js +45 -43
  6. package/bin/docx-redline.js +3 -0
  7. package/core/list-targeting.js +101 -110
  8. package/core/paragraph-targeting.js +501 -61
  9. package/core/paragraph-text.js +209 -0
  10. package/core/revision-cloning.js +38 -0
  11. package/core/types.js +64 -10
  12. package/core/word-xml.js +43 -15
  13. package/dist/docx-redline-js.esm.js +2849 -466
  14. package/dist/docx-redline-js.esm.js.map +4 -4
  15. package/dist/docx-redline-js.esm.min.js +87 -76
  16. package/dist/docx-redline-js.esm.min.js.map +4 -4
  17. package/docs/TESTING.md +342 -23
  18. package/docs/plans/2026-09-05-structural-revisions-and-fidelity-oracles.md +1669 -0
  19. package/docs/plans/completed/2026-09-01-performance-and-complexity-reduction.md +669 -0
  20. package/docs/plans/completed/2026-09-03-agent-friendly-document-workflows.md +427 -0
  21. package/docs/plans/completed/2026-09-04-comment-anchor-and-cli-reliability.md +519 -0
  22. package/docs/plans/completed/PERFORMANCE-CONSOLIDATION.md +69 -0
  23. package/docs/plans/completed/structural-revision-capability-matrix.md +115 -0
  24. package/docs/schemas/document-operations.schema.json +109 -0
  25. package/docs/test-comparison-dashboard.html +4250 -7
  26. package/engine/formatting-removal.js +11 -2
  27. package/engine/oxml-engine.js +491 -336
  28. package/engine/reconstruction-mode.js +15 -14
  29. package/engine/reconstruction-writer.js +247 -142
  30. package/engine/route-selection.js +35 -0
  31. package/engine/rpr-helpers.js +334 -35
  32. package/engine/run-builders.js +239 -196
  33. package/engine/surgical-diff-application.js +222 -37
  34. package/engine/surgical-mode.js +134 -6
  35. package/engine/surgical-spans.js +52 -1
  36. package/engine/table-cell-context.js +3 -6
  37. package/engine/table-mode.js +1 -1
  38. package/index.d.ts +234 -6
  39. package/index.js +24 -1
  40. package/node/cli.js +317 -0
  41. package/node/docx-document.js +302 -0
  42. package/node/index.d.ts +31 -0
  43. package/node/index.js +2 -0
  44. package/node/zip-archive.js +52 -0
  45. package/orchestration/list-markdown.js +10 -16
  46. package/orchestration/list-parsing.js +7 -12
  47. package/orchestration/list-structural-fallback.js +21 -10
  48. package/package.json +24 -3
  49. package/pipeline/content-analysis.js +12 -17
  50. package/pipeline/ingestion-export.js +3 -31
  51. package/pipeline/ingestion-paragraph.js +10 -5
  52. package/pipeline/list-generation.js +150 -55
  53. package/pipeline/list-markers.js +70 -3
  54. package/pipeline/serialization.js +4 -2
  55. package/pipeline/structured-content.js +160 -0
  56. package/scripts/apply_changes.mjs +27 -0
  57. package/scripts/benchmark-operation-session.mjs +137 -0
  58. package/scripts/benchmark-targeting-browser.html +74 -0
  59. package/scripts/benchmark-targeting-hot-paths.mjs +67 -0
  60. package/scripts/benchmark-test-runner.mjs +59 -0
  61. package/scripts/build-test-dashboard.mjs +23 -0
  62. package/scripts/export-lane1-fixtures.mjs +380 -0
  63. package/scripts/export-reredline-stress-fixtures.mjs +317 -0
  64. package/scripts/export-validation-fixtures.mjs +1 -1
  65. package/scripts/extract_text.mjs +7 -0
  66. package/scripts/generate-paragraph-boundary-fixtures.ps1 +215 -0
  67. package/scripts/generate-test-dashboard.mjs +362 -11
  68. package/scripts/lib/word-coverage-catalogue.mjs +6 -2
  69. package/scripts/profile-route-selection.mjs +19 -0
  70. package/scripts/render-agenda-multilevel.mjs +0 -5
  71. package/scripts/render-multilevel-cases.mjs +0 -1
  72. package/scripts/run-tests.mjs +107 -35
  73. package/scripts/word-com-corpus-suite.ps1 +3 -0
  74. package/scripts/word-com-differential.ps1 +64 -4
  75. package/scripts/word-com-suite.ps1 +3 -0
  76. package/services/batch-operation-orchestrator.js +494 -0
  77. package/services/capture-engine.js +226 -0
  78. package/services/comment-builders.js +23 -6
  79. package/services/comment-engine.js +108 -47
  80. package/services/comment-locator.js +187 -82
  81. package/services/comment-replies.js +95 -0
  82. package/services/document-inspection.js +258 -0
  83. package/services/document-operation-applier.js +372 -0
  84. package/services/document-operation-contract.js +323 -0
  85. package/services/document-operation-mutations.js +1733 -0
  86. package/services/document-operation-session.js +258 -0
  87. package/services/numbering-service.js +14 -5
  88. package/services/operation-heuristics.js +173 -0
  89. package/services/operation-preflight.js +366 -0
  90. package/services/receipt-collector.js +288 -0
  91. package/services/revision-comment-management.js +37 -5
  92. package/services/revision-token.js +290 -0
  93. package/services/standalone-docx-plumbing.js +123 -8
  94. package/services/standalone-operation-runner.d.ts +296 -0
  95. package/services/standalone-operation-runner.js +10 -1455
  96. package/services/table-reconciliation.js +15 -6
  97. package/docs/VALIDATION.md +0 -183
  98. package/docs/WORD-MANUAL-REVIEW.md +0 -138
  99. package/docs/plans/2026-09-01-performance-and-complexity-reduction.md +0 -210
  100. /package/docs/plans/{2026-08-30-reliability-testing-improvements.md → completed/2026-08-30-reliability-testing-improvements.md} +0 -0
package/README.md CHANGED
@@ -1,360 +1,604 @@
1
- # @ansonlai/docx-redline-js
2
-
3
- Host-independent OOXML reconciliation engine for `.docx` manipulation with track changes (redlines).
4
-
5
- Converts AI-generated or programmatic text/markdown edits into valid Office Open XML (OOXML) with `w:ins`/`w:del` revision markup that Microsoft Word renders as native tracked changes.
6
-
7
- ## Features
8
-
9
- - Text reconciliation with word-level diffing and native-looking redlines
10
- - Formatting updates (bold, italic, underline, strikethrough) via surgical `w:rPrChange`
11
- - Lists: generate and edit real Word lists (`w:numPr`) from markdown
12
- - Tables: virtual-grid diffing for cell-level edits with merge safety
13
- - Comments: inject OOXML comments anchored to text ranges
14
- - Revision management: detect existing revisions, consume move revisions, and accept/reject tracked changes by author or for all authors
15
- - Comment management: delete comments by author or for all authors
16
- - Highlights: apply highlight colors to runs
17
- - Markdown and OOXML conversion in both directions
18
- - Status/error result fields for parse, targeting, and existing-revision failures
19
- - Package plumbing helpers for numbering.xml, comments.xml, content types, and relationships
20
- - Zero host dependencies: works in Node.js, browsers, Deno, and similar JS runtimes with DOM parsing support
21
- - TypeScript declarations included via `index.d.ts`
22
-
23
- ## Install
24
-
25
- ### npm / Node.js
26
-
27
- ```bash
28
- npm install @ansonlai/docx-redline-js
29
- ```
30
-
31
- ### CDN (browser `<script type="module">`)
32
-
33
- ```html
34
- <script type="module">
35
- import { applyRedlineToOxml } from 'https://esm.sh/@ansonlai/docx-redline-js';
36
- </script>
37
- ```
38
-
39
- Or use the pre-bundled file (no import map needed, `diff-match-patch` is inlined):
40
-
41
- ```html
42
- <script type="module">
43
- import { applyRedlineToOxml } from 'https://cdn.jsdelivr.net/npm/@ansonlai/docx-redline-js/dist/docx-redline-js.esm.min.js';
44
- </script>
45
- ```
46
-
47
- ### Local git clone
48
-
49
- ```bash
50
- git clone https://github.com/AnsonLai/docx-redline-js.git
51
- ```
52
-
53
- ```js
54
- import { applyRedlineToOxml } from './docx-redline-js/index.js';
55
- ```
56
-
57
- ## Quick Start
58
-
59
- ### Node.js
60
-
61
- ```js
62
- import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
63
- import {
64
- configureXmlProvider,
65
- setDefaultAuthor,
66
- applyRedlineToOxml
67
- } from '@ansonlai/docx-redline-js';
68
-
69
- configureXmlProvider({ DOMParser, XMLSerializer });
70
- setDefaultAuthor('My App');
71
-
72
- const result = await applyRedlineToOxml(
73
- paragraphOoxml,
74
- 'Original sentence.',
75
- 'Updated sentence.',
76
- { generateRedlines: true, author: 'Editor' }
77
- );
78
-
79
- console.log(result.hasChanges);
80
- console.log(result.oxml);
81
- ```
82
-
83
- ### Browser
84
-
85
- ```js
86
- import {
87
- setDefaultAuthor,
88
- applyRedlineToOxml
89
- } from '@ansonlai/docx-redline-js';
90
-
91
- setDefaultAuthor('Browser Editor');
92
-
93
- const result = await applyRedlineToOxml(oxml, original, modified, {
94
- generateRedlines: true
95
- });
96
- ```
97
-
98
- ## API Reference
99
-
100
- ### Configuration (call once at startup)
101
-
102
- | Function | Purpose |
103
- |----------|---------|
104
- | `configureXmlProvider({ DOMParser, XMLSerializer })` | Inject XML parser. Required in Node.js; browsers usually provide native support. |
105
- | `configureLogger({ log, warn, error })` | Replace default console logger. |
106
- | `setDefaultAuthor(name)` | Set fallback track-change author (default: `'Author'`). |
107
- | `setPlatform(label)` | Set platform label for diagnostics (default: `'Unknown'`). |
108
-
109
- ### Engine (primary reconciliation APIs)
110
-
111
- | Function | Purpose |
112
- |----------|---------|
113
- | `applyRedlineToOxml(oxml, original, modified, options)` | Core engine entry point for text/markdown reconciliation with optional redlines. |
114
- | `applyRedlineToOxmlWithListFallback(oxml, original, modified, options)` | Core engine with automatic single-line list structural fallback. |
115
- | `reconcileMarkdownTableOoxml(oxml, original, markdownTable, options)` | Table-specific reconciliation helper. |
116
-
117
- Common `applyRedlineToOxml` options:
118
-
119
- | Option | Purpose |
120
- |--------|---------|
121
- | `generateRedlines` | When `true`, emit Word-native tracked changes; when `false`, apply clean text changes. |
122
- | `author` | Track-change author used for generated revisions. |
123
- | `existingRevisions` | Existing-revision policy. `'reject-input'` is the default. `'accept-all-first'` normalizes before a real edit but returns the untouched input on no-op. `'accept-all-first-keep-normalized'` explicitly returns accepted revisions as a change even on no-op. |
124
- | `removeFormatting` | When `true` and the text is unchanged with no Markdown hints, explicitly remove existing bold/italic/underline/strikethrough formatting. Defaults to `false`. |
125
- | `sanitizeInput` | Opt-in removal of a standalone leading assistant-preface line. Defaults to `false`; dollar-delimited text and literal `\\n` sequences are always preserved. |
126
-
127
- Common result fields:
128
-
129
- | Field | Purpose |
130
- |-------|---------|
131
- | `status` | Optional non-breaking status: `'ok'`, `'no-op'`, or `'error'`. |
132
- | `error` | Present when `status === 'error'`; includes a stable `code` such as `PARSE_ERROR`, `TARGET_NOT_FOUND`, `PARTIAL_TARGET`, `EXISTING_REVISIONS`, `DIFF_TOKEN_LIMIT`, or `BATCH_OPERATION_FAILED`. |
133
-
134
- Word diffs are deterministic by default (no wall-clock timeout). Inputs above
135
- the safe ceiling of 262,144 unique diff tokens return `DIFF_TOKEN_LIMIT` with
136
- the original OOXML unchanged so callers can split the operation without risking
137
- silent text loss.
138
-
139
- ### Pipeline (lower-level access)
140
-
141
- | Function | Purpose |
142
- |----------|---------|
143
- | `ReconciliationPipeline` | Direct pipeline access (ingest, diff, patch, serialize). |
144
- | `ingestWordOoxmlToPlainText(oxml)` | Extract plain text from OOXML. |
145
- | `ingestWordOoxmlToMarkdown(oxml)` | Convert OOXML to markdown. |
146
- | `ingestWordOoxmlToPlainTextResult(oxml)` | Extract text as `{ text, status, error?, warnings? }`, distinguishing malformed input from an empty document. |
147
- | `ingestWordOoxmlToMarkdownResult(oxml)` | Markdown counterpart to the result-returning plain-text helper. |
148
- | `ingestOoxml(oxml)` | Flatten OOXML into an internal run model with offsets. |
149
- | `preprocessMarkdown(text)` | Normalize markdown and extract format hints. |
150
- | `containsTrackedChanges(xmlDoc)` | Detect `w:ins`, `w:del`, move revisions, property changes, and paragraph-mark revision markup in a parsed OOXML document/fragment. |
151
- | `validateRedlineOoxml(oxml)` | Validate generated redline OOXML against the package's structural invariants (no nested revisions, `w:delText` inside `w:del`, complete metadata, unique revision ids, preserved boundary whitespace). Returns `{ valid, issues }`; run it before writing output into a package. |
152
-
153
- ### Services
154
-
155
- | Function | Purpose |
156
- |----------|---------|
157
- | `injectCommentsIntoOoxml(oxml, comments, options)` | Add comments anchored to text ranges. |
158
- | `acceptTrackedChangesInOoxml(oxml, { author?, allAuthors? })` | Accept `w:ins` / `w:del` / `w:moveFrom` / `w:moveTo` / `*PrChange` revisions for one author or all authors. |
159
- | `rejectTrackedChangesInOoxml(oxml, { author?, allAuthors? })` | Reject `w:ins` / `w:del` / `w:moveFrom` / `w:moveTo` / `*PrChange` revisions for one author or all authors. |
160
- | `deleteCommentsByAuthorInOoxml(oxml, { author?, allAuthors? })` | Delete comments and matching anchors/references for one author or all authors. |
161
- | `generateTableOoxml(headers, rows, options)` | Generate a `w:tbl` from tabular data. |
162
- | `createDynamicNumberingIdState(numberingXml)` | Allocate numbering IDs without collisions. |
163
- | `ensureNumberingArtifactsInZip(zip, numberingXml)` | Merge numbering artifacts into a `.docx` package. |
164
- | `ensureCommentsArtifactsInZip(zip, commentsXml)` | Merge comments artifacts into a `.docx` package. |
165
- | `validateDocxPackage(zip)` | Validate `.docx` structural consistency. |
166
-
167
- Malformed OOXML never escapes these public transform APIs as a raw parser
168
- exception. Transforms return `status: 'error'` with `error.code === 'PARSE_ERROR'`;
169
- validators return a `PARSE_ERROR` issue. Recoverable XML parser
170
- diagnostics are forwarded through the configured logger and included in
171
- `warnings` where the result shape supports them.
172
-
173
- ### Deep Imports
174
-
175
- For advanced usage, import specific submodules:
176
-
177
- ```js
178
- import {
179
- applyOperationToDocumentXml,
180
- applyOperationsToDocumentXml,
181
- orderOperationsForStableTargets
182
- } from '@ansonlai/docx-redline-js/services/standalone-operation-runner.js';
183
- import { getParagraphText } from '@ansonlai/docx-redline-js/core/paragraph-targeting.js';
184
- ```
185
-
186
- Use `applyOperationsToDocumentXml(...)` for mixed batches. It stably runs comments before text-changing operations so replacements cannot invalidate their original anchors. Other operation types retain their relative order. Batch results retain each operation's original 1-based index and expose the actual `executionOrder`.
187
-
188
- Batches are atomic by default: any operation error returns the original
189
- `documentXml`, `hasChanges: false`, empty package artifacts, and
190
- `rolledBack: true`. The default `continueOnError: true` still attempts the full
191
- batch so `results` describes what would have applied. Callers that intentionally
192
- consume partial results must pass `{ atomic: false }`; use
193
- `{ continueOnError: false }` to stop after the first error.
194
-
195
- ### Output Shape Matrix
196
-
197
- Different APIs return different OOXML shapes. Use this as a packaging safety check.
198
-
199
- | API | Typical input scope | Output field | Possible root/output shape | Safe to write directly into `word/document.xml` |
200
- |-----|----------------------|--------------|----------------------------|--------------------------------------------------|
201
- | `applyRedlineToOxml(...)` | Paragraph, range, or table-scope OOXML | `result.oxml` | Fragment, `<w:document>`, or package payload (`<pkg:package>`) | No. Inspect first. |
202
- | `applyRedlineToOxmlWithListFallback(...)` | Paragraph or range-scope OOXML | `result.oxml` | Fragment, `<w:document>`, or package payload (`<pkg:package>`) | No. Inspect first. |
203
- | `reconcileMarkdownTableOoxml(...)` | Table or paragraph-scope OOXML | `result.oxml` | Same shapes as `applyRedlineToOxml(...)` for the supplied scope | No. Inspect first. |
204
- | `applyOperationToDocumentXml(...)` | Full `word/document.xml` string | `result.documentXml` | `<w:document>` | Yes. This is the document-safe helper. |
205
- | `applyOperationsToDocumentXml(...)` | Full `word/document.xml` plus an operation batch | `result.documentXml` | `<w:document>` | Yes. Atomic by default; comments are applied before text-changing operations. |
206
- | `extractReplacementNodesFromOoxml(...)` | Any OOXML payload | `{ replacementNodes, numberingXml, sourceType }` | Normalized to `fragment`, `document`, or `package` | Yes. Use this when consuming `result.oxml`. |
207
-
208
- ### Do / Don't for Packaging
209
-
210
- - Do use `applyOperationToDocumentXml(...).documentXml` when your intent is to replace `word/document.xml`.
211
- - Do use `applyOperationsToDocumentXml(...)` rather than an unsorted loop for batches containing comments and replacements that target the same original paragraph.
212
- - Redline application strips proofing markers (`w:proofErr`) from the matched target paragraph before diffing, while preserving complex-field scaffolding (`w:fldChar`, `w:instrText`) and its cached visible result as inert structure. Adjacent edits do not revise or move an unchanged field result.
213
- - Hyperlinks, bookmarks, comment range markers, tabs/breaks, and footnote/endnote references are treated as structural OOXML that should survive adjacent redline edits instead of being orphaned or wrapped in deletions.
214
- - Do use `extractReplacementNodesFromOoxml(...)` when you are consuming `result.oxml` from paragraph/range/table APIs.
215
- - Do merge numbering/comments artifacts with `ensureNumberingArtifactsInZip(...)` and `ensureCommentsArtifactsInZip(...)` when those parts are present.
216
- - Don't write payloads that start with `<pkg:package` directly into `word/document.xml`.
217
- - Don't assume every `result.oxml` payload is a raw paragraph fragment.
218
-
219
- ## Working With `.docx` Files
220
-
221
- This package operates on OOXML strings (XML parts inside `.docx` zip archives), not raw `.docx` binaries.
222
-
223
- Typical flow:
224
-
225
- 1. Extract the `.docx` zip (for example with JSZip, fflate, or similar)
226
- 2. Read `word/document.xml`
227
- 3. Apply reconciliation APIs to XML strings
228
- 4. Merge numbering/comments artifacts when needed
229
- 5. Write the archive back to a `.docx` file
230
-
231
- ```js
232
- import JSZip from 'jszip';
233
- import {
234
- applyRedlineToOxml,
235
- extractReplacementNodesFromOoxml,
236
- ensureNumberingArtifactsInZip,
237
- validateDocxPackage
238
- } from '@ansonlai/docx-redline-js';
239
- import { applyOperationToDocumentXml } from '@ansonlai/docx-redline-js/services/standalone-operation-runner.js';
240
-
241
- const zip = await JSZip.loadAsync(docxBuffer);
242
- const documentXml = await zip.file('word/document.xml').async('string');
243
-
244
- const opResult = await applyOperationToDocumentXml(
245
- documentXml,
246
- { type: 'redline', target: 'old text', modified: 'new text' },
247
- 'Editor'
248
- );
249
-
250
- // applyOperationToDocumentXml(...) returns a full w:document payload.
251
- zip.file('word/document.xml', opResult.documentXml);
252
-
253
- const fragmentResult = await applyRedlineToOxml(
254
- paragraphOoxml,
255
- 'Item text',
256
- '1. Item text',
257
- { generateRedlines: true, author: 'Editor' }
258
- );
259
- const normalized = extractReplacementNodesFromOoxml(fragmentResult.oxml);
260
-
261
- // If sourceType === 'package', merge extracted content/artifacts instead of
262
- // writing the raw pkg:package payload into word/document.xml.
263
- if (normalized.numberingXml) {
264
- await ensureNumberingArtifactsInZip(zip, normalized.numberingXml);
265
- }
266
-
267
- await validateDocxPackage(zip);
268
- const output = await zip.generateAsync({ type: 'nodebuffer' });
269
- ```
270
-
271
- ## Validating Output
272
-
273
- For the test-lane design and instructions for adding regression, synthetic
274
- Word, and real-corpus cases, see [docs/TESTING.md](./docs/TESTING.md).
275
-
276
- Run the automated package checks:
277
-
278
- ```bash
279
- npm test
280
- npm run test:isolation
281
- npm run check:types
282
- npm run lint
283
- npm run test:coverage
284
- ```
285
-
286
- For release-time fixture export:
287
-
288
- ```bash
289
- node scripts/export-validation-fixtures.mjs
290
- ```
291
-
292
- On Windows with desktop Word installed, you can smoke-test a completed `.docx`:
293
-
294
- ```bash
295
- npm run smoke:word -- path/to/file.docx
296
- ```
297
-
298
- To validate against Word as an independent oracle (Word itself accepts and
299
- rejects the generated revisions and the resulting text is compared to the
300
- expected outcomes):
301
-
302
- ```bash
303
- npm run test:word
304
- ```
305
-
306
- This Windows-only test command generates an English legal/administrative task
307
- suite under `tmp/word-validation/` and drives installed desktop Microsoft Word
308
- through COM. Its 33 cases include targeted reliability checks for literal
309
- content, multi-paragraph replacement, prior-revision no-op, atomic rollback,
310
- hostile revision IDs, bookmarks, internal hyperlinks, mixed formatted runs,
311
- content controls, table cells, structural tabs, locked complex fields,
312
- comments, footnotes/endnotes, headers/footers, and external hyperlinks.
313
- Structure-focused cases also assert required
314
- OOXML elements before Word independently checks Accept All and Reject All. The
315
- published library remains clean, host-independent JavaScript; Word automation
316
- exists only in development scripts.
317
-
318
- Use `npm run report:word:coverage` to print the validated task-by-structure
319
- matrix across all 33 synthetic and 31 SuperDoc scenarios. Before a release,
320
- `npm run review:word:prepare -- --cycle=0` creates a pending human-review
321
- manifest with changed cases, a rotating 20% synthetic sample, and legal plus
322
- administrative corpus representatives. See [docs/TESTING.md](./docs/TESTING.md)
323
- and [docs/WORD-MANUAL-REVIEW.md](./docs/WORD-MANUAL-REVIEW.md); preparation and
324
- AI preflight never count as human sign-off.
325
-
326
- A nightly GitHub Actions workflow additionally validates generated fixtures
327
- against the ECMA-376 transitional schemas (`xmllint`), opens them with
328
- LibreOffice, and runs an extended fuzz sweep of the accept/reject round-trip
329
- invariant with a fresh seed. See [docs/VALIDATION.md](./docs/VALIDATION.md).
330
-
331
- ## Architecture
332
-
333
- See [ARCHITECTURE.md](./ARCHITECTURE.md) for module layout, data flow, and contributor guidance.
334
-
335
- See [AGENTS.md](./AGENTS.md) for a concise reference for AI coding agents.
336
-
337
- See [docs/VALIDATION.md](./docs/VALIDATION.md) for release-time validation steps.
338
-
339
- See [docs/TESTING.md](./docs/TESTING.md) for how the test lanes work and how to
340
- add new cases.
341
-
342
- ## Test Corpus Attribution
343
-
344
- Real-document reliability testing uses selected references from
345
- [docx-corpus](https://docxcorp.us/), built by
346
- [SuperDoc](https://superdoc.dev/). The dataset is licensed under the
347
- [Open Data Commons Attribution License (ODC-By) 1.0](https://opendatacommons.org/licenses/by/1-0/).
348
-
349
- Only explicitly pinned English legal and administrative documents are eligible
350
- for the initial corpus lane. References and provenance live in
351
- `tests/corpus/superdoc-english-legal-administrative.json`; downloaded documents
352
- are hash-verified and kept in ignored `tmp/` storage rather than committed. On
353
- Windows with desktop Word installed, run the reviewed 31-scenario/23-document lane with:
354
-
355
- ```bash
356
- npm run test:corpus:word
357
- ```
358
-
359
- ODC-By applies to the database; individual documents may carry additional
360
- rights, so each selected document must be reviewed before becoming a test case.
1
+ # @ansonlai/docx-redline-js
2
+
3
+ Host-independent OOXML reconciliation engine for `.docx` manipulation with track changes (redlines).
4
+
5
+ Converts AI-generated or programmatic text/markdown edits into valid Office Open XML (OOXML) with `w:ins`/`w:del` revision markup that Microsoft Word renders as native tracked changes.
6
+
7
+ ## Features
8
+
9
+ - Text reconciliation with word-level diffing and native-looking redlines
10
+ - Formatting updates (bold, italic, underline, strikethrough) via surgical `w:rPrChange`
11
+ - Lists: generate and edit real Word lists (`w:numPr`) from markdown
12
+ - Tables: virtual-grid diffing for cell-level edits with merge safety
13
+ - Comments: inject OOXML comments anchored to text ranges
14
+ - Revision management: detect existing revisions, consume move revisions, and accept/reject tracked changes by author or for all authors
15
+ - Comment management: delete comments by author or for all authors
16
+ - Highlights: apply highlight colors to runs
17
+ - Markdown and OOXML conversion in both directions
18
+ - Status/error result fields for parse, targeting, and existing-revision failures
19
+ - Package plumbing helpers for numbering.xml, comments.xml, content types, and relationships
20
+ - Zero host dependencies: works in Node.js, browsers, Deno, and similar JS runtimes with DOM parsing support
21
+ ## Documentation Index
22
+
23
+ | Document | Description |
24
+ |---|---|
25
+ | **[README.md](./README.md)** | Library overview, installation, quick start, and public API reference |
26
+ | **[AGENTS.md](./AGENTS.md)** | AI coding agent quick reference, targeting rules, and complete CLI workflow |
27
+ | **[ARCHITECTURE.md](./ARCHITECTURE.md)** | Contributor architecture, module responsibilities, end-to-end data flow, and contracts |
28
+ | **[docs/TESTING.md](./docs/TESTING.md)** | Complete testing guide, test lanes, independent oracle validation, and Word visual review checklist |
29
+ | **[CHANGELOG.md](./CHANGELOG.md)** | Release history, breaking changes, and migration notes |
30
+
31
+ ## Install
32
+
33
+ ### npm / Node.js
34
+
35
+ ```bash
36
+ npm install @ansonlai/docx-redline-js
37
+ ```
38
+
39
+ ### CDN (browser `<script type="module">`)
40
+
41
+ ```html
42
+ <script type="module">
43
+ import { applyRedlineToOxml } from 'https://esm.sh/@ansonlai/docx-redline-js';
44
+ </script>
45
+ ```
46
+
47
+ Or use the pre-bundled file (no import map needed, `diff-match-patch` is inlined):
48
+
49
+ ```html
50
+ <script type="module">
51
+ import { applyRedlineToOxml } from 'https://cdn.jsdelivr.net/npm/@ansonlai/docx-redline-js/dist/docx-redline-js.esm.min.js';
52
+ </script>
53
+ ```
54
+
55
+ ### Local git clone
56
+
57
+ ```bash
58
+ git clone https://github.com/AnsonLai/docx-redline-js.git
59
+ ```
60
+
61
+ ```js
62
+ import { applyRedlineToOxml } from './docx-redline-js/index.js';
63
+ ```
64
+
65
+ ## Quick Start
66
+
67
+ ### Node.js
68
+
69
+ ```js
70
+ import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
71
+ import {
72
+ configureXmlProvider,
73
+ setDefaultAuthor,
74
+ applyRedlineToOxml
75
+ } from '@ansonlai/docx-redline-js';
76
+
77
+ configureXmlProvider({ DOMParser, XMLSerializer });
78
+ setDefaultAuthor('My App');
79
+
80
+ const result = await applyRedlineToOxml(
81
+ paragraphOoxml,
82
+ 'Original sentence.',
83
+ 'Updated sentence.',
84
+ { generateRedlines: true, author: 'Editor' }
85
+ );
86
+
87
+ console.log(result.hasChanges);
88
+ console.log(result.oxml);
89
+ ```
90
+
91
+ ### Browser
92
+
93
+ ```js
94
+ import {
95
+ setDefaultAuthor,
96
+ applyRedlineToOxml
97
+ } from '@ansonlai/docx-redline-js';
98
+
99
+ setDefaultAuthor('Browser Editor');
100
+
101
+ const result = await applyRedlineToOxml(oxml, original, modified, {
102
+ generateRedlines: true
103
+ });
104
+ ```
105
+
106
+ ## API Reference
107
+
108
+ ### Agent-friendly inspection and complete DOCX editing
109
+
110
+ ```js
111
+ import { inspectDocumentParts } from '@ansonlai/docx-redline-js';
112
+ const inventory = inspectDocumentParts({ documentXml, commentsXml, numberingXml });
113
+ ```
114
+
115
+ Inspection returns exact paragraph text, target IDs/fingerprints, headings,
116
+ table/list context, revision authors, and joined comment anchors. Filters such
117
+ as `search`, `indexes`, `range`, `revisedOnly`, `inTable`, and `skipEmpty`
118
+ limit output. `revisionView` accepts `accepted`, `rejected`, or `current`.
119
+
120
+ For complete `.docx` buffers in Node:
121
+
122
+ ```js
123
+ import { openDocx } from '@ansonlai/docx-redline-js/node';
124
+ const document = openDocx(inputBuffer);
125
+ const result = await document.applyOperations(operations, {
126
+ author: 'Editor', atomic: true, validate: true
127
+ });
128
+ const outputBuffer = result.toBuffer();
129
+ ```
130
+
131
+ The Node facade performs edits, artifact merges, package wiring, validation,
132
+ and commit as one transaction. It defaults to strict targets and returns the
133
+ untouched input with `written: false` on atomic failure. It is isolated from
134
+ the root/browser dependency graph.
135
+
136
+ Install `@xmldom/xmldom` alongside the package when using the Node facade or
137
+ CLI; it remains an optional peer so browser consumers do not install a DOM shim.
138
+
139
+ ### Agent CLI
140
+
141
+ ```bash
142
+ docx-redline extract contract.docx --range 10:30
143
+ docx-redline preflight contract.docx --operations operations.json --author "Editor"
144
+ docx-redline apply contract.docx --operations operations.json --author "Editor" --output reviewed.docx
145
+ docx-redline validate reviewed.docx
146
+ ```bash
147
+ # Inline one-liner edit (no operations file needed)
148
+ docx-redline apply contract.docx --target "Original clause text" --modified "New clause text" --output reviewed.docx
149
+
150
+ # Direct edit without tracked changes
151
+ docx-redline apply contract.docx --target "Typo fix" --modified "Fixed typo" --no-redlines --output clean.docx
152
+
153
+ # High-assurance atomic batch
154
+ docx-redline apply contract.docx --operations operations.json --atomic --output reviewed.docx
155
+ ```
156
+
157
+ All commands emit JSON on stdout. `apply` defaults:
158
+ - **Author**: Defaults to `'AI Redliner'` (or `DOCX_REDLINE_AUTHOR` environment variable).
159
+ - **Output overwrite**: Destination files provided via `--output` overwrite by default. Pass `--no-overwrite` or `--no-clobber` to safeguard existing destination files. The source input is never overwritten unless `--in-place` is specified.
160
+ - **Transactionality**: Defaults to `atomic: false` (applies valid operations and reports any failures). Pass `--atomic` for all-or-nothing rollback on any operation error.
161
+ - **Tracked changes**: Defaults to `generateRedlines: true`. Pass `--no-redlines` when clean direct text edits are desired.
162
+ - **Inline edits**: Use `--target <text>` with `--modified <text>` or `--comment <text>` for quick one-liners without creating a JSON file.
163
+
164
+ See [the agent workflow in AGENTS.md](./AGENTS.md#agent-document-workflow-cli) and the
165
+ [operation JSON Schema](docs/schemas/document-operations.schema.json).
166
+
167
+ ### Configuration (call once at startup)
168
+
169
+ | Function | Purpose |
170
+ |----------|---------|
171
+ | `configureXmlProvider({ DOMParser, XMLSerializer })` | Inject XML parser. Required in Node.js; browsers usually provide native support. |
172
+ | `configureLogger({ log, warn, error })` | Replace default console logger. |
173
+ | `setDefaultAuthor(name)` | Set fallback track-change author (default: `'AI Redliner'`, configurable via `DOCX_REDLINE_AUTHOR` environment variable). |
174
+ | `setPlatform(label)` | Set platform label for diagnostics (default: `'Unknown'`). |
175
+
176
+ ### Options and Defaults Reference
177
+
178
+ | Option | Type | Default | Description |
179
+ |--------|------|---------|-------------|
180
+ | `generateRedlines` | `boolean` | `true` | When `true`, emit Word-native tracked changes (`w:ins`/`w:del`). When `false`, apply clean direct edits without revision markup. **Note: Redlines are not always the preferred method** — pass `generateRedlines: false` (or `--no-redlines` via CLI) when producing clean execution drafts, restructuring documents, or when revision clutter is unwanted. |
181
+ | `author` | `string` | `'AI Redliner'` | Reviewer/author name stamped on generated tracked changes and comments. Overridable via `DOCX_REDLINE_AUTHOR` env variable. |
182
+ | `atomic` | `boolean` | `false` | Batch transaction mode. By default (`false`), valid edits are applied and failing operations report errors. When `true`, any operation failure rolls back the entire batch to the original document state (`rolledBack: true`, `hasChanges: false`). Feature prominently in high-assurance workflows. |
183
+ | `structuredContent` | `boolean` | `true` | Auto-detects Markdown tables, headings (`#`), and lists in replacement text and renders them as native Word elements (`w:tbl`, `w:pStyle`, `w:numPr`). Pass `false` to treat replacement text strictly as plain text. |
184
+ | `pairReplacements` | `boolean` | `true` | Links adjacent `<w:del>` and `<w:ins>` revisions with matching timestamps so Word groups them as a single replacement in the Reviewing Pane. |
185
+ | `strictTargets` | `boolean` | `true` (CLI/facade) | Requires exact target descriptors (`exactText`, `paragraphId`, `index`, `occurrence`, `fingerprint`) and forbids ambiguous matching. Defaults to `false` in low-level runner for backwards compatibility. |
186
+ | `existingRevisions` | `string` | `'merge-same-author'` | How to handle paragraphs with existing tracked changes. `'merge-same-author'` automatically merges subsequent edits from the same author against the pre-revision baseline while protecting different authors' revisions with `EXISTING_REVISIONS`. Pass `'accept-all-first'` to normalize prior revisions or `'reject-input'` to refuse editing revised paragraphs. |
187
+ | `removeFormatting` | `boolean` | `false` | When `true` and the text is unchanged with no Markdown hints, strips existing bold/italic/underline/strikethrough formatting. |
188
+ | `sanitizeInput` | `boolean` | `false` | Opt-in removal of standalone leading assistant-preface lines. Literal dollar signs and `\n` sequences are always preserved. |
189
+
190
+ Same-author revision merging refuses paragraphs containing comment anchors with
191
+ `COMMENTED_CONTENT_MERGE`; resolve those comments first so the merge cannot
192
+ remove or orphan their anchors.
193
+
194
+ Common result fields:
195
+
196
+ | Field | Purpose |
197
+ |-------|---------|
198
+ | `status` | Operation status: `'ok'`, `'partial'`, `'no-op'`, or `'error'`. |
199
+ | `error` | Present on failure; includes a stable `code` such as `PARSE_ERROR`, `TARGET_NOT_FOUND`, `PARTIAL_TARGET`, `EXISTING_REVISIONS`, `DIFF_TOKEN_LIMIT`, or `BATCH_OPERATION_FAILED`. |
200
+ | `written` | CLI/facade boolean indicating whether the output file was successfully written to disk. |
201
+ | `rolledBack` | Present and `true` when an atomic batch encountered an error and rolled back all changes. |
202
+
203
+ Word diffs are deterministic by default (no wall-clock timeout). Inputs above
204
+ the safe ceiling of 262,144 unique diff tokens return `DIFF_TOKEN_LIMIT` with
205
+ the original OOXML unchanged so callers can split the operation without risking
206
+ silent text loss.
207
+
208
+ ### Replacing a heading with a tracked list
209
+
210
+ The list route treats a one-paragraph heading expanded into multiple markdown
211
+ items as a structural block replacement. The deleted heading stays in its own
212
+ tracked paragraph, and every inserted item becomes a separate Word list
213
+ paragraph. Accepting the revisions produces only the list items; rejecting them
214
+ restores the original heading exactly.
215
+
216
+ ```js
217
+ const result = await applyRedlineToOxml(
218
+ headingParagraphOoxml,
219
+ 'A.\tPURPOSE',
220
+ '* Article A. Purpose and Interagency Alignment\n' +
221
+ '* Key Focus: Joint Street Outreach & Medical Triage',
222
+ { generateRedlines: true, author: 'Editor' }
223
+ );
224
+ ```
225
+
226
+ `w:numId w:val="0"` means numbering is explicitly suppressed; it is not a list
227
+ definition and is never reused for generated bullets. New list items receive a
228
+ positive numbering ID. Font family, size, language, and related script
229
+ properties are inherited from the source paragraph, while heading emphasis
230
+ (such as bold or underline) is not copied unless the replacement markdown asks
231
+ for it. For a complete `word/document.xml`, prefer
232
+ `applyOperationToDocumentXml(...)` so replacement nodes and numbering artifacts
233
+ are imported at the correct scope.
234
+
235
+ ### Planning large mixed-content insertions
236
+
237
+ For an attachment or schedule containing several kinds of content, use
238
+ `planStructuredReplacement(...)` before applying the edit. It decomposes the
239
+ Markdown into typed heading, paragraph, list, and table blocks, normalizes the
240
+ block boundaries, and returns one atomic operation with
241
+ `structuredContent: true`.
242
+
243
+ ```js
244
+ import { planStructuredReplacement } from '@ansonlai/docx-redline-js';
245
+
246
+ const plan = planStructuredReplacement(
247
+ { exactText: 'Date', index: 172 },
248
+ `# ATTACHMENT 4
249
+
250
+ Introductory paragraph.
251
+
252
+ | Agency | Contact |
253
+ | --- | --- |
254
+ | BCHD | Dr. Jenkins |
255
+
256
+ ## Protocol
257
+
258
+ 1. Joint clearance
259
+ 2. Rapid escalation`,
260
+ { author: 'Editor' }
261
+ );
262
+
263
+ if (!plan.valid || !plan.operation) {
264
+ throw new Error(plan.issues.map(issue => issue.message).join(' '));
265
+ }
266
+
267
+ const result = await document.applyOperations([plan.operation], {
268
+ author: 'Editor', atomic: true, validate: true
269
+ });
270
+ ```
271
+
272
+ Every Markdown table must include a separator row immediately below its header.
273
+ Missing separators, missing data rows, and inconsistent column counts are
274
+ reported as structured errors instead of being inserted as visible pipe text.
275
+ Use `#` through `#########` to request Word heading paragraphs; blank lines
276
+ separate paragraphs; adjacent list markers form a real Word list. Keep the
277
+ planned content in one operation so later blocks do not depend on an anchor that
278
+ an earlier block has already replaced.
279
+
280
+ ### Pipeline (lower-level access)
281
+
282
+ | Function | Purpose |
283
+ |----------|---------|
284
+ | `ReconciliationPipeline` | Direct pipeline access (ingest, diff, patch, serialize). |
285
+ | `ingestWordOoxmlToPlainText(oxml)` | Extract plain text from OOXML. |
286
+ | `ingestWordOoxmlToMarkdown(oxml)` | Convert OOXML to markdown. |
287
+ | `ingestWordOoxmlToPlainTextResult(oxml)` | Extract text as `{ text, status, error?, warnings? }`, distinguishing malformed input from an empty document. |
288
+ | `ingestWordOoxmlToMarkdownResult(oxml)` | Markdown counterpart to the result-returning plain-text helper. |
289
+ | `ingestOoxml(oxml)` | Flatten OOXML into an internal run model with offsets. |
290
+ | `preprocessMarkdown(text)` | Normalize markdown and extract format hints. |
291
+ | `analyzeStructuredContent(markdown)` | Decompose mixed Markdown into typed blocks and report malformed table syntax without creating an operation. |
292
+ | `planStructuredReplacement(target, markdown, options)` | Validate mixed content and return one atomic `structuredContent` replacement operation, or `operation: null` with issues. |
293
+ | `containsTrackedChanges(xmlDoc)` | Detect `w:ins`, `w:del`, move revisions, property changes, and paragraph-mark revision markup in a parsed OOXML document/fragment. |
294
+ | `validateRedlineOoxml(oxml)` | Validate generated redline OOXML against the package's structural invariants (no nested revisions, `w:delText` inside `w:del`, complete metadata, unique revision ids, preserved boundary whitespace). Returns `{ valid, issues }`; run it before writing output into a package. |
295
+
296
+ ### Services
297
+
298
+ | Function | Purpose |
299
+ |----------|---------|
300
+ | `injectCommentsIntoOoxml(oxml, comments, options)` | Add comments anchored to text ranges. |
301
+ | `applyCommentReplyToParts(options)` | Build a threaded reply in `comments.xml` and `commentsExtended.xml` without adding a document-body anchor. |
302
+ | `acceptTrackedChangesInOoxml(oxml, { author?, allAuthors? })` | Accept `w:ins` / `w:del` / `w:moveFrom` / `w:moveTo` / `*PrChange` revisions for one author or all authors. |
303
+ | `rejectTrackedChangesInOoxml(oxml, { author?, allAuthors? })` | Reject `w:ins` / `w:del` / `w:moveFrom` / `w:moveTo` / `*PrChange` revisions for one author or all authors. |
304
+ | `deleteCommentsByAuthorInOoxml(oxml, { author?, allAuthors? })` | Delete matching comment definitions and anchors present in the supplied OOXML payload. Real `.docx` packages require updating both `word/comments.xml` and `word/document.xml`. |
305
+ | `generateTableOoxml(headers, rows, options)` | Generate a `w:tbl` from tabular data. |
306
+ | `createDynamicNumberingIdState(numberingXml)` | Allocate numbering IDs without collisions. |
307
+ | `ensureNumberingArtifactsInZip(zip, numberingXml, options)` | Add numbering artifacts to a `.docx` package. Replacement of existing numbering without `mergeNumberingXmlBySchemaOrder` is deprecated and will throw in the next major version. |
308
+ | `ensureCommentsArtifactsInZip(zip, commentsXml)` | Merge comments artifacts into a `.docx` package. |
309
+ | `ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXml)` | Add or replace modern Word comment-thread metadata in a `.docx` package. |
310
+ | `validateDocxPackage(zip)` | Validate `.docx` structural consistency. |
311
+
312
+ Malformed OOXML never escapes these public transform APIs as a raw parser
313
+ exception. Transforms return `status: 'error'` with `error.code === 'PARSE_ERROR'`;
314
+ validators return a `PARSE_ERROR` issue. Recoverable XML parser
315
+ diagnostics are forwarded through the configured logger and included in
316
+ `warnings` where the result shape supports them.
317
+
318
+ ### Deep Imports
319
+
320
+ For advanced usage, import specific submodules:
321
+
322
+ ```js
323
+ import {
324
+ applyOperationToDocumentXml,
325
+ applyOperationsToDocumentXml,
326
+ preflightOperations,
327
+ orderOperationsForStableTargets
328
+ } from '@ansonlai/docx-redline-js/standalone-runner';
329
+ import { getParagraphText } from '@ansonlai/docx-redline-js/core/paragraph-targeting.js';
330
+ ```
331
+
332
+ Use `applyOperationsToDocumentXml(...)` for mixed batches. It stably runs comments before text-changing operations so replacements cannot invalidate their original anchors. Other operation types retain their relative order. Batch results retain each operation's original 1-based index and expose the actual `executionOrder`.
333
+
334
+ Threaded replies use a comment operation with no body target:
335
+
336
+ ```js
337
+ { type: 'comment_reply', parentCommentId: 8, commentContent: 'Agreed; I revised this.', author: 'Editor' }
338
+ ```
339
+
340
+ For complete `.docx` files, use `openDocx(...).applyOperations(...)`; it reads
341
+ the existing comments parts and writes the required `commentsExtended.xml`
342
+ relationship and content type. Inspection reports `paraId` and
343
+ `parentCommentId` so callers can discover and verify the thread hierarchy.
344
+
345
+ A whole-paragraph `delete` that targets existing comment markup fails with
346
+ `COMMENTED_CONTENT_DELETE`. In the Node facade and CLI, the error also includes
347
+ the affected comment author and text. Resolve the feedback or explicitly remove
348
+ the comment first; the library will not silently discard reviewer context.
349
+
350
+ The batch runner keeps one live document DOM and performs one final full-document
351
+ serialization. Accuracy remains the controlling constraint: each operation has
352
+ an internal savepoint so an error or no-op cannot leak a partial edit or consumed
353
+ revision ID into later operations.
354
+
355
+ Batches are atomic by default: any operation error returns the original
356
+ `documentXml`, `hasChanges: false`, empty package artifacts, and
357
+ `rolledBack: true`. The default `continueOnError: true` still attempts the full
358
+ batch so `results` describes what would have applied. Callers that intentionally
359
+ consume partial results must pass `{ atomic: false }`; use
360
+ `{ continueOnError: false }` to stop after the first error.
361
+
362
+ Comment anchors use exact matching first, then a unique ASCII-space/NBSP
363
+ equivalent match that preserves source offsets and text. Missing anchors return
364
+ `ANCHOR_NOT_FOUND`; repeated matches return `AMBIGUOUS_ANCHOR`. Both are
365
+ operation errors and therefore roll back atomic batches. When `textToComment`
366
+ is omitted, the exact text of the resolved paragraph is used.
367
+
368
+ Operations may override the batch author and may use a strict target descriptor:
369
+
370
+ ```js
371
+ const operations = [{
372
+ type: 'replace',
373
+ author: 'Contract Editor',
374
+ target: {
375
+ exactText: 'Either party may terminate on notice.',
376
+ paragraphId: '1A2B3C4D',
377
+ index: 12,
378
+ fingerprint: 'fnv1a32:...'
379
+ },
380
+ modified: 'Either party may terminate on 30 days written notice.'
381
+ }];
382
+
383
+ const preflight = preflightOperations(documentXml, operations, 'Fallback Author');
384
+ if (!preflight.valid) {
385
+ // Resolve missing/ambiguous targets, anchors, revision policies, or conflicts.
386
+ }
387
+
388
+ const result = await applyOperationsToDocumentXml(
389
+ documentXml,
390
+ operations,
391
+ 'Fallback Author',
392
+ null,
393
+ { strictTargets: true }
394
+ );
395
+ ```
396
+
397
+ Preflight is read-only and uses strict targeting by default. It reports
398
+ `AMBIGUOUS_TARGET` with candidates instead of selecting the first duplicate,
399
+ does not use fuzzy fallback, checks comment/highlight anchors and existing
400
+ revision policy, identifies same-paragraph operation conflicts, and reports
401
+ authors plus required comments/numbering artifacts.
402
+
403
+ Application currently defaults to permissive targeting for backward compatibility,
404
+ but will default to `strictTargets: true` in v1.0.0. When permissive resolution
405
+ chooses among multiple candidate paragraphs heuristically, it emits an
406
+ `AMBIGUOUS_TARGET_HEURISTIC_USED` warning containing candidate count and migration
407
+ guidance. Callers should pass `{ strictTargets: true }` and use strict descriptors
408
+ (`paragraphId`, `index`, `occurrence`, or `fingerprint`) to prepare for v1.0.0.
409
+
410
+ ### Mutation Receipts
411
+
412
+ Both single-operation (`applyOperationToDocumentXml`) and batch
413
+ (`applyOperationsToDocumentXml`) results expose commit-aware **Mutation Receipts**
414
+ (`result.receipt` on single results and per-item `results[i].receipt`, plus `result.receipts`
415
+ for the full batch).
416
+
417
+ ```js
418
+ const result = await applyOperationsToDocumentXml(documentXml, operations, 'Agent');
419
+ for (const receipt of result.receipts) {
420
+ console.log(receipt.operationIndex, receipt.finalDisposition, receipt.committed);
421
+ console.log('Revisions:', receipt.revisionItems);
422
+ console.log('Comments:', receipt.commentIds);
423
+ }
424
+ ```
425
+
426
+ Receipts report:
427
+ - `operationIndex` (1-based), `operationId`, and `authorUsed`
428
+ - `attemptedDisposition` and `finalDisposition` (`applied`, `refused`, `no_change`, `rolled_back`, or `not_attempted`)
429
+ - `committed` (boolean: verified committed into serialized package output)
430
+ - `revisionItems` (exact allocated revision IDs with kind and target part)
431
+ - `commentIds`, `numberingIds`, and `relationshipIds`
432
+ - `affectedTargets` (resolved target coordinates) and `warnings`
433
+
434
+ Before completing an operation or batch transaction, `reconcileReceiptsAgainstOutput`
435
+ verifies every reported committed durable ID against a fresh parse of the output OOXML.
436
+ Any discrepancy fails closed and triggers immediate rollback.
437
+
438
+ ### Output Shape Matrix
439
+
440
+ Different APIs return different OOXML shapes. Use this as a packaging safety check.
441
+
442
+ | API | Typical input scope | Output field | Possible root/output shape | Safe to write directly into `word/document.xml` |
443
+ |-----|----------------------|--------------|----------------------------|--------------------------------------------------|
444
+ | `applyRedlineToOxml(...)` | Paragraph, range, or table-scope OOXML | `result.oxml` | Fragment, `<w:document>`, or package payload (`<pkg:package>`) | No. Inspect first. |
445
+ | `applyRedlineToOxmlWithListFallback(...)` | Paragraph or range-scope OOXML | `result.oxml` | Fragment, `<w:document>`, or package payload (`<pkg:package>`) | No. Inspect first. |
446
+ | `reconcileMarkdownTableOoxml(...)` | Table or paragraph-scope OOXML | `result.oxml` | Same shapes as `applyRedlineToOxml(...)` for the supplied scope | No. Inspect first. |
447
+ | `applyOperationToDocumentXml(...)` | Full `word/document.xml` string | `result.documentXml` | `<w:document>` | Yes. This is the document-safe helper. |
448
+ | `applyOperationsToDocumentXml(...)` | Full `word/document.xml` plus an operation batch | `result.documentXml` | `<w:document>` | Yes. Atomic by default; comments are applied before text-changing operations. |
449
+ | `extractReplacementNodesFromOoxml(...)` | Any OOXML payload | `{ replacementNodes, numberingXml, sourceType }` | Normalized to `fragment`, `document`, or `package` | Yes. Use this when consuming `result.oxml`. |
450
+
451
+ ### Do / Don't for Packaging
452
+
453
+ - Do use `applyOperationToDocumentXml(...).documentXml` when your intent is to replace `word/document.xml`.
454
+ - Do use `applyOperationsToDocumentXml(...)` rather than an unsorted loop for batches containing comments and replacements that target the same original paragraph.
455
+ - Redline application strips proofing markers (`w:proofErr`) from the matched target paragraph before diffing, while preserving complex-field scaffolding (`w:fldChar`, `w:instrText`) and its cached visible result as inert structure. Adjacent edits do not revise or move an unchanged field result.
456
+ - Hyperlinks, bookmarks, comment range markers, tabs/breaks, and footnote/endnote references are treated as structural OOXML that should survive adjacent redline edits instead of being orphaned or wrapped in deletions.
457
+ - Treat `w:numId w:val="0"` as numbering suppression, never as a reusable list
458
+ ID. Generated bullet and numbered paragraphs must reference a positive ID
459
+ whose definition is merged into `word/numbering.xml`.
460
+ - Do use `extractReplacementNodesFromOoxml(...)` when you are consuming `result.oxml` from paragraph/range/table APIs.
461
+ - Do merge numbering/comments artifacts with `ensureNumberingArtifactsInZip(...)` and `ensureCommentsArtifactsInZip(...)` when those parts are present. Supply `mergeNumberingXmlBySchemaOrder` when numbering already exists.
462
+ - Don't write payloads that start with `<pkg:package` directly into `word/document.xml`.
463
+ - Don't assume every `result.oxml` payload is a raw paragraph fragment.
464
+
465
+ ## Working With `.docx` Files
466
+
467
+ This package operates on OOXML strings (XML parts inside `.docx` zip archives), not raw `.docx` binaries.
468
+
469
+ Typical flow:
470
+
471
+ 1. Extract the `.docx` zip (for example with JSZip, fflate, or similar)
472
+ 2. Read `word/document.xml`
473
+ 3. Apply reconciliation APIs to XML strings
474
+ 4. Merge numbering/comments artifacts when needed
475
+ 5. Write the archive back to a `.docx` file
476
+
477
+ ```js
478
+ import JSZip from 'jszip';
479
+ import {
480
+ applyRedlineToOxml,
481
+ extractReplacementNodesFromOoxml,
482
+ ensureNumberingArtifactsInZip,
483
+ mergeNumberingXmlBySchemaOrder,
484
+ validateDocxPackage
485
+ } from '@ansonlai/docx-redline-js';
486
+ import { applyOperationToDocumentXml } from '@ansonlai/docx-redline-js/standalone-runner';
487
+
488
+ const zip = await JSZip.loadAsync(docxBuffer);
489
+ const documentXml = await zip.file('word/document.xml').async('string');
490
+
491
+ const opResult = await applyOperationToDocumentXml(
492
+ documentXml,
493
+ { type: 'redline', target: 'old text', modified: 'new text' },
494
+ 'Editor'
495
+ );
496
+
497
+ // applyOperationToDocumentXml(...) returns a full w:document payload.
498
+ zip.file('word/document.xml', opResult.documentXml);
499
+
500
+ const fragmentResult = await applyRedlineToOxml(
501
+ paragraphOoxml,
502
+ 'Item text',
503
+ '1. Item text',
504
+ { generateRedlines: true, author: 'Editor' }
505
+ );
506
+ const normalized = extractReplacementNodesFromOoxml(fragmentResult.oxml);
507
+
508
+ // If sourceType === 'package', merge extracted content/artifacts instead of
509
+ // writing the raw pkg:package payload into word/document.xml.
510
+ if (normalized.numberingXml) {
511
+ await ensureNumberingArtifactsInZip(zip, normalized.numberingXml, {
512
+ mergeNumberingXml: mergeNumberingXmlBySchemaOrder
513
+ });
514
+ }
515
+
516
+ await validateDocxPackage(zip);
517
+ const output = await zip.generateAsync({ type: 'nodebuffer' });
518
+ ```
519
+
520
+ ## Validating Output
521
+
522
+ For the test-lane design and instructions for adding regression, synthetic
523
+ Word, and real-corpus cases, see [docs/TESTING.md](./docs/TESTING.md).
524
+
525
+ Run the automated package checks:
526
+
527
+ ```bash
528
+ npm test
529
+ npm run test:isolation
530
+ npm run check:types
531
+ npm run lint
532
+ npm run test:coverage
533
+ ```
534
+
535
+ For release-time fixture export:
536
+
537
+ ```bash
538
+ node scripts/export-validation-fixtures.mjs
539
+ ```
540
+
541
+ On Windows with desktop Word installed, you can smoke-test a completed `.docx`:
542
+
543
+ ```bash
544
+ npm run smoke:word -- path/to/file.docx
545
+ ```
546
+
547
+ To validate against Word as an independent oracle (Word itself accepts and
548
+ rejects the generated revisions and the resulting text is compared to the
549
+ expected outcomes):
550
+
551
+ ```bash
552
+ npm run test:word
553
+ ```
554
+
555
+ This Windows-only test command generates an English legal/administrative task
556
+ suite under `tmp/word-validation/` and drives installed desktop Microsoft Word
557
+ through COM. Its 33 cases include targeted reliability checks for literal
558
+ content, multi-paragraph replacement, prior-revision no-op, atomic rollback,
559
+ hostile revision IDs, bookmarks, internal hyperlinks, mixed formatted runs,
560
+ content controls, table cells, structural tabs, locked complex fields,
561
+ comments, footnotes/endnotes, headers/footers, and external hyperlinks.
562
+ Structure-focused cases also assert required
563
+ OOXML elements before Word independently checks Accept All and Reject All. The
564
+ published library remains clean, host-independent JavaScript; Word automation
565
+ exists only in development scripts.
566
+
567
+ Use `npm run report:word:coverage` to print the validated task-by-structure
568
+ matrix across all 33 synthetic and 31 SuperDoc scenarios. Before a release,
569
+ `npm run review:word:prepare -- --cycle=0` creates a pending human-review
570
+ manifest with changed cases, a rotating 20% synthetic sample, and legal plus
571
+ administrative corpus representatives. See the [Word visual review guide in docs/TESTING.md](./docs/TESTING.md#microsoft-word-visual-review-guide);
572
+ preparation and AI preflight never count as human sign-off.
573
+
574
+ A nightly GitHub Actions workflow additionally validates generated fixtures
575
+ against the ECMA-376 transitional schemas (`xmllint`), opens them with
576
+ LibreOffice, and runs an extended fuzz sweep of the accept/reject round-trip
577
+ invariant with a fresh seed. See [Release validation in docs/TESTING.md](./docs/TESTING.md#release-validation-and-independent-oracles).
578
+
579
+ ## Architecture & Contributing
580
+
581
+ - **[ARCHITECTURE.md](./ARCHITECTURE.md)**: Detailed module layout, end-to-end reconciliation flow, and contributor fast orientation.
582
+ - **[AGENTS.md](./AGENTS.md)**: Concise quick reference for AI coding agents and CLI automation.
583
+ - **[docs/TESTING.md](./docs/TESTING.md)**: Comprehensive testing model, test lanes, independent oracle checks, and visual review checklist.
584
+ - **[CHANGELOG.md](./CHANGELOG.md)**: Version history, migration guides, and deprecation schedules.
585
+
586
+ ## Test Corpus Attribution
587
+
588
+ Real-document reliability testing uses selected references from
589
+ [docx-corpus](https://docxcorp.us/), built by
590
+ [SuperDoc](https://superdoc.dev/). The dataset is licensed under the
591
+ [Open Data Commons Attribution License (ODC-By) 1.0](https://opendatacommons.org/licenses/by/1-0/).
592
+
593
+ Only explicitly pinned English legal and administrative documents are eligible
594
+ for the initial corpus lane. References and provenance live in
595
+ `tests/corpus/superdoc-english-legal-administrative.json`; downloaded documents
596
+ are hash-verified and kept in ignored `tmp/` storage rather than committed. On
597
+ Windows with desktop Word installed, run the reviewed 31-scenario/23-document lane with:
598
+
599
+ ```bash
600
+ npm run test:corpus:word
601
+ ```
602
+
603
+ ODC-By applies to the database; individual documents may carry additional
604
+ rights, so each selected document must be reviewed before becoming a test case.