@ansonlai/docx-redline-js 0.8.3 → 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 (71) hide show
  1. package/ARCHITECTURE.md +221 -12
  2. package/CHANGELOG.md +803 -586
  3. package/README.md +1018 -901
  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 +4551 -967
  9. package/dist/docx-redline-js.esm.js.map +4 -4
  10. package/dist/docx-redline-js.esm.min.js +112 -106
  11. package/dist/docx-redline-js.esm.min.js.map +4 -4
  12. package/dist/docx-redline.bundle.cjs +4690 -1106
  13. package/dist/docx-redline.bundle.cjs.map +4 -4
  14. package/dist/docx-redline.bundle.js +4690 -1106
  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 +23 -4
  18. package/docs/SKILL_AUTHORING.md +165 -150
  19. package/docs/TESTING.md +1142 -1059
  20. package/docs/schemas/document-operations.schema.json +221 -168
  21. package/document/docx-document.js +1022 -1010
  22. package/engine/oxml-engine.js +567 -560
  23. package/engine/paragraph-mark-revisions.js +292 -0
  24. package/engine/reconstruction-mapper.js +312 -311
  25. package/engine/reconstruction-mode.js +183 -183
  26. package/engine/reconstruction-writer.js +67 -10
  27. package/engine/rpr-helpers.js +42 -0
  28. package/engine/run-builders.js +0 -73
  29. package/engine/surgical-diff-application.js +604 -605
  30. package/engine/table-mode.js +63 -63
  31. package/index.d.ts +51 -1
  32. package/index.js +47 -3
  33. package/node/cli-help.js +1 -0
  34. package/node/cli.js +992 -974
  35. package/orchestration/list-structural-fallback.js +67 -9
  36. package/package.json +1 -1
  37. package/pipeline/diff-engine.js +130 -1
  38. package/pipeline/ingestion-export.js +227 -243
  39. package/pipeline/ingestion-paragraph.js +93 -98
  40. package/pipeline/list-generation.js +415 -419
  41. package/pipeline/pipeline.js +206 -204
  42. package/pipeline/serialization.js +111 -94
  43. package/scripts/export-validation-fixtures.mjs +19 -6
  44. package/scripts/generate-word-revision-semantics-matrix.mjs +134 -0
  45. package/scripts/lib/word-coverage-catalogue.mjs +7 -2
  46. package/scripts/render-multilevel-bullet-visual.ps1 +2 -2
  47. package/scripts/word-com-corpus-visual-suite.ps1 +2 -2
  48. package/scripts/word-com-differential.ps1 +443 -335
  49. package/scripts/word-com-visual-suite.ps1 +2 -2
  50. package/scripts/word-revision-semantics-matrix.ps1 +108 -0
  51. package/services/batch-operation-orchestrator.js +26 -0
  52. package/services/capture-engine.js +228 -226
  53. package/services/document-operation-applier.js +215 -0
  54. package/services/document-operation-contract.js +628 -537
  55. package/services/document-operation-mutations.js +789 -46
  56. package/services/error-recovery.js +17 -0
  57. package/services/list-conversion-diagnostics.js +296 -0
  58. package/services/list-format-change.js +586 -0
  59. package/services/list-format-mutation.js +416 -0
  60. package/services/list-format.js +305 -0
  61. package/services/numbering-allocation.js +642 -0
  62. package/services/numbering-helpers.js +354 -524
  63. package/services/operation-batch-compiler.js +612 -436
  64. package/services/operation-heuristics.js +197 -173
  65. package/services/operation-preflight.js +1 -0
  66. package/services/receipt-collector.js +288 -288
  67. package/services/revision-comment-management.js +634 -594
  68. package/services/revision-fidelity-verifier.js +498 -0
  69. package/services/standalone-docx-plumbing.js +627 -631
  70. package/services/standalone-operation-runner.d.ts +606 -427
  71. package/services/table-reconciliation.js +103 -49
package/README.md CHANGED
@@ -1,901 +1,1018 @@
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
- - Localized exact replacements against inspected targets, plus optional fail-closed speculative CLI edits
20
- - Package plumbing helpers for numbering.xml, comments.xml, content types, and relationships
21
- - Zero host dependencies: works in Node.js, browsers, Deno, and similar JS runtimes with DOM parsing support
22
-
23
- ## What's New in 0.8.x
24
-
25
- The current release is 0.8.3. The CLI contract remains version 8 throughout the 0.8 line.
26
-
27
- - **0.8.0, universal document facade:** `openDocx` and `DocxDocument` are exported from the primary entry point and work
28
- on `Uint8Array` input in any JavaScript runtime. ZIP handling uses `fflate` and SHA-256 is pure JavaScript, so there are no
29
- Node built-in dependencies. The XML provider falls back to `@xmldom/xmldom` automatically, and the
30
- `@ansonlai/docx-redline-js/bundle` export provides zero-dependency single-file bundles for sandboxes such as n8n Code
31
- nodes. The `/node` entry point keeps its existing API. See the [0.8.0 release notes](./docs/releases/0.8.0.md).
32
- - **0.8.1, headers, footers and comment threads:** an operation's `part` field edits headers and footers;
33
- `inspect().headersFooters` lists them. Comment threads can be resolved or reopened with `doc.resolveComment` or the
34
- `comment_resolve` operation, and comments can be deleted by id with `doc.deleteComments({ ids })` or
35
- `docx-redline delete-comments --comment-id`. See the [0.8.1 release notes](./docs/releases/0.8.1.md).
36
- - **0.8.2, fixes:** a replacement beside a hyperlink no longer pulls adjacent plain text into the link, and Reject All no
37
- longer reorders text around a manual line break. See the [0.8.2 release notes](./docs/releases/0.8.2.md).
38
- - **0.8.3, list fixes:** Reject All now restores list edits exactly (no extra empty paragraph after an inserted
39
- paragraph, no merged source paragraphs after a list-range replacement, original list numbering kept through `openDocx`),
40
- and inspection no longer reports tracked historical list, style or heading properties as current. See the
41
- [0.8.3 release notes](./docs/releases/0.8.3.md).
42
-
43
- Version 0.7.0 introduced contract-8 localized edits: a small `find`/`replace` request against an inspected target instead
44
- of a complete paragraph, plus an optional speculative form that fails without writing when the anchor is missing or
45
- ambiguous. They remain available unchanged; see the [0.7.0 release notes](./docs/releases/0.7.0.md).
46
-
47
- ## Documentation Index
48
-
49
- | Document | Description |
50
- |---|---|
51
- | **[README.md](./README.md)** | Library overview, installation, quick start, and public API reference |
52
- | **[docs/AGENT_FAST_START.md](./docs/AGENT_FAST_START.md)** | Compact ordinary-edit protocol for structured tools and shell-only agents |
53
- | **[AGENTS.md](./AGENTS.md)** | Short repository launch card for task routing and contributor verification |
54
- | **[docs/AGENT_KNOWLEDGE_BASE.md](./docs/AGENT_KNOWLEDGE_BASE.md)** | Full agent reference, CLI workflow, operation examples, error recovery, options, and gotchas |
55
- | **[ARCHITECTURE.md](./ARCHITECTURE.md)** | Contributor architecture, module responsibilities, end-to-end data flow, and contracts |
56
- | **[docs/TESTING.md](./docs/TESTING.md)** | Complete testing guide, test lanes, independent oracle validation, and Word visual review checklist |
57
- | **[CHANGELOG.md](./CHANGELOG.md)** | Release history, breaking changes, and migration notes |
58
-
59
- ## Repository Layout
60
-
61
- The package exposes three levels of API:
62
-
63
- | Level | Entry point | Use it for |
64
- |---|---|---|
65
- | Host-independent OOXML API | `index.js` | Paragraph/range transforms and exported OOXML utilities in browsers, Node.js, or another DOM-capable runtime |
66
- | Standalone document XML runner | `services/standalone-operation-runner.js` | Applying operations to a complete `word/document.xml` string |
67
- | Node/DOCX API and CLI | `node/index.js`, `bin/docx-redline.js` | Reading, changing, validating, and writing complete `.docx` ZIP packages |
68
-
69
- Implementation folders have distinct roles: `core/` holds shared OOXML and
70
- targeting primitives; `pipeline/` handles ingestion, diffing, markdown, lists,
71
- and serialization; `engine/` performs reconciliation; `services/` coordinates
72
- document operations and package artifacts; `node/` contains Node-only ZIP and
73
- whole-document code. Tests are directly runnable `tests/*.mjs` files, while
74
- `tests/helpers/` and `tests/fixtures/` contain support code and data.
75
-
76
- Contributors and coding agents should start with the routing table in
77
- [AGENTS.md](./AGENTS.md#pick-the-route) before exploring the tree. The full
78
- dependency and ownership map is in [ARCHITECTURE.md](./ARCHITECTURE.md).
79
-
80
- For document-operation JSON, choose operations by the desired output structure,
81
- not by the everyday meaning of the type name. `redline` and `replace` provide
82
- ordinary text replacement; `list-change` and `table-reconciliation` provide
83
- structural intent; and ordinary `insert` is a compatibility alias of the
84
- redline path unless it includes a rejected-view target and anchor. In every
85
- ordinary text-bearing operation, `modified` is the complete desired content for
86
- the target. See the [operation model](./docs/AGENT_KNOWLEDGE_BASE.md#operation-model-choose-by-output-shape)
87
- and the [JSON schema](./docs/schemas/document-operations.schema.json).
88
-
89
- ## Install
90
-
91
- ### npm / Node.js
92
-
93
- ```bash
94
- npm install @ansonlai/docx-redline-js
95
- ```
96
-
97
- ### CDN (browser `<script type="module">`)
98
-
99
- ```html
100
- <script type="module">
101
- import { applyRedlineToOxml } from 'https://esm.sh/@ansonlai/docx-redline-js';
102
- </script>
103
- ```
104
-
105
- Or use the pre-bundled file (no import map needed, `diff-match-patch` is inlined):
106
-
107
- ```html
108
- <script type="module">
109
- import { applyRedlineToOxml } from 'https://cdn.jsdelivr.net/npm/@ansonlai/docx-redline-js/dist/docx-redline-js.esm.min.js';
110
- </script>
111
- ```
112
-
113
- ### Local git clone
114
-
115
- ```bash
116
- git clone https://github.com/AnsonLai/docx-redline-js.git
117
- ```
118
-
119
- ```js
120
- import { applyRedlineToOxml } from './docx-redline-js/index.js';
121
- ```
122
-
123
- ## Quick Start
124
-
125
- ### Node.js
126
-
127
- ```js
128
- import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
129
- import {
130
- configureXmlProvider,
131
- setDefaultAuthor,
132
- applyRedlineToOxml
133
- } from '@ansonlai/docx-redline-js';
134
-
135
- configureXmlProvider({ DOMParser, XMLSerializer });
136
- setDefaultAuthor('My App');
137
-
138
- const result = await applyRedlineToOxml(
139
- paragraphOoxml,
140
- 'Original sentence.',
141
- 'Updated sentence.',
142
- { generateRedlines: true, author: 'Editor' }
143
- );
144
-
145
- console.log(result.hasChanges);
146
- console.log(result.oxml);
147
- ```
148
-
149
- ### Browser
150
-
151
- ```js
152
- import {
153
- setDefaultAuthor,
154
- applyRedlineToOxml
155
- } from '@ansonlai/docx-redline-js';
156
-
157
- setDefaultAuthor('Browser Editor');
158
-
159
- const result = await applyRedlineToOxml(oxml, original, modified, {
160
- generateRedlines: true
161
- });
162
- ```
163
-
164
- ## API Reference
165
-
166
- ### Agent-friendly inspection and complete DOCX editing
167
-
168
- ```js
169
- import { inspectDocumentParts } from '@ansonlai/docx-redline-js';
170
- const inventory = inspectDocumentParts({ documentXml, commentsXml, numberingXml });
171
- ```
172
-
173
- Inspection returns exact paragraph text, target IDs/fingerprints, headings,
174
- table/list context, revision authors, and joined comment anchors. Filters such
175
- as `search`, `indexes`, `range`, `revisedOnly`, `inTable`, and `skipEmpty`
176
- limit output. Search is a case-insensitive substring match; `around` adds nearby drafting
177
- context, while `limit` and `after` page direct hits. When a search yields 0
178
- matches in the active view, `selection.hint` provides guidance if matches exist
179
- in the alternate revision view (such as searching for deleted text to restore).
180
- Compact `extract --revised` output retains revision authors and marks paragraphs
181
- whose content is wholly hidden in the selected view as `deleted` or `inserted`.
182
- One such paragraph can be restored without restating it:
183
- `docx-redline apply input.docx --restore --target-id ID --output reviewed.docx`.
184
- Add one exact `--find`/`--replace` pair to make a localized correction while
185
- restoring. The shortcut requires a strong rejected-view paragraph ID and does
186
- not infer a range or adjacent content.
187
- `revisionView` accepts `accepted`, `rejected`, or `current`. Returned
188
- paragraphs identify matches versus context and include `humanReference` for
189
- user-facing summaries.
190
-
191
- For complete `.docx` buffers in Node:
192
-
193
- ```js
194
- import { openDocx } from '@ansonlai/docx-redline-js/node';
195
- const document = openDocx(inputBuffer);
196
- const result = await document.applyOperations(operations, {
197
- author: 'Editor', atomic: true, validate: true
198
- });
199
- const outputBuffer = result.toBuffer();
200
- ```
201
-
202
- The Node facade performs edits, artifact merges, package wiring, validation,
203
- and commit as one transaction. It defaults to strict targets and returns the
204
- untouched input with `written: false` on atomic failure. It is isolated from
205
- the root/browser dependency graph.
206
-
207
- Install `@xmldom/xmldom` alongside the package when using the Node facade or
208
- CLI; it remains an optional peer so browser consumers do not install a DOM shim.
209
-
210
- #### Example agent session wrapper (development only)
211
-
212
- [`examples/agent-session-wrapper.mjs`](./examples/agent-session-wrapper.mjs)
213
- demonstrates how a custom agent harness can keep one `DocxDocument` open, return
214
- short revision-bound target handles, apply safe defaults once, and translate a
215
- narrow edit request into canonical document operations:
216
-
217
- ```js
218
- import { readFile } from 'node:fs/promises';
219
- import { createExampleAgentSession } from './examples/agent-session-wrapper.mjs';
220
-
221
- const session = createExampleAgentSession(await readFile('contract.docx'), {
222
- profile: { author: 'Editor' }
223
- });
224
- const inspection = session.inspect({ search: 'termination', around: 2 });
225
- const clause = inspection.targets.find(target => target.role === 'match');
226
- const result = await session.applyEdits([{
227
- target: clause.handle,
228
- replacements: [{
229
- find: 'The Company may terminate',
230
- replace: 'Either party may terminate'
231
- }]
232
- }]);
233
- ```
234
-
235
- This file is a testable sample, not a package export or supported alternate
236
- mutation engine. It delegates to `@ansonlai/docx-redline-js/node`, is excluded
237
- from the published package files, and is intended to help MCP servers, Claude
238
- skills, OpenCode tools, and other custom harnesses design thin integrations.
239
- The sample binds each handle to the inspected package revision and view, returns
240
- new handles after successful mutations, and expands localized exact replacements
241
- into complete desired paragraph text before delegating to the canonical redline
242
- operation. Duplicate matches require an explicit `occurrence`; missing,
243
- ambiguous, overlapping, and conflicting patches fail before document mutation.
244
- From a source checkout, run `npm run benchmark:agent` to compare its native
245
- execution and serialized request size with a canonical stateless Node workflow.
246
- The example and its benchmark are excluded from the published package. The
247
- benchmark explicitly does not claim to measure LLM reasoning or provider/tool
248
- latency. Checked comparative results are in the
249
- [agent protocol rollout audit](./docs/validation-reports/2026-09-12-agent-protocol-rollout.md).
250
-
251
- > **Pre-1.0 integration warning:** Agent skills, MCP servers, and harnesses
252
- > should pin the exact `@ansonlai/docx-redline-js` release they were tested
253
- > against and declare the CLI contract version/capabilities they require. Do
254
- > not treat a newly installed release or contract as verified until the
255
- > integration's compatibility tests pass. See the
256
- > [skill-authoring contract](./docs/SKILL_AUTHORING.md#runtime-negotiation).
257
-
258
- ### Agent CLI
259
-
260
- ```bash
261
- # Default agent workflow: inspect once, then apply once
262
- docx-redline extract contract.docx --search "termination" --around 3
263
- node emit-operations.mjs | docx-redline apply contract.docx --operations - --profile agent --compact --output reviewed.docx
264
- ```
265
-
266
- ```bash
267
- # Inline one-liner edit (no operations file needed)
268
- docx-redline apply contract.docx --target "Original clause text" --modified "New clause text" --output reviewed.docx
269
-
270
- # Direct edit without tracked changes
271
- docx-redline apply contract.docx --target "Typo fix" --modified "Fixed typo" --no-redlines --output clean.docx
272
-
273
- # Cross-author edit inside another reviewer's pending insertion
274
- docx-redline apply contract.docx --target "Another author's clause" --modified "Revised clause" --existing-revisions slice-cross-author --output reviewed.docx
275
-
276
- # High-assurance atomic batch with nonzero exit on any incomplete result
277
- docx-redline apply contract.docx --operations operations.json --atomic --require-complete --output reviewed.docx
278
-
279
- # Optional read-only audit before a deliberately staged batch
280
- docx-redline preflight contract.docx --operations operations.json --author "Editor"
281
- ```
282
-
283
- All commands emit JSON on stdout. `apply` defaults:
284
- - **Author**: Defaults to `'AI Redliner'` (or `DOCX_REDLINE_AUTHOR` environment variable).
285
- - **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.
286
- - **Existing revisions**: Defaults to `'merge-same-author'`. Pass `--existing-revisions slice-cross-author` to edit inside another reviewer's pending insertions with native carrier slicing.
287
- - **Transactionality**: Defaults to `atomic: false` (applies valid operations and reports any failures). Pass `--atomic` for all-or-nothing rollback on any operation error.
288
- - **Complete-success exit**: Pass `--require-complete` when a progressive `partial` result must exit with code `3`; errors exit with code `2`. Without the flag, partial results retain the legacy zero exit code, so always inspect `completion`.
289
- - **Agent profile**: `--profile agent` enables complete-success exit behavior while retaining the ordinary progressive transaction and existing-revision defaults. Compose it with `--atomic` or an explicit `--existing-revisions` policy when intended, and verify the resolved `effectiveOptions`.
290
- - **Operations from stdin**: `--operations -` reads the same array or `{ operations, expectedRevision }` envelope accepted from a file. Feed it from a JSON serializer or structured process API, not shell-interpolated legal text.
291
- - **Compact stdout**: `--compact` emits one-line mutation JSON. It changes serialization only, not operation semantics.
292
- - **Tracked changes**: Defaults to `generateRedlines: true`. Pass `--no-redlines` when clean direct text edits are desired.
293
- - **Inline edits**: Use `--target <text>` with `--modified <text>` or `--comment <text>` for quick one-liners without creating a JSON file.
294
- - **Localized edits**: After extraction, use `replacements` with the inspected `paragraphId` plus `fingerprint` to avoid restating a long paragraph. Retain `exactText` locally for drafting. Targetless `--find`/`--replace` is an optional speculative path described below.
295
- - **Invisible spaces**: Localized matching is case-sensitive and tries exact text first. If no exact span exists, ordinary spaces may match DOCX non-breaking spaces; ambiguous equivalent matches still fail closed, and `change.replacements[i].matchMode` reports `space_equivalent` when used.
296
- - **Compact mutation results**: `apply`, `accept`, `reject`, and `delete-comments` omit full OOXML/package payloads and inspection text from stdout. CLI operation results omit duplicate nested receipt bodies; the ordered top-level `receipts` array is authoritative. Successful exact target-match diagnostics are omitted and equivalent-whitespace matches retain only mode/count. Node and standalone-runner results remain unchanged. Use `validate` when full issue arrays are needed.
297
-
298
- Inspection commands default to 20 direct matches and a 48 KiB soft response
299
- budget when no explicit positional scope is supplied. When a search returns 0
300
- matches in the requested view, `selection.hint` provides immediate guidance if
301
- matches exist in the alternate view (such as searching for a deleted clause to
302
- restore). Use `--limit` with `--after <paragraph-index>` to continue, `--around N`
303
- (aliases `--context` and `-C`) to include nearby paragraphs, and `--all` only
304
- for deliberate unbounded inspection. Every retained target is complete; an
305
- individually oversized paragraph is returned whole with an `oversizeItem`
306
- marker. Machine `index` and `ref` fields are for targeting and pagination, not
307
- user-facing Word locations; use `humanReference`, `provision`, or
308
- `nearestHeading` in reports.
309
-
310
- `docx-redline version` reports contract version 8 and the additive
311
- `command-help-v1`, `inspection-context-v1`, `bounded-inspection-v1`,
312
- `human-document-references-v1`, `batch-start-source-binding`,
313
- `recovery-envelope-v1`, `require-complete-exit`, `operations-stdin`,
314
- `agent-safety-profile-v2`, `deduplicated-cli-receipts`, and
315
- `compact-cli-json-v1`, `localized-replacements-v1`,
316
- `speculative-search-apply-v1`, `localized-change-summary-v1`, and
317
- `restore-shortcuts-v1`, plus `localized-space-equivalence-v1` capabilities. Header/footer
318
- `part` targeting, the `comment_resolve` operation and `delete-comments --comment-id` are advertised as
319
- `header-footer-parts-v1`, `comment-resolve-operation-v1` and `delete-comments-by-id-v1`.
320
- Wrappers should negotiate only the
321
- capabilities they use. Run `docx-redline <command> --help` for that command's
322
- machine-readable options, behavior, exit codes, canonical GitHub documentation links, and compact examples.
323
-
324
- #### Optional speculative CLI path
325
-
326
- Harnesses with a measured confidence policy may attempt a targetless localized
327
- edit. This is not the default agent workflow:
328
-
329
- ```bash
330
- # Globally unique literal
331
- docx-redline apply contract.docx --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
332
-
333
- # Optional directional scope using a longer, fairly unique nearby phrase
334
- docx-redline apply contract.docx --search "distinctive nearby heading or phrase" --context-range 1:3 --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
335
- ```
336
-
337
- Generic or repeated anchors can widen the unioned scope until multiple
338
- paragraphs contain `find`, causing a safe `AMBIGUOUS_TARGET` failure and a
339
- recovery turn. `--around 3` is symmetric. On success,
340
- `change.context.anchorMatchCount > 1` signals a repeated anchor; confirm the
341
- returned location and bounded evidence. Matching remains case-sensitive and
342
- exact-first, with a narrow ordinary-space/NBSP fallback that still refuses
343
- ambiguity.
344
-
345
- See the [compact agent fast start](./docs/AGENT_FAST_START.md), the
346
- [skill/harness authoring contract](./docs/SKILL_AUTHORING.md), and the
347
- [operation JSON Schema](docs/schemas/document-operations.schema.json).
348
-
349
- ### Configuration (call once at startup)
350
-
351
- | Function | Purpose |
352
- |----------|---------|
353
- | `configureXmlProvider({ DOMParser, XMLSerializer })` | Inject XML parser. Required in Node.js; browsers usually provide native support. |
354
- | `configureLogger({ log, warn, error })` | Replace default console logger. |
355
- | `setDefaultAuthor(name)` | Set fallback track-change author (default: `'AI Redliner'`, configurable via `DOCX_REDLINE_AUTHOR` environment variable). |
356
- | `setPlatform(label)` | Set platform label for diagnostics (default: `'Unknown'`). |
357
-
358
- ### Options and Defaults Reference
359
-
360
- | Option | Type | Default | Description |
361
- |--------|------|---------|-------------|
362
- | `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. |
363
- | `author` | `string` | `'AI Redliner'` | Reviewer/author name stamped on generated tracked changes and comments. Overridable via `DOCX_REDLINE_AUTHOR` env variable. |
364
- | `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. |
365
- | `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. |
366
- | `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. |
367
- | `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. |
368
- | `existingRevisions` | `string` | `'merge-same-author'` | How to handle paragraphs with existing tracked changes. `'merge-same-author'` merges revisions from the same author and protects other authors with `EXISTING_REVISIONS`. `'slice-cross-author'` keeps that same-author merge behavior while allowing Word-native edits inside another author's pending insertion. Pass `'accept-all-first'` to normalize prior revisions or `'reject-input'` to refuse editing revised paragraphs. |
369
- | `removeFormatting` | `boolean` | `false` | When `true` and the text is unchanged with no Markdown hints, strips existing bold/italic/underline/strikethrough formatting. |
370
- | `sanitizeInput` | `boolean` | `false` | Opt-in removal of standalone leading assistant-preface lines. Literal dollar signs and `\n` sequences are always preserved. |
371
-
372
- Same-author revision merging refuses paragraphs containing comment anchors with
373
- `COMMENTED_CONTENT_MERGE`; resolve those comments first so the merge cannot
374
- remove or orphan their anchors.
375
-
376
- Common result fields:
377
-
378
- | Field | Purpose |
379
- |-------|---------|
380
- | `status` | Operation status: `'ok'`, `'partial'`, `'no-op'`, or `'error'`. |
381
- | `error` | Present on failure; retains a stable `code` and adds recovery envelope version, stage, category, bounded context, and a machine-readable recovery action. |
382
- | `written` | CLI/facade boolean indicating whether the output file was successfully written to disk. |
383
- | `completion` | CLI-only boolean that is `true` only when a destination was written, top-level status is neither error nor partial, and no operation result failed. |
384
- | `rolledBack` | Present and `true` when an atomic batch encountered an error and rolled back all changes. |
385
-
386
- Word diffs are deterministic by default (no wall-clock timeout). Inputs above
387
- the safe ceiling of 262,144 unique diff tokens return `DIFF_TOKEN_LIMIT` with
388
- the original OOXML unchanged so callers can split the operation without risking
389
- silent text loss.
390
-
391
- ### Editing inside existing revisions (cross-author slicing)
392
-
393
- During multi-round legal negotiations, a reviewer often needs to edit text that was previously inserted by another reviewer whose revision is still pending. Pass `existingRevisions: 'slice-cross-author'` (or `--existing-revisions slice-cross-author` via CLI) to edit inside another author's pending insertion without erasing their attribution or requiring prior acceptance:
394
-
395
- ```js
396
- const result = await applyRedlineToOxml(paragraphOoxml, originalText, modifiedText, {
397
- generateRedlines: true,
398
- author: 'Reviewer B',
399
- existingRevisions: 'slice-cross-author'
400
- });
401
- ```
402
-
403
- The engine applies Microsoft Word Desktop-native tracked change structures:
404
- - **Insertions inside pending insertions**: The carrier `<w:ins>` is split into sibling `<w:ins>` containers at the paragraph level (`[ins(Author A), ins(Author B), ins(Author A)]`), maintaining strict schema compliance without illegal `ins/ins` nesting.
405
- - **Deletions inside pending insertions**: The new `<w:del>` is nested directly inside the carrier `<w:ins>` (valid under ECMA-376 Part 1 `CT_RunTrackChange`), ensuring that if Author A's insertion is rejected, Author B's dependent deletion is cleanly removed with it.
406
- - **Straddle deletions**: Deletions spanning between baseline text and pending insertions cleanly partition across their respective container contexts without invalid coalescing.
407
- - **Lifecycle parity**: Accepting or rejecting either reviewer independently produces identical results to Microsoft Word Desktop's native review pane.
408
-
409
- ### Replacing a heading with a tracked list
410
-
411
- The list route treats a one-paragraph heading expanded into multiple markdown
412
- items as a structural block replacement. The deleted heading stays in its own
413
- tracked paragraph, and every inserted item becomes a separate Word list
414
- paragraph. Accepting the revisions produces only the list items; rejecting them
415
- restores the original heading exactly.
416
-
417
- ```js
418
- const result = await applyRedlineToOxml(
419
- headingParagraphOoxml,
420
- 'A.\tPURPOSE',
421
- '* Article A. Purpose and Interagency Alignment\n' +
422
- '* Key Focus: Joint Street Outreach & Medical Triage',
423
- { generateRedlines: true, author: 'Editor' }
424
- );
425
- ```
426
-
427
- `w:numId w:val="0"` means numbering is explicitly suppressed; it is not a list
428
- definition and is never reused for generated bullets. New list items receive a
429
- positive numbering ID. Font family, size, language, and related script
430
- properties are inherited from the source paragraph, while heading emphasis
431
- (such as bold or underline) is not copied unless the replacement markdown asks
432
- for it. For a complete `word/document.xml`, prefer
433
- `applyOperationToDocumentXml(...)` so replacement nodes and numbering artifacts
434
- are imported at the correct scope.
435
-
436
- ### Planning large mixed-content insertions
437
-
438
- For an attachment or schedule containing several kinds of content, use
439
- `planStructuredReplacement(...)` before applying the edit. It decomposes the
440
- Markdown into typed heading, paragraph, list, and table blocks, normalizes the
441
- block boundaries, and returns one atomic operation with
442
- `structuredContent: true`.
443
-
444
- ```js
445
- import { planStructuredReplacement } from '@ansonlai/docx-redline-js';
446
-
447
- const plan = planStructuredReplacement(
448
- { exactText: 'Date', index: 172 },
449
- `# ATTACHMENT 4
450
-
451
- Introductory paragraph.
452
-
453
- | Agency | Contact |
454
- | --- | --- |
455
- | BCHD | Dr. Jenkins |
456
-
457
- ## Protocol
458
-
459
- 1. Joint clearance
460
- 2. Rapid escalation`,
461
- { author: 'Editor' }
462
- );
463
-
464
- if (!plan.valid || !plan.operation) {
465
- throw new Error(plan.issues.map(issue => issue.message).join(' '));
466
- }
467
-
468
- const result = await document.applyOperations([plan.operation], {
469
- author: 'Editor', atomic: true, validate: true
470
- });
471
- ```
472
-
473
- Every Markdown table must include a separator row immediately below its header.
474
- Missing separators, missing data rows, and inconsistent column counts are
475
- reported as structured errors instead of being inserted as visible pipe text.
476
- Use `#` through `#########` to request Word heading paragraphs; blank lines
477
- separate paragraphs; adjacent list markers form a real Word list. Keep the
478
- planned content in one operation so later blocks do not depend on an anchor that
479
- an earlier block has already replaced.
480
-
481
- ### Pipeline (lower-level access)
482
-
483
- | Function | Purpose |
484
- |----------|---------|
485
- | `ReconciliationPipeline` | Direct pipeline access (ingest, diff, patch, serialize). |
486
- | `ingestWordOoxmlToPlainText(oxml)` | Extract plain text from OOXML. |
487
- | `ingestWordOoxmlToMarkdown(oxml)` | Convert OOXML to markdown. |
488
- | `ingestWordOoxmlToPlainTextResult(oxml)` | Extract text as `{ text, status, error?, warnings? }`, distinguishing malformed input from an empty document. |
489
- | `ingestWordOoxmlToMarkdownResult(oxml)` | Markdown counterpart to the result-returning plain-text helper. |
490
- | `ingestOoxml(oxml)` | Flatten OOXML into an internal run model with offsets. |
491
- | `preprocessMarkdown(text)` | Normalize markdown and extract format hints. |
492
- | `analyzeStructuredContent(markdown)` | Decompose mixed Markdown into typed blocks and report malformed table syntax without creating an operation. |
493
- | `planStructuredReplacement(target, markdown, options)` | Validate mixed content and return one atomic `structuredContent` replacement operation, or `operation: null` with issues. |
494
- | `containsTrackedChanges(xmlDoc)` | Detect `w:ins`, `w:del`, move revisions, property changes, and paragraph-mark revision markup in a parsed OOXML document/fragment. |
495
- | `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. |
496
-
497
- ### Services
498
-
499
- | Function | Purpose |
500
- |----------|---------|
501
- | `injectCommentsIntoOoxml(oxml, comments, options)` | Add comments anchored to text ranges. |
502
- | `applyCommentReplyToParts(options)` | Build a threaded reply in `comments.xml` and `commentsExtended.xml` without adding a document-body anchor. |
503
- | `acceptTrackedChangesInOoxml(oxml, { author?, allAuthors? })` | Accept `w:ins` / `w:del` / `w:moveFrom` / `w:moveTo` / `*PrChange` revisions for one author or all authors. |
504
- | `rejectTrackedChangesInOoxml(oxml, { author?, allAuthors? })` | Reject `w:ins` / `w:del` / `w:moveFrom` / `w:moveTo` / `*PrChange` revisions for one author or all authors. |
505
- | `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`. |
506
- | `generateTableOoxml(headers, rows, options)` | Generate a `w:tbl` from tabular data. |
507
- | `createDynamicNumberingIdState(numberingXml)` | Allocate numbering IDs without collisions. |
508
- | `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. |
509
- | `ensureCommentsArtifactsInZip(zip, commentsXml)` | Merge comments artifacts into a `.docx` package. |
510
- | `ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXml)` | Add or replace modern Word comment-thread metadata in a `.docx` package. |
511
- | `validateDocxPackage(zip)` | Validate `.docx` structural consistency. |
512
-
513
- Malformed OOXML never escapes these public transform APIs as a raw parser
514
- exception. Transforms return `status: 'error'` with `error.code === 'PARSE_ERROR'`;
515
- validators return a `PARSE_ERROR` issue. Recoverable XML parser
516
- diagnostics are forwarded through the configured logger and included in
517
- `warnings` where the result shape supports them.
518
-
519
- Cross-author slicing also verifies its exact accepted-view text before success.
520
- If a structural boundary prevents exact reconstruction, the transform returns
521
- `status: 'error'` with `error.code === 'PATCH_ROUNDTRIP_MISMATCH'`,
522
- `hasChanges: false`, and the original OOXML unchanged.
523
- Pure insertion-only slicing uses an exact character-local diff so repeated words
524
- cannot move an insertion to a different occurrence. Leading/trailing spaces,
525
- tabs, and non-breaking spaces are treated as real changes rather than no-ops.
526
- For text-bearing replacements, the exact accepted-view text of the resolved
527
- paragraph—not a space-normalized caller target—defines mutation offsets. Word-level replacement
528
- hunks that differ only by ordinary spaces and NBSPs are refined to character
529
- edits so unchanged hyperlinks and their relationship attributes stay in place.
530
- Runner results expose bounded `resolvedTarget.targetTextMatch` code-point
531
- diagnostics when equivalent whitespace was used to identify the target.
532
-
533
- ### Deep Imports
534
-
535
- For advanced usage, import specific submodules:
536
-
537
- ```js
538
- import {
539
- applyOperationToDocumentXml,
540
- applyOperationsToDocumentXml,
541
- preflightOperations,
542
- orderOperationsForStableTargets
543
- } from '@ansonlai/docx-redline-js/standalone-runner';
544
- import { getParagraphText } from '@ansonlai/docx-redline-js/core/paragraph-targeting.js';
545
- ```
546
-
547
- 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`.
548
-
549
- Before mutation, the runner resolves strong source descriptors against the
550
- immutable batch-start document and binds them to session-local source
551
- identities. Independent edits therefore do not need to be manually sorted when
552
- an earlier structural rewrite changes later paragraph indexes or fingerprints.
553
- Targets that deliberately refer to uniquely created paragraph text are compiled
554
- into an internal capture dependency. True overlap is not guessed: incompatible
555
- writes to one source fail before mutation with `OVERLAPPING_SOURCE_TARGETS` or
556
- `REVISION_ORDER_CONFLICT`, and mutating capture fan-out without distinct
557
- selectors fails with `CAPTURE_FANOUT_CONFLICT`.
558
-
559
- Threaded replies use a comment operation with no body target:
560
-
561
- ```js
562
- { type: 'comment_reply', parentCommentId: 8, commentContent: 'Agreed; I revised this.', author: 'Editor' }
563
- ```
564
-
565
- For complete `.docx` files, use `openDocx(...).applyOperations(...)`; it reads
566
- the existing comments parts, writes the required `commentsExtended.xml`
567
- relationship and content type, adds the reply's own range/reference markers to the body
568
- (Word does not display a reply without them), and keeps `commentsIds.xml` and
569
- `commentsExtensible.xml` in step when they exist. Inspection reports `paraId`, `parentCommentId` and `done`
570
- so callers can discover and verify the thread hierarchy.
571
-
572
- Resolve or reopen a thread with an operation (usable from the CLI through `apply --operations`) or the API:
573
-
574
- ```js
575
- { type: 'comment_resolve', commentId: 8 } // resolve
576
- { type: 'comment_resolve', commentId: 8, resolved: false } // reopen
577
- await doc.resolveComment(8, { resolved: true });
578
- ```
579
-
580
- Resolved is a thread-level state in Word, so any id in the thread updates the root and all replies. Delete
581
- specific comments with `doc.deleteComments({ ids: [8] })` or `docx-redline delete-comments file.docx --comment-id 8`;
582
- deleting a thread root also deletes its replies.
583
-
584
- Headers and footers are edited by adding `part` to an operation: `{ type: 'replace', part: { kind: 'footer' }, target: { exactText: 'Draft v1' }, modified: 'Draft v2' }`.
585
- `openDocx(...).inspect().headersFooters` lists the parts (path, kind, type, sections, text, whether they contain
586
- fields). Fields such as `PAGE` are treated as atomic, comments are not allowed in headers or footers, and `accept`/`reject`
587
- also resolve revisions inside them. Creating a header or footer that does not exist is not supported; see
588
- [Known limitations](docs/plans/KNOWN_BUGS.md#4-headers-and-footers).
589
-
590
- A whole-paragraph `delete` that targets existing comment markup fails with
591
- `COMMENTED_CONTENT_DELETE`. In the Node facade and CLI, the error also includes
592
- the affected comment author and text. Resolve the feedback or explicitly remove
593
- the comment first; the library will not silently discard reviewer context.
594
-
595
- The batch runner keeps one live document DOM and performs one final full-document
596
- serialization. Accuracy remains the controlling constraint: each operation has
597
- an internal savepoint so an error or no-op cannot leak a partial edit or consumed
598
- revision ID into later operations.
599
-
600
- Batches are progressive by default (`atomic: false`): valid operations commit
601
- while failed operations remain unapplied and are reported in `results`. Pass
602
- `{ atomic: true }` when any operation error must return the original
603
- `documentXml`, `hasChanges: false`, empty package artifacts, and
604
- `rolledBack: true`. The default `continueOnError: true` still attempts the full
605
- batch so `results` describes every operation; use `{ continueOnError: false }`
606
- to stop after the first error.
607
-
608
- Every failed or partial mutation includes `retryPlan`. Its `base` is `original`
609
- after rollback/no commit and `output` after a progressive partial commit;
610
- `committedIndexes`, `failedIndexes`, and `unattemptedIndexes` identify the safe
611
- replay scope. Errors use recovery envelope version 1 and always report
612
- `sameArgumentsSafe: false`. Follow `error.recovery.action`; do not infer a retry
613
- from prose. Authorization-sensitive actions, such as resolving comments, are
614
- marked with `requiresUserAuthorization: true`. For `EXISTING_REVISIONS`, the
615
- non-normalizing surgical recommendation is `slice-cross-author`; accepting or
616
- rejecting prior revisions still requires explicit authority.
617
-
618
- Comment anchors use exact matching first, then a unique ASCII-space/NBSP
619
- equivalent match that preserves source offsets and text. Missing anchors return
620
- `ANCHOR_NOT_FOUND`; repeated matches return `AMBIGUOUS_ANCHOR`. Both are
621
- operation errors and therefore roll back atomic batches. When `textToComment`
622
- is omitted, the exact text of the resolved paragraph is used.
623
-
624
- Operations may override the batch author and may use a strict target descriptor:
625
-
626
- ```js
627
- const operations = [{
628
- type: 'replace',
629
- author: 'Contract Editor',
630
- target: {
631
- exactText: 'Either party may terminate on notice.',
632
- paragraphId: '1A2B3C4D',
633
- index: 12,
634
- fingerprint: 'fnv1a32:...'
635
- },
636
- modified: 'Either party may terminate on 30 days written notice.'
637
- }];
638
-
639
- const preflight = preflightOperations(documentXml, operations, 'Fallback Author');
640
- if (!preflight.valid) {
641
- // Resolve missing/ambiguous targets, anchors, revision policies, or conflicts.
642
- }
643
-
644
- const result = await applyOperationsToDocumentXml(
645
- documentXml,
646
- operations,
647
- 'Fallback Author',
648
- null,
649
- { strictTargets: true }
650
- );
651
- ```
652
-
653
- Preflight is read-only and uses strict targeting by default. It reports
654
- `AMBIGUOUS_TARGET` with candidates instead of selecting the first duplicate,
655
- does not use fuzzy fallback, checks comment/highlight anchors and existing
656
- revision policy, identifies same-paragraph operation conflicts, and reports
657
- authors plus required comments/numbering artifacts.
658
-
659
- Application currently defaults to permissive targeting for backward compatibility,
660
- but will default to `strictTargets: true` in v1.0.0. When permissive resolution
661
- chooses among multiple candidate paragraphs heuristically, it emits an
662
- `AMBIGUOUS_TARGET_HEURISTIC_USED` warning containing candidate count and migration
663
- guidance. Callers should pass `{ strictTargets: true }` and use strict descriptors
664
- (`paragraphId`, `index`, `occurrence`, or `fingerprint`) to prepare for v1.0.0.
665
-
666
- ### Mutation Receipts
667
-
668
- Both single-operation (`applyOperationToDocumentXml`) and batch
669
- (`applyOperationsToDocumentXml`) results expose commit-aware **Mutation Receipts**
670
- (`result.receipt` on single results and per-item `results[i].receipt`, plus `result.receipts`
671
- for the full batch).
672
-
673
- This full receipt shape is retained by the Node facade and standalone runner.
674
- Only compact CLI JSON removes duplicate `results[i].receipt` bodies and keeps
675
- the ordered top-level `receipts` array.
676
-
677
- ```js
678
- const result = await applyOperationsToDocumentXml(documentXml, operations, 'Agent');
679
- for (const receipt of result.receipts) {
680
- console.log(receipt.operationIndex, receipt.finalDisposition, receipt.committed);
681
- console.log('Revisions:', receipt.revisionItems);
682
- console.log('Comments:', receipt.commentIds);
683
- }
684
- ```
685
-
686
- Receipts report:
687
- - `operationIndex` (1-based), `operationId`, and `authorUsed`
688
- - `attemptedDisposition` and `finalDisposition` (`applied`, `refused`, `no_change`, `rolled_back`, or `not_attempted`)
689
- - `committed` (boolean: verified committed into serialized package output)
690
- - `revisionItems` (exact allocated revision IDs with kind and target part)
691
- - `commentIds`, `numberingIds`, and `relationshipIds`
692
- - `affectedTargets` (resolved target coordinates) and `warnings`
693
-
694
- Before completing an operation or batch transaction, `reconcileReceiptsAgainstOutput`
695
- verifies every reported committed durable ID against a fresh parse of the output OOXML.
696
- Any discrepancy fails closed and triggers immediate rollback.
697
-
698
- ### Output Shape Matrix
699
-
700
- Different APIs return different OOXML shapes. Use this as a packaging safety check.
701
-
702
- | API | Typical input scope | Output field | Possible root/output shape | Safe to write directly into `word/document.xml` |
703
- |-----|----------------------|--------------|----------------------------|--------------------------------------------------|
704
- | `applyRedlineToOxml(...)` | Paragraph, range, or table-scope OOXML | `result.oxml` | Fragment, `<w:document>`, or package payload (`<pkg:package>`) | No. Inspect first. |
705
- | `applyRedlineToOxmlWithListFallback(...)` | Paragraph or range-scope OOXML | `result.oxml` | Fragment, `<w:document>`, or package payload (`<pkg:package>`) | No. Inspect first. |
706
- | `reconcileMarkdownTableOoxml(...)` | Table or paragraph-scope OOXML | `result.oxml` | Same shapes as `applyRedlineToOxml(...)` for the supplied scope | No. Inspect first. |
707
- | `applyOperationToDocumentXml(...)` | Full `word/document.xml` string | `result.documentXml` | `<w:document>` | Yes. This is the document-safe helper. |
708
- | `applyOperationsToDocumentXml(...)` | Full `word/document.xml` plus an operation batch | `result.documentXml` | `<w:document>` | Yes. Atomic by default; comments are applied before text-changing operations. |
709
- | `extractReplacementNodesFromOoxml(...)` | Any OOXML payload | `{ replacementNodes, numberingXml, sourceType }` | Normalized to `fragment`, `document`, or `package` | Yes. Use this when consuming `result.oxml`. |
710
-
711
- ### Do / Don't for Packaging
712
-
713
- - Do use `applyOperationToDocumentXml(...).documentXml` when your intent is to replace `word/document.xml`.
714
- - Do use `applyOperationsToDocumentXml(...)` rather than an unsorted loop for batches containing comments and replacements that target the same original paragraph.
715
- - 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.
716
- - 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.
717
- - Treat `w:numId w:val="0"` as numbering suppression, never as a reusable list
718
- ID. Generated bullet and numbered paragraphs must reference a positive ID
719
- whose definition is merged into `word/numbering.xml`.
720
- - Do use `extractReplacementNodesFromOoxml(...)` when you are consuming `result.oxml` from paragraph/range/table APIs.
721
- - Do merge numbering/comments artifacts with `ensureNumberingArtifactsInZip(...)` and `ensureCommentsArtifactsInZip(...)` when those parts are present. Supply `mergeNumberingXmlBySchemaOrder` when numbering already exists.
722
- - Don't write payloads that start with `<pkg:package` directly into `word/document.xml`.
723
- - Don't assume every `result.oxml` payload is a raw paragraph fragment.
724
-
725
- ## Working With `.docx` Files
726
-
727
- This package operates on OOXML strings (XML parts inside `.docx` zip archives), not raw `.docx` binaries.
728
-
729
- Typical flow:
730
-
731
- 1. Extract the `.docx` zip (for example with JSZip, fflate, or similar)
732
- 2. Read `word/document.xml`
733
- 3. Apply reconciliation APIs to XML strings
734
- 4. Merge numbering/comments artifacts when needed
735
- 5. Write the archive back to a `.docx` file
736
-
737
- ```js
738
- import JSZip from 'jszip';
739
- import {
740
- applyRedlineToOxml,
741
- extractReplacementNodesFromOoxml,
742
- ensureNumberingArtifactsInZip,
743
- mergeNumberingXmlBySchemaOrder,
744
- validateDocxPackage
745
- } from '@ansonlai/docx-redline-js';
746
- import { applyOperationToDocumentXml } from '@ansonlai/docx-redline-js/standalone-runner';
747
-
748
- const zip = await JSZip.loadAsync(docxBuffer);
749
- const documentXml = await zip.file('word/document.xml').async('string');
750
-
751
- const opResult = await applyOperationToDocumentXml(
752
- documentXml,
753
- { type: 'redline', target: 'old text', modified: 'new text' },
754
- 'Editor'
755
- );
756
-
757
- // Restoring another reviewer's pending whole-paragraph deletion requires
758
- // explicit intent. The restored counterproposal becomes a separately tracked
759
- // sibling paragraph; a normal redline operation remains fail-closed.
760
- const restoration = await applyOperationToDocumentXml(
761
- documentXml,
762
- {
763
- type: 'restore',
764
- // restore targets default to the rejected view, where deleted text exists
765
- target: { paragraphId: '1A2B3C4D', exactText: 'Original deleted paragraph text.' },
766
- modified: 'Restored or adjusted paragraph text.'
767
- },
768
- 'Editor'
769
- );
770
-
771
- // A single restoration follows its deleted source paragraph. A range
772
- // restoration follows the complete deleted source block. Unchanged legacy
773
- // validation defects are retained as baseline issues; newly generated errors
774
- // fail closed before commit.
775
- // inspect/extract descriptors report their revisionView, and each fingerprint
776
- // is computed from the same view as exactText. Keep those fields together.
777
-
778
- // To insert run-level text at a location visible only in the rejected view,
779
- // provide explicit rejected-view intent and an exact anchor-relative offset.
780
- const deletedTextInsertion = await applyOperationToDocumentXml(
781
- documentXml,
782
- {
783
- type: 'insert',
784
- target: { paragraphId: '1A2B3C4D', revisionView: 'rejected' },
785
- anchor: { exactText: 'must pay', occurrence: 1, offset: 5 },
786
- modified: '[clarification] ',
787
- existingRevisions: 'slice-cross-author'
788
- },
789
- 'Editor'
790
- );
791
-
792
- // applyOperationToDocumentXml(...) returns a full w:document payload.
793
- zip.file('word/document.xml', opResult.documentXml);
794
-
795
- const fragmentResult = await applyRedlineToOxml(
796
- paragraphOoxml,
797
- 'Item text',
798
- '1. Item text',
799
- { generateRedlines: true, author: 'Editor' }
800
- );
801
- const normalized = extractReplacementNodesFromOoxml(fragmentResult.oxml);
802
-
803
- // If sourceType === 'package', merge extracted content/artifacts instead of
804
- // writing the raw pkg:package payload into word/document.xml.
805
- if (normalized.numberingXml) {
806
- await ensureNumberingArtifactsInZip(zip, normalized.numberingXml, {
807
- mergeNumberingXml: mergeNumberingXmlBySchemaOrder
808
- });
809
- }
810
-
811
- await validateDocxPackage(zip);
812
- const output = await zip.generateAsync({ type: 'nodebuffer' });
813
- ```
814
-
815
- ## Validating Output
816
-
817
- For the test-lane design and instructions for adding regression, synthetic
818
- Word, and real-corpus cases, see [docs/TESTING.md](./docs/TESTING.md).
819
-
820
- Run the automated package checks:
821
-
822
- ```bash
823
- npm test
824
- npm run test:isolation
825
- npm run check:types
826
- npm run lint
827
- npm run test:coverage
828
- ```
829
-
830
- For release-time fixture export:
831
-
832
- ```bash
833
- node scripts/export-validation-fixtures.mjs
834
- ```
835
-
836
- On Windows with desktop Word installed, you can smoke-test a completed `.docx`:
837
-
838
- ```bash
839
- npm run smoke:word -- path/to/file.docx
840
- ```
841
-
842
- To validate against Word as an independent oracle (Word itself accepts and
843
- rejects the generated revisions and the resulting text is compared to the
844
- expected outcomes):
845
-
846
- ```bash
847
- npm run test:word
848
- ```
849
-
850
- This Windows-only test command generates an English legal/administrative task
851
- suite under `tmp/word-validation/` and drives installed desktop Microsoft Word
852
- through COM. Its 33 cases include targeted reliability checks for literal
853
- content, multi-paragraph replacement, prior-revision no-op, atomic rollback,
854
- hostile revision IDs, bookmarks, internal hyperlinks, mixed formatted runs,
855
- content controls, table cells, structural tabs, locked complex fields,
856
- comments, footnotes/endnotes, headers/footers, and external hyperlinks.
857
- Structure-focused cases also assert required
858
- OOXML elements before Word independently checks Accept All and Reject All. The
859
- published library remains clean, host-independent JavaScript; Word automation
860
- exists only in development scripts.
861
-
862
- Use `npm run report:word:coverage` to print the validated task-by-structure
863
- matrix across all 33 synthetic and 31 SuperDoc scenarios. Before a release,
864
- `npm run review:word:prepare -- --cycle=0` creates a pending human-review
865
- manifest with changed cases, a rotating 20% synthetic sample, and legal plus
866
- administrative corpus representatives. See the [Word visual review guide in docs/TESTING.md](./docs/TESTING.md#microsoft-word-visual-review-guide);
867
- preparation and AI preflight never count as human sign-off.
868
-
869
- A nightly GitHub Actions workflow additionally validates generated fixtures
870
- against the ECMA-376 transitional schemas (`xmllint`), opens them with
871
- LibreOffice, and runs an extended fuzz sweep of the accept/reject round-trip
872
- invariant with a fresh seed. See [Release validation in docs/TESTING.md](./docs/TESTING.md#release-validation-and-independent-oracles).
873
-
874
- ## Architecture & Contributing
875
-
876
- - **[ARCHITECTURE.md](./ARCHITECTURE.md)**: Detailed module layout, end-to-end reconciliation flow, and contributor fast orientation.
877
- - **[AGENTS.md](./AGENTS.md)**: Fast-start routing and operational guardrails for AI coding agents.
878
- - **[docs/AGENT_FAST_START.md](./docs/AGENT_FAST_START.md)**: Minimal ordinary-edit contract for agent integrations.
879
- - **[docs/AGENT_KNOWLEDGE_BASE.md](./docs/AGENT_KNOWLEDGE_BASE.md)**: Full agent reference for APIs, operations, CLI automation, recovery, and gotchas.
880
- - **[docs/TESTING.md](./docs/TESTING.md)**: Comprehensive testing model, test lanes, independent oracle checks, and visual review checklist.
881
- - **[CHANGELOG.md](./CHANGELOG.md)**: Version history, migration guides, and deprecation schedules.
882
-
883
- ## Test Corpus Attribution
884
-
885
- Real-document reliability testing uses selected references from
886
- [docx-corpus](https://docxcorp.us/), built by
887
- [SuperDoc](https://superdoc.dev/). The dataset is licensed under the
888
- [Open Data Commons Attribution License (ODC-By) 1.0](https://opendatacommons.org/licenses/by/1-0/).
889
-
890
- Only explicitly pinned English legal and administrative documents are eligible
891
- for the initial corpus lane. References and provenance live in
892
- `tests/corpus/superdoc-english-legal-administrative.json`; downloaded documents
893
- are hash-verified and kept in ignored `tmp/` storage rather than committed. On
894
- Windows with desktop Word installed, run the reviewed 31-scenario/23-document lane with:
895
-
896
- ```bash
897
- npm run test:corpus:word
898
- ```
899
-
900
- ODC-By applies to the database; individual documents may carry additional
901
- 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
+ - Reject All verification: every tracked operation is refused unless Word's Reject All of the output reproduces the source
19
+ - Status/error result fields for parse, targeting, and existing-revision failures
20
+ - Localized exact replacements against inspected targets, plus optional fail-closed speculative CLI edits
21
+ - Package plumbing helpers for numbering.xml, comments.xml, content types, and relationships
22
+ - Zero host dependencies: works in Node.js, browsers, Deno, and similar JS runtimes with DOM parsing support
23
+
24
+ ## What's New in 0.9.0 and 0.8.x
25
+
26
+ 0.9.0 moves the CLI contract to version 9 and adds the `list-format` operation (`convert-plain`, `change-format`, `join`),
27
+ `paragraph-format` numbering binding, list-conversion diagnostics, a single numbering allocator and merge, and a Reject All
28
+ revision fidelity verifier that refuses by default (`REVISION_FIDELITY_VIOLATION`). See the
29
+ [0.9.0 release notes](./docs/releases/0.9.0.md) and the [CHANGELOG](./CHANGELOG.md). The CLI contract was version 8
30
+ throughout the 0.8 line.
31
+
32
+ - **0.8.0, universal document facade:** `openDocx` and `DocxDocument` are exported from the primary entry point and work
33
+ on `Uint8Array` input in any JavaScript runtime. ZIP handling uses `fflate` and SHA-256 is pure JavaScript, so there are no
34
+ Node built-in dependencies. The XML provider falls back to `@xmldom/xmldom` automatically, and the
35
+ `@ansonlai/docx-redline-js/bundle` export provides zero-dependency single-file bundles for sandboxes such as n8n Code
36
+ nodes. The `/node` entry point keeps its existing API. See the [0.8.0 release notes](./docs/releases/0.8.0.md).
37
+ - **0.8.1, headers, footers and comment threads:** an operation's `part` field edits headers and footers;
38
+ `inspect().headersFooters` lists them. Comment threads can be resolved or reopened with `doc.resolveComment` or the
39
+ `comment_resolve` operation, and comments can be deleted by id with `doc.deleteComments({ ids })` or
40
+ `docx-redline delete-comments --comment-id`. See the [0.8.1 release notes](./docs/releases/0.8.1.md).
41
+ - **0.8.2, fixes:** a replacement beside a hyperlink no longer pulls adjacent plain text into the link, and Reject All no
42
+ longer reorders text around a manual line break. See the [0.8.2 release notes](./docs/releases/0.8.2.md).
43
+ - **0.8.3, list fixes:** Reject All now restores list edits exactly (no extra empty paragraph after an inserted
44
+ paragraph, no merged source paragraphs after a list-range replacement, original list numbering kept through `openDocx`),
45
+ and inspection no longer reports tracked historical list, style or heading properties as current. See the
46
+ [0.8.3 release notes](./docs/releases/0.8.3.md).
47
+ - **0.8.4, fixes:** localized replacements and list conversion work in paragraphs with soft line breaks, generated
48
+ bullets get their own numbering IDs, `format` accepts `textOccurrence` to format a later match, and tracked tables use
49
+ Word's row-level markup, so Word's Reject All removes them and an appended table keeps the paragraph's formatting. See
50
+ the [0.8.4 release notes](./docs/releases/0.8.4.md).
51
+
52
+ Version 0.7.0 introduced contract-8 localized edits: a small `find`/`replace` request against an inspected target instead
53
+ of a complete paragraph, plus an optional speculative form that fails without writing when the anchor is missing or
54
+ ambiguous. They remain available unchanged; see the [0.7.0 release notes](./docs/releases/0.7.0.md).
55
+
56
+ ## Documentation Index
57
+
58
+ | Document | Description |
59
+ |---|---|
60
+ | **[README.md](./README.md)** | Library overview, installation, quick start, and public API reference |
61
+ | **[docs/AGENT_FAST_START.md](./docs/AGENT_FAST_START.md)** | Compact ordinary-edit protocol for structured tools and shell-only agents |
62
+ | **[AGENTS.md](./AGENTS.md)** | Short repository launch card for task routing and contributor verification |
63
+ | **[docs/AGENT_KNOWLEDGE_BASE.md](./docs/AGENT_KNOWLEDGE_BASE.md)** | Full agent reference, CLI workflow, operation examples, error recovery, options, and gotchas |
64
+ | **[ARCHITECTURE.md](./ARCHITECTURE.md)** | Contributor architecture, module responsibilities, end-to-end data flow, and contracts |
65
+ | **[docs/TESTING.md](./docs/TESTING.md)** | Complete testing guide, test lanes, independent oracle validation, and Word visual review checklist |
66
+ | **[CHANGELOG.md](./CHANGELOG.md)** | Release history, breaking changes, and migration notes |
67
+
68
+ ## Repository Layout
69
+
70
+ The package exposes three levels of API:
71
+
72
+ | Level | Entry point | Use it for |
73
+ |---|---|---|
74
+ | Host-independent OOXML API | `index.js` | Paragraph/range transforms and exported OOXML utilities in browsers, Node.js, or another DOM-capable runtime |
75
+ | Standalone document XML runner | `services/standalone-operation-runner.js` | Applying operations to a complete `word/document.xml` string |
76
+ | Node/DOCX API and CLI | `node/index.js`, `bin/docx-redline.js` | Reading, changing, validating, and writing complete `.docx` ZIP packages |
77
+
78
+ Implementation folders have distinct roles: `core/` holds shared OOXML and
79
+ targeting primitives; `pipeline/` handles ingestion, diffing, markdown, lists,
80
+ and serialization; `engine/` performs reconciliation; `services/` coordinates
81
+ document operations and package artifacts; `node/` contains Node-only ZIP and
82
+ whole-document code. Tests are directly runnable `tests/*.mjs` files, while
83
+ `tests/helpers/` and `tests/fixtures/` contain support code and data.
84
+
85
+ Contributors and coding agents should start with the routing table in
86
+ [AGENTS.md](./AGENTS.md#pick-the-route) before exploring the tree. The full
87
+ dependency and ownership map is in [ARCHITECTURE.md](./ARCHITECTURE.md).
88
+
89
+ For document-operation JSON, choose operations by the desired output structure,
90
+ not by the everyday meaning of the type name. `redline` and `replace` provide
91
+ ordinary text replacement; `list-change` and `table-reconciliation` provide
92
+ structural intent; and ordinary `insert` is a compatibility alias of the
93
+ redline path unless it includes a rejected-view target and anchor. In every
94
+ ordinary text-bearing operation, `modified` is the complete desired content for
95
+ the target. See the [operation model](./docs/AGENT_KNOWLEDGE_BASE.md#operation-model-choose-by-output-shape)
96
+ and the [JSON schema](./docs/schemas/document-operations.schema.json).
97
+
98
+ ## Install
99
+
100
+ ### npm / Node.js
101
+
102
+ ```bash
103
+ npm install @ansonlai/docx-redline-js
104
+ ```
105
+
106
+ ### CDN (browser `<script type="module">`)
107
+
108
+ ```html
109
+ <script type="module">
110
+ import { applyRedlineToOxml } from 'https://esm.sh/@ansonlai/docx-redline-js';
111
+ </script>
112
+ ```
113
+
114
+ Or use the pre-bundled file (no import map needed, `diff-match-patch` is inlined):
115
+
116
+ ```html
117
+ <script type="module">
118
+ import { applyRedlineToOxml } from 'https://cdn.jsdelivr.net/npm/@ansonlai/docx-redline-js/dist/docx-redline-js.esm.min.js';
119
+ </script>
120
+ ```
121
+
122
+ ### Local git clone
123
+
124
+ ```bash
125
+ git clone https://github.com/AnsonLai/docx-redline-js.git
126
+ ```
127
+
128
+ ```js
129
+ import { applyRedlineToOxml } from './docx-redline-js/index.js';
130
+ ```
131
+
132
+ ## Quick Start
133
+
134
+ ### Node.js
135
+
136
+ ```js
137
+ import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
138
+ import {
139
+ configureXmlProvider,
140
+ setDefaultAuthor,
141
+ applyRedlineToOxml
142
+ } from '@ansonlai/docx-redline-js';
143
+
144
+ configureXmlProvider({ DOMParser, XMLSerializer });
145
+ setDefaultAuthor('My App');
146
+
147
+ const result = await applyRedlineToOxml(
148
+ paragraphOoxml,
149
+ 'Original sentence.',
150
+ 'Updated sentence.',
151
+ { generateRedlines: true, author: 'Editor' }
152
+ );
153
+
154
+ console.log(result.hasChanges);
155
+ console.log(result.oxml);
156
+ ```
157
+
158
+ ### Browser
159
+
160
+ ```js
161
+ import {
162
+ setDefaultAuthor,
163
+ applyRedlineToOxml
164
+ } from '@ansonlai/docx-redline-js';
165
+
166
+ setDefaultAuthor('Browser Editor');
167
+
168
+ const result = await applyRedlineToOxml(oxml, original, modified, {
169
+ generateRedlines: true
170
+ });
171
+ ```
172
+
173
+ ## API Reference
174
+
175
+ ### Agent-friendly inspection and complete DOCX editing
176
+
177
+ ```js
178
+ import { inspectDocumentParts } from '@ansonlai/docx-redline-js';
179
+ const inventory = inspectDocumentParts({ documentXml, commentsXml, numberingXml });
180
+ ```
181
+
182
+ Inspection returns exact paragraph text, target IDs/fingerprints, headings,
183
+ table/list context, revision authors, and joined comment anchors. Filters such
184
+ as `search`, `indexes`, `range`, `revisedOnly`, `inTable`, and `skipEmpty`
185
+ limit output. Search is a case-insensitive substring match; `around` adds nearby drafting
186
+ context, while `limit` and `after` page direct hits. When a search yields 0
187
+ matches in the active view, `selection.hint` provides guidance if matches exist
188
+ in the alternate revision view (such as searching for deleted text to restore).
189
+ Compact `extract --revised` output retains revision authors and marks paragraphs
190
+ whose content is wholly hidden in the selected view as `deleted` or `inserted`.
191
+ One such paragraph can be restored without restating it:
192
+ `docx-redline apply input.docx --restore --target-id ID --output reviewed.docx`.
193
+ Add one exact `--find`/`--replace` pair to make a localized correction while
194
+ restoring. The shortcut requires a strong rejected-view paragraph ID and does
195
+ not infer a range or adjacent content.
196
+ `revisionView` accepts `accepted`, `rejected`, or `current`. Returned
197
+ paragraphs identify matches versus context and include `humanReference` for
198
+ user-facing summaries.
199
+
200
+ For complete `.docx` buffers in Node:
201
+
202
+ ```js
203
+ import { openDocx } from '@ansonlai/docx-redline-js/node';
204
+ const document = openDocx(inputBuffer);
205
+ const result = await document.applyOperations(operations, {
206
+ author: 'Editor', atomic: true, validate: true
207
+ });
208
+ const outputBuffer = result.toBuffer();
209
+ ```
210
+
211
+ The Node facade performs edits, artifact merges, package wiring, validation,
212
+ and commit as one transaction. It defaults to strict targets and returns the
213
+ untouched input with `written: false` on atomic failure. It is isolated from
214
+ the root/browser dependency graph.
215
+
216
+ Install `@xmldom/xmldom` alongside the package when using the Node facade or
217
+ CLI; it remains an optional peer so browser consumers do not install a DOM shim.
218
+
219
+ #### Example agent session wrapper (development only)
220
+
221
+ [`examples/agent-session-wrapper.mjs`](./examples/agent-session-wrapper.mjs)
222
+ demonstrates how a custom agent harness can keep one `DocxDocument` open, return
223
+ short revision-bound target handles, apply safe defaults once, and translate a
224
+ narrow edit request into canonical document operations:
225
+
226
+ ```js
227
+ import { readFile } from 'node:fs/promises';
228
+ import { createExampleAgentSession } from './examples/agent-session-wrapper.mjs';
229
+
230
+ const session = createExampleAgentSession(await readFile('contract.docx'), {
231
+ profile: { author: 'Editor' }
232
+ });
233
+ const inspection = session.inspect({ search: 'termination', around: 2 });
234
+ const clause = inspection.targets.find(target => target.role === 'match');
235
+ const result = await session.applyEdits([{
236
+ target: clause.handle,
237
+ replacements: [{
238
+ find: 'The Company may terminate',
239
+ replace: 'Either party may terminate'
240
+ }]
241
+ }]);
242
+ ```
243
+
244
+ This file is a testable sample, not a package export or supported alternate
245
+ mutation engine. It delegates to `@ansonlai/docx-redline-js/node`, is excluded
246
+ from the published package files, and is intended to help MCP servers, Claude
247
+ skills, OpenCode tools, and other custom harnesses design thin integrations.
248
+ The sample binds each handle to the inspected package revision and view, returns
249
+ new handles after successful mutations, and expands localized exact replacements
250
+ into complete desired paragraph text before delegating to the canonical redline
251
+ operation. Duplicate matches require an explicit `occurrence`; missing,
252
+ ambiguous, overlapping, and conflicting patches fail before document mutation.
253
+ From a source checkout, run `npm run benchmark:agent` to compare its native
254
+ execution and serialized request size with a canonical stateless Node workflow.
255
+ The example and its benchmark are excluded from the published package. The
256
+ benchmark explicitly does not claim to measure LLM reasoning or provider/tool
257
+ latency. Checked comparative results are in the
258
+ [agent protocol rollout audit](./docs/validation-reports/2026-09-12-agent-protocol-rollout.md).
259
+
260
+ > **Pre-1.0 integration warning:** Agent skills, MCP servers, and harnesses
261
+ > should pin the exact `@ansonlai/docx-redline-js` release they were tested
262
+ > against and declare the CLI contract version/capabilities they require. Do
263
+ > not treat a newly installed release or contract as verified until the
264
+ > integration's compatibility tests pass. See the
265
+ > [skill-authoring contract](./docs/SKILL_AUTHORING.md#runtime-negotiation).
266
+
267
+ ### Agent CLI
268
+
269
+ ```bash
270
+ # Default agent workflow: inspect once, then apply once
271
+ docx-redline extract contract.docx --search "termination" --around 3
272
+ node emit-operations.mjs | docx-redline apply contract.docx --operations - --profile agent --compact --output reviewed.docx
273
+ ```
274
+
275
+ ```bash
276
+ # Inline one-liner edit (no operations file needed)
277
+ docx-redline apply contract.docx --target "Original clause text" --modified "New clause text" --output reviewed.docx
278
+
279
+ # Direct edit without tracked changes
280
+ docx-redline apply contract.docx --target "Typo fix" --modified "Fixed typo" --no-redlines --output clean.docx
281
+
282
+ # Cross-author edit inside another reviewer's pending insertion
283
+ docx-redline apply contract.docx --target "Another author's clause" --modified "Revised clause" --existing-revisions slice-cross-author --output reviewed.docx
284
+
285
+ # High-assurance atomic batch with nonzero exit on any incomplete result
286
+ docx-redline apply contract.docx --operations operations.json --atomic --require-complete --output reviewed.docx
287
+
288
+ # Optional read-only audit before a deliberately staged batch
289
+ docx-redline preflight contract.docx --operations operations.json --author "Editor"
290
+ ```
291
+
292
+ All commands emit JSON on stdout. `apply` defaults:
293
+ - **Author**: Defaults to `'AI Redliner'` (or `DOCX_REDLINE_AUTHOR` environment variable).
294
+ - **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.
295
+ - **Existing revisions**: Defaults to `'merge-same-author'`. Pass `--existing-revisions slice-cross-author` to edit inside another reviewer's pending insertions with native carrier slicing.
296
+ - **Transactionality**: Defaults to `atomic: false` (applies valid operations and reports any failures). Pass `--atomic` for all-or-nothing rollback on any operation error.
297
+ - **Complete-success exit**: Pass `--require-complete` when a progressive `partial` result must exit with code `3`; errors exit with code `2`. Without the flag, partial results retain the legacy zero exit code, so always inspect `completion`.
298
+ - **Agent profile**: `--profile agent` enables complete-success exit behavior while retaining the ordinary progressive transaction and existing-revision defaults. Compose it with `--atomic` or an explicit `--existing-revisions` policy when intended, and verify the resolved `effectiveOptions`.
299
+ - **Operations from stdin**: `--operations -` reads the same array or `{ operations, expectedRevision }` envelope accepted from a file. Feed it from a JSON serializer or structured process API, not shell-interpolated legal text.
300
+ - **Compact stdout**: `--compact` emits one-line mutation JSON. It changes serialization only, not operation semantics.
301
+ - **Tracked changes**: Defaults to `generateRedlines: true`. Pass `--no-redlines` when clean direct text edits are desired.
302
+ - **Inline edits**: Use `--target <text>` with `--modified <text>` or `--comment <text>` for quick one-liners without creating a JSON file.
303
+ - **Localized edits**: After extraction, use `replacements` with the inspected `paragraphId` plus `fingerprint` to avoid restating a long paragraph. Retain `exactText` locally for drafting. Targetless `--find`/`--replace` is an optional speculative path described below.
304
+ - **Invisible spaces**: Localized matching is case-sensitive and tries exact text first. If no exact span exists, ordinary spaces may match DOCX non-breaking spaces; ambiguous equivalent matches still fail closed, and `change.replacements[i].matchMode` reports `space_equivalent` when used.
305
+ - **Compact mutation results**: `apply`, `accept`, `reject`, and `delete-comments` omit full OOXML/package payloads and inspection text from stdout. CLI operation results omit duplicate nested receipt bodies; the ordered top-level `receipts` array is authoritative. Successful exact target-match diagnostics are omitted and equivalent-whitespace matches retain only mode/count. Node and standalone-runner results remain unchanged. Use `validate` when full issue arrays are needed.
306
+
307
+ Inspection commands default to 20 direct matches and a 48 KiB soft response
308
+ budget when no explicit positional scope is supplied. When a search returns 0
309
+ matches in the requested view, `selection.hint` provides immediate guidance if
310
+ matches exist in the alternate view (such as searching for a deleted clause to
311
+ restore). Use `--limit` with `--after <paragraph-index>` to continue, `--around N`
312
+ (aliases `--context` and `-C`) to include nearby paragraphs, and `--all` only
313
+ for deliberate unbounded inspection. Every retained target is complete; an
314
+ individually oversized paragraph is returned whole with an `oversizeItem`
315
+ marker. Machine `index` and `ref` fields are for targeting and pagination, not
316
+ user-facing Word locations; use `humanReference`, `provision`, or
317
+ `nearestHeading` in reports.
318
+
319
+ `docx-redline version` reports contract version 9 (adds the `list-format` operation type). Contract 9 adds the
320
+ capabilities `revision-fidelity-v1`, `list-conversion-diagnostics-v1`, `paragraph-numbering-binding-v1`,
321
+ `list-format-v1` (`convert-plain`) and `list-format-change-v1` (`change-format` and `join`), alongside the additive
322
+ `command-help-v1`, `inspection-context-v1`, `bounded-inspection-v1`,
323
+ `human-document-references-v1`, `batch-start-source-binding`,
324
+ `recovery-envelope-v1`, `require-complete-exit`, `operations-stdin`,
325
+ `agent-safety-profile-v2`, `deduplicated-cli-receipts`, and
326
+ `compact-cli-json-v1`, `localized-replacements-v1`,
327
+ `speculative-search-apply-v1`, `localized-change-summary-v1`, and
328
+ `restore-shortcuts-v1`, plus `localized-space-equivalence-v1` capabilities. Header/footer
329
+ `part` targeting, the `comment_resolve` operation and `delete-comments --comment-id` are advertised as
330
+ `header-footer-parts-v1`, `comment-resolve-operation-v1` and `delete-comments-by-id-v1`. The remaining advertised
331
+ capabilities are `atomic-batch-results-on-package-failure`, `baseline-aware-validation`, `compact-mutation-results`,
332
+ `cross-author-revision-slicing`, `document-scoped-list-revision-ids` and `format-text-occurrence-v1`.
333
+ Wrappers should negotiate only the
334
+ capabilities they use. Run `docx-redline <command> --help` for that command's
335
+ machine-readable options, behavior, exit codes, canonical GitHub documentation links, and compact examples.
336
+
337
+ #### Optional speculative CLI path
338
+
339
+ Harnesses with a measured confidence policy may attempt a targetless localized
340
+ edit. This is not the default agent workflow:
341
+
342
+ ```bash
343
+ # Globally unique literal
344
+ docx-redline apply contract.docx --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
345
+
346
+ # Optional directional scope using a longer, fairly unique nearby phrase
347
+ docx-redline apply contract.docx --search "distinctive nearby heading or phrase" --context-range 1:3 --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
348
+ ```
349
+
350
+ Generic or repeated anchors can widen the unioned scope until multiple
351
+ paragraphs contain `find`, causing a safe `AMBIGUOUS_TARGET` failure and a
352
+ recovery turn. `--around 3` is symmetric. On success,
353
+ `change.context.anchorMatchCount > 1` signals a repeated anchor; confirm the
354
+ returned location and bounded evidence. Matching remains case-sensitive and
355
+ exact-first, with a narrow ordinary-space/NBSP fallback that still refuses
356
+ ambiguity.
357
+
358
+ See the [compact agent fast start](./docs/AGENT_FAST_START.md), the
359
+ [skill/harness authoring contract](./docs/SKILL_AUTHORING.md), and the
360
+ [operation JSON Schema](docs/schemas/document-operations.schema.json).
361
+
362
+ ### Configuration (call once at startup)
363
+
364
+ | Function | Purpose |
365
+ |----------|---------|
366
+ | `configureXmlProvider({ DOMParser, XMLSerializer })` | Inject XML parser. Required in Node.js; browsers usually provide native support. |
367
+ | `configureLogger({ log, warn, error })` | Replace default console logger. |
368
+ | `setDefaultAuthor(name)` | Set fallback track-change author (default: `'AI Redliner'`, configurable via `DOCX_REDLINE_AUTHOR` environment variable). |
369
+ | `setPlatform(label)` | Set platform label for diagnostics (default: `'Unknown'`). |
370
+
371
+ ### Options and Defaults Reference
372
+
373
+ | Option | Type | Default | Description |
374
+ |--------|------|---------|-------------|
375
+ | `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. |
376
+ | `author` | `string` | `'AI Redliner'` | Reviewer/author name stamped on generated tracked changes and comments. Overridable via `DOCX_REDLINE_AUTHOR` env variable. |
377
+ | `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. |
378
+ | `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. |
379
+ | `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. |
380
+ | `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. |
381
+ | `existingRevisions` | `string` | `'merge-same-author'` | How to handle paragraphs with existing tracked changes. `'merge-same-author'` merges revisions from the same author and protects other authors with `EXISTING_REVISIONS`. `'slice-cross-author'` keeps that same-author merge behavior while allowing Word-native edits inside another author's pending insertion. Pass `'accept-all-first'` to normalize prior revisions or `'reject-input'` to refuse editing revised paragraphs. |
382
+ | `removeFormatting` | `boolean` | `false` | When `true` and the text is unchanged with no Markdown hints, strips existing bold/italic/underline/strikethrough formatting. |
383
+ | `verifyRevisionFidelity` | `'enforce' \| 'warn' \| 'off'` | `'enforce'` | Post-apply Reject All check described under [Reject All verification](#reject-all-verification). CLI: `--revision-fidelity`; the agent profile rejects `off`. |
384
+ | `listMarkers` | `'literal'` | unset | Per operation. Accepts list-marker text as literal text and suppresses `LIST_MARKER_NOT_BOUND`, `UNSUPPORTED_LIST_CONVERSION`, `LIST_FORMAT_NOT_APPLIED` and `UNSUPPORTED_LIST_FORMAT_CHANGE` for that operation. |
385
+ | `strictListConversion` | `boolean` | `atomic` | Makes `UNSUPPORTED_LIST_CONVERSION` and `UNSUPPORTED_LIST_FORMAT_CHANGE` errors instead of warnings (capability `list-conversion-diagnostics-v1`). The CLI agent profile and `--atomic` set it; an operation's `listMarkers: 'literal'` wins. |
386
+ | `sanitizeInput` | `boolean` | `false` | Opt-in removal of standalone leading assistant-preface lines. Literal dollar signs and `\n` sequences are always preserved. |
387
+
388
+ Same-author revision merging refuses paragraphs containing comment anchors with
389
+ `COMMENTED_CONTENT_MERGE`; resolve those comments first so the merge cannot
390
+ remove or orphan their anchors.
391
+
392
+ Common result fields:
393
+
394
+ | Field | Purpose |
395
+ |-------|---------|
396
+ | `status` | Operation status: `'ok'`, `'partial'`, `'no-op'`, or `'error'`. |
397
+ | `error` | Present on failure; retains a stable `code` and adds recovery envelope version, stage, category, bounded context, and a machine-readable recovery action. |
398
+ | `written` | CLI/facade boolean indicating whether the output file was successfully written to disk. |
399
+ | `completion` | CLI-only boolean that is `true` only when a destination was written, top-level status is neither error nor partial, and no operation result failed. |
400
+ | `rolledBack` | Present and `true` when an atomic batch encountered an error and rolled back all changes. |
401
+
402
+ Word diffs are deterministic by default (no wall-clock timeout). Inputs above
403
+ the safe ceiling of 262,144 unique diff tokens return `DIFF_TOKEN_LIMIT` with
404
+ the original OOXML unchanged so callers can split the operation without risking
405
+ silent text loss.
406
+
407
+ ### Editing inside existing revisions (cross-author slicing)
408
+
409
+ During multi-round legal negotiations, a reviewer often needs to edit text that was previously inserted by another reviewer whose revision is still pending. Pass `existingRevisions: 'slice-cross-author'` (or `--existing-revisions slice-cross-author` via CLI) to edit inside another author's pending insertion without erasing their attribution or requiring prior acceptance:
410
+
411
+ ```js
412
+ const result = await applyRedlineToOxml(paragraphOoxml, originalText, modifiedText, {
413
+ generateRedlines: true,
414
+ author: 'Reviewer B',
415
+ existingRevisions: 'slice-cross-author'
416
+ });
417
+ ```
418
+
419
+ The engine applies Microsoft Word Desktop-native tracked change structures:
420
+ - **Insertions inside pending insertions**: The carrier `<w:ins>` is split into sibling `<w:ins>` containers at the paragraph level (`[ins(Author A), ins(Author B), ins(Author A)]`), maintaining strict schema compliance without illegal `ins/ins` nesting.
421
+ - **Deletions inside pending insertions**: The new `<w:del>` is nested directly inside the carrier `<w:ins>` (valid under ECMA-376 Part 1 `CT_RunTrackChange`), ensuring that if Author A's insertion is rejected, Author B's dependent deletion is cleanly removed with it.
422
+ - **Straddle deletions**: Deletions spanning between baseline text and pending insertions cleanly partition across their respective container contexts without invalid coalescing.
423
+ - **Lifecycle parity**: Accepting or rejecting either reviewer independently produces identical results to Microsoft Word Desktop's native review pane.
424
+
425
+ ### Replacing a heading with a tracked list
426
+
427
+ The list route treats a one-paragraph heading expanded into multiple markdown
428
+ items as a structural block replacement. The deleted heading stays in its own
429
+ tracked paragraph, and every inserted item becomes a separate Word list
430
+ paragraph. Accepting the revisions produces only the list items; rejecting them
431
+ restores the original heading exactly.
432
+
433
+ ```js
434
+ const result = await applyRedlineToOxml(
435
+ headingParagraphOoxml,
436
+ 'A.\tPURPOSE',
437
+ '* Article A. Purpose and Interagency Alignment\n' +
438
+ '* Key Focus: Joint Street Outreach & Medical Triage',
439
+ { generateRedlines: true, author: 'Editor' }
440
+ );
441
+ ```
442
+
443
+ `w:numId w:val="0"` means numbering is explicitly suppressed; it is not a list
444
+ definition and is never reused for generated bullets. New list items receive a
445
+ positive numbering ID. Font family, size, language, and related script
446
+ properties are inherited from the source paragraph, while heading emphasis
447
+ (such as bold or underline) is not copied unless the replacement markdown asks
448
+ for it. For a complete `word/document.xml`, prefer
449
+ `applyOperationToDocumentXml(...)` so replacement nodes and numbering artifacts
450
+ are imported at the correct scope.
451
+
452
+ ### List operations (`list-format`)
453
+
454
+ Text edits never create or restyle list structure. Typed markers such as `1.` or `A.` stay literal text, and a
455
+ `list-change` that retypes markers keeps the old numbering (it warns `LIST_FORMAT_NOT_APPLIED`, or fails
456
+ `UNSUPPORTED_LIST_FORMAT_CHANGE` under the strict policy, with a `list-format` request in `recovery.suggestedOperation`).
457
+ Structure changes use the `list-format` operation. Every mode records each paragraph's previous `w:numPr` in a tracked
458
+ `w:pPrChange`, so Reject All restores the original labels exactly and Accept All shows the new ones. The shared numbering
459
+ definitions in `numbering.xml` are never edited in place: new definitions come from one allocator and are merged by one numbering
460
+ merge, and an id that already exists with different content fails with `NUMBERING_ID_CONFLICT`. `paragraph-format`
461
+ `properties.numbering` binds or removes a list on a paragraph the same way (tracked `w:pPrChange`). Unknown
462
+ `paragraph-format.properties` keys are rejected with `INVALID_OPERATION`. The CLI advertises `list-format-v1` (contract version 9) for
463
+ `convert-plain` and `list-format-change-v1` for `change-format` and `join`.
464
+
465
+ ```js
466
+ const doc = openDocx(bytes);
467
+ const result = await doc.applyOperations([
468
+ // Typed headers "A. Scope" .. "C. Fees" become a real A./B./C. list. Each marker is a tracked deletion, and body
469
+ // paragraphs in the range stay plain. A typed start value ("C.") is kept.
470
+ { type: 'list-format', target: { exactText: 'A. Scope' }, targetEnd: { exactText: 'C. Fees' },
471
+ list: { mode: 'convert-plain' } },
472
+ // Bullets become 1., 2., 3. A new definition copies the list's levels and indentation.
473
+ { type: 'list-format', target: { exactText: 'Photo identification' },
474
+ list: { mode: 'change-format', format: 'decimal' } },
475
+ // A later list continues an earlier one (1. 2. / 1. 2. becomes 1. 2. / 3. 4.).
476
+ { type: 'list-format', target: { exactText: 'Confidentiality' },
477
+ list: { mode: 'join', continueFrom: { exactText: 'Termination' } } }
478
+ ], { author: 'Editor' });
479
+ ```
480
+
481
+ A changed-text header (`1. Old Terms` -> `1. New Terms`) takes two operations on the same paragraph in one batch: a
482
+ `redline` of the text that keeps the typed marker exactly, and a `list-format` that converts the paragraph. The batch
483
+ compiler allows this pair and always runs the redline first, whatever the batch order. The list-format then strips the
484
+ unchanged marker. Accept All shows label `1.` with `New Terms`, and Reject All shows the literal `1. Old Terms`:
485
+
486
+ ```js
487
+ await doc.applyOperations([
488
+ { type: 'redline', target: { exactText: '1. Old Terms' }, replacements: [{ find: 'Old', replace: 'New' }] },
489
+ { type: 'list-format', target: { exactText: '1. Old Terms' }, list: { mode: 'convert-plain' } }
490
+ ], { author: 'Editor' });
491
+ ```
492
+
493
+ A redline in that pair that adds, removes or changes the marker is refused before anything is written, with
494
+ `LIST_FORMAT_COMPOSITION_CONFLICT` (`reason: 'marker-changed'`). So is a redline that spans paragraphs or leaves the text
495
+ unchanged. To give a paragraph without a typed marker a label, use `strip: 'none'` with `format` (and `start`). A
496
+ `change-format` or `join` with `scope: 'list'` (the default) also writes every other item of the list. In the same batch,
497
+ each of those items accepts only a single-paragraph `redline`; any other write to them fails with
498
+ `LIST_FORMAT_COMPOSITION_CONFLICT` (`reason: 'list-rebind'`).
499
+ Style-linked lists (heading numbering), mid-list splits, and markers inside hyperlinks or fields are refused with a
500
+ recovery envelope. They are not approximated. Removing a list binding is `paragraph-format` with `properties.numbering:
501
+ null`. A `list-format` `detach` mode is not available yet. See the
502
+ [knowledge base](docs/AGENT_KNOWLEDGE_BASE.md#operation-model-choose-by-output-shape) for every mode, field and refusal.
503
+
504
+ ### Planning large mixed-content insertions
505
+
506
+ For an attachment or schedule containing several kinds of content, use
507
+ `planStructuredReplacement(...)` before applying the edit. It decomposes the
508
+ Markdown into typed heading, paragraph, list, and table blocks, normalizes the
509
+ block boundaries, and returns one atomic operation with
510
+ `structuredContent: true`.
511
+
512
+ ```js
513
+ import { planStructuredReplacement } from '@ansonlai/docx-redline-js';
514
+
515
+ const plan = planStructuredReplacement(
516
+ { exactText: 'Date', index: 172 },
517
+ `# ATTACHMENT 4
518
+
519
+ Introductory paragraph.
520
+
521
+ | Agency | Contact |
522
+ | --- | --- |
523
+ | BCHD | Dr. Jenkins |
524
+
525
+ ## Protocol
526
+
527
+ 1. Joint clearance
528
+ 2. Rapid escalation`,
529
+ { author: 'Editor' }
530
+ );
531
+
532
+ if (!plan.valid || !plan.operation) {
533
+ throw new Error(plan.issues.map(issue => issue.message).join(' '));
534
+ }
535
+
536
+ const result = await document.applyOperations([plan.operation], {
537
+ author: 'Editor', atomic: true, validate: true
538
+ });
539
+ ```
540
+
541
+ Every Markdown table must include a separator row immediately below its header.
542
+ Missing separators, missing data rows, and inconsistent column counts are
543
+ reported as structured errors instead of being inserted as visible pipe text.
544
+ Use `#` through `#########` to request Word heading paragraphs; blank lines
545
+ separate paragraphs; adjacent list markers form a real Word list. Keep the
546
+ planned content in one operation so later blocks do not depend on an anchor that
547
+ an earlier block has already replaced.
548
+
549
+ ### Pipeline (lower-level access)
550
+
551
+ | Function | Purpose |
552
+ |----------|---------|
553
+ | `ReconciliationPipeline` | Direct pipeline access (ingest, diff, patch, serialize). |
554
+ | `ingestWordOoxmlToPlainText(oxml)` | Extract plain text from OOXML. |
555
+ | `ingestWordOoxmlToMarkdown(oxml)` | Convert OOXML to markdown. |
556
+ | `ingestWordOoxmlToPlainTextResult(oxml)` | Extract text as `{ text, status, error?, warnings? }`, distinguishing malformed input from an empty document. |
557
+ | `ingestWordOoxmlToMarkdownResult(oxml)` | Markdown counterpart to the result-returning plain-text helper. |
558
+ | `ingestOoxml(oxml)` | Flatten OOXML into an internal run model with offsets. |
559
+ | `preprocessMarkdown(text)` | Normalize markdown and extract format hints. |
560
+ | `analyzeStructuredContent(markdown)` | Decompose mixed Markdown into typed blocks and report malformed table syntax without creating an operation. |
561
+ | `planStructuredReplacement(target, markdown, options)` | Validate mixed content and return one atomic `structuredContent` replacement operation, or `operation: null` with issues. |
562
+ | `containsTrackedChanges(xmlDoc)` | Detect `w:ins`, `w:del`, move revisions, property changes, and paragraph-mark revision markup in a parsed OOXML document/fragment. |
563
+ | `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. |
564
+
565
+ ### Services
566
+
567
+ | Function | Purpose |
568
+ |----------|---------|
569
+ | `injectCommentsIntoOoxml(oxml, comments, options)` | Add comments anchored to text ranges. |
570
+ | `applyCommentReplyToParts(options)` | Build a threaded reply in `comments.xml` and `commentsExtended.xml` without adding a document-body anchor. |
571
+ | `acceptTrackedChangesInOoxml(oxml, { author?, allAuthors? })` | Accept `w:ins` / `w:del` / `w:moveFrom` / `w:moveTo` / `*PrChange` revisions for one author or all authors. |
572
+ | `rejectTrackedChangesInOoxml(oxml, { author?, allAuthors? })` | Reject `w:ins` / `w:del` / `w:moveFrom` / `w:moveTo` / `*PrChange` revisions for one author or all authors. |
573
+ | `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`. |
574
+ | `generateTableOoxml(headers, rows, options)` | Generate a `w:tbl` from tabular data. |
575
+ | `createDynamicNumberingIdState(numberingXml)` | Allocate numbering IDs without collisions. |
576
+ | `ensureNumberingArtifactsInZip(zip, numberingXml, options)` | Add numbering artifacts to a `.docx` package. Always merges into an existing `word/numbering.xml` through the package's single numbering merge (`mergeNumberingParts`); an incoming id that already exists with different content fails with `NUMBERING_ID_CONFLICT`. The former `mergeNumberingXml` option is ignored. |
577
+ | `ensureCommentsArtifactsInZip(zip, commentsXml)` | Merge comments artifacts into a `.docx` package. |
578
+ | `ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXml)` | Add or replace modern Word comment-thread metadata in a `.docx` package. |
579
+ | `validateDocxPackage(zip)` | Validate `.docx` structural consistency. |
580
+
581
+ Malformed OOXML never escapes these public transform APIs as a raw parser
582
+ exception. Transforms return `status: 'error'` with `error.code === 'PARSE_ERROR'`;
583
+ validators return a `PARSE_ERROR` issue. Recoverable XML parser
584
+ diagnostics are forwarded through the configured logger and included in
585
+ `warnings` where the result shape supports them.
586
+
587
+ Cross-author slicing also verifies its exact accepted-view text before success.
588
+ If a structural boundary prevents exact reconstruction, the transform returns
589
+ `status: 'error'` with `error.code === 'PATCH_ROUNDTRIP_MISMATCH'`,
590
+ `hasChanges: false`, and the original OOXML unchanged.
591
+ Pure insertion-only slicing uses an exact character-local diff so repeated words
592
+ cannot move an insertion to a different occurrence. Leading/trailing spaces,
593
+ tabs, and non-breaking spaces are treated as real changes rather than no-ops.
594
+ For text-bearing replacements, the exact accepted-view text of the resolved
595
+ paragraph—not a space-normalized caller target—defines mutation offsets. Word-level replacement
596
+ hunks that differ only by ordinary spaces and NBSPs are refined to character
597
+ edits so unchanged hyperlinks and their relationship attributes stay in place.
598
+ Runner results expose bounded `resolvedTarget.targetTextMatch` code-point
599
+ diagnostics when equivalent whitespace was used to identify the target.
600
+
601
+ ### Deep Imports
602
+
603
+ For advanced usage, import specific submodules:
604
+
605
+ ```js
606
+ import {
607
+ applyOperationToDocumentXml,
608
+ applyOperationsToDocumentXml,
609
+ preflightOperations,
610
+ orderOperationsForStableTargets
611
+ } from '@ansonlai/docx-redline-js/standalone-runner';
612
+ import { getParagraphText } from '@ansonlai/docx-redline-js/core/paragraph-targeting.js';
613
+ ```
614
+
615
+ 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`.
616
+
617
+ Before mutation, the runner resolves strong source descriptors against the
618
+ immutable batch-start document and binds them to session-local source
619
+ identities. Independent edits therefore do not need to be manually sorted when
620
+ an earlier structural rewrite changes later paragraph indexes or fingerprints.
621
+ Targets that deliberately refer to uniquely created paragraph text are compiled
622
+ into an internal capture dependency. True overlap is not guessed: incompatible
623
+ writes to one source fail before mutation with `OVERLAPPING_SOURCE_TARGETS`,
624
+ `REVISION_ORDER_CONFLICT` or `LIST_FORMAT_COMPOSITION_CONFLICT` (the one composed pair, a
625
+ marker-preserving `redline` plus a `list-format`, runs redline first), and mutating capture fan-out without distinct
626
+ selectors fails with `CAPTURE_FANOUT_CONFLICT`.
627
+
628
+ Threaded replies use a comment operation with no body target:
629
+
630
+ ```js
631
+ { type: 'comment_reply', parentCommentId: 8, commentContent: 'Agreed; I revised this.', author: 'Editor' }
632
+ ```
633
+
634
+ For complete `.docx` files, use `openDocx(...).applyOperations(...)`; it reads
635
+ the existing comments parts, writes the required `commentsExtended.xml`
636
+ relationship and content type, adds the reply's own range/reference markers to the body
637
+ (Word does not display a reply without them), and keeps `commentsIds.xml` and
638
+ `commentsExtensible.xml` in step when they exist. Inspection reports `paraId`, `parentCommentId` and `done`
639
+ so callers can discover and verify the thread hierarchy.
640
+
641
+ Resolve or reopen a thread with an operation (usable from the CLI through `apply --operations`) or the API:
642
+
643
+ ```js
644
+ { type: 'comment_resolve', commentId: 8 } // resolve
645
+ { type: 'comment_resolve', commentId: 8, resolved: false } // reopen
646
+ await doc.resolveComment(8, { resolved: true });
647
+ ```
648
+
649
+ Resolved is a thread-level state in Word, so any id in the thread updates the root and all replies. Delete
650
+ specific comments with `doc.deleteComments({ ids: [8] })` or `docx-redline delete-comments file.docx --comment-id 8`;
651
+ deleting a thread root also deletes its replies.
652
+
653
+ Headers and footers are edited by adding `part` to an operation: `{ type: 'replace', part: { kind: 'footer' }, target: { exactText: 'Draft v1' }, modified: 'Draft v2' }`.
654
+ `openDocx(...).inspect().headersFooters` lists the parts (path, kind, type, sections, text, whether they contain
655
+ fields). Fields such as `PAGE` are treated as atomic, comments are not allowed in headers or footers, and `accept`/`reject`
656
+ also resolve revisions inside them. Creating a header or footer that does not exist is not supported; see
657
+ [Known limitations](docs/plans/KNOWN_BUGS.md#4-headers-and-footers).
658
+
659
+ A whole-paragraph `delete` that targets existing comment markup fails with
660
+ `COMMENTED_CONTENT_DELETE`. In the Node facade and CLI, the error also includes
661
+ the affected comment author and text. Resolve the feedback or explicitly remove
662
+ the comment first; the library will not silently discard reviewer context.
663
+
664
+ The batch runner keeps one live document DOM and performs one final full-document
665
+ serialization. Accuracy remains the controlling constraint: each operation has
666
+ an internal savepoint so an error or no-op cannot leak a partial edit or consumed
667
+ revision ID into later operations.
668
+
669
+ Batches are progressive by default (`atomic: false`): valid operations commit
670
+ while failed operations remain unapplied and are reported in `results`. Pass
671
+ `{ atomic: true }` when any operation error must return the original
672
+ `documentXml`, `hasChanges: false`, empty package artifacts, and
673
+ `rolledBack: true`. The default `continueOnError: true` still attempts the full
674
+ batch so `results` describes every operation; use `{ continueOnError: false }`
675
+ to stop after the first error.
676
+
677
+ Every failed or partial mutation includes `retryPlan`. Its `base` is `original`
678
+ after rollback/no commit and `output` after a progressive partial commit;
679
+ `committedIndexes`, `failedIndexes`, and `unattemptedIndexes` identify the safe
680
+ replay scope. Errors use recovery envelope version 1 and always report
681
+ `sameArgumentsSafe: false`. Follow `error.recovery.action`; do not infer a retry
682
+ from prose. Authorization-sensitive actions, such as resolving comments, are
683
+ marked with `requiresUserAuthorization: true`. For `EXISTING_REVISIONS`, the
684
+ non-normalizing surgical recommendation is `slice-cross-author`; accepting or
685
+ rejecting prior revisions still requires explicit authority.
686
+
687
+ Comment anchors use exact matching first, then a unique ASCII-space/NBSP
688
+ equivalent match that preserves source offsets and text. Missing anchors return
689
+ `ANCHOR_NOT_FOUND`; repeated matches return `AMBIGUOUS_ANCHOR`. Both are
690
+ operation errors and therefore roll back atomic batches. When `textToComment`
691
+ is omitted, the exact text of the resolved paragraph is used.
692
+
693
+ Operations may override the batch author and may use a strict target descriptor:
694
+
695
+ ```js
696
+ const operations = [{
697
+ type: 'replace',
698
+ author: 'Contract Editor',
699
+ target: {
700
+ exactText: 'Either party may terminate on notice.',
701
+ paragraphId: '1A2B3C4D',
702
+ index: 12,
703
+ fingerprint: 'fnv1a32:...'
704
+ },
705
+ modified: 'Either party may terminate on 30 days written notice.'
706
+ }];
707
+
708
+ const preflight = preflightOperations(documentXml, operations, 'Fallback Author');
709
+ if (!preflight.valid) {
710
+ // Resolve missing/ambiguous targets, anchors, revision policies, or conflicts.
711
+ }
712
+
713
+ const result = await applyOperationsToDocumentXml(
714
+ documentXml,
715
+ operations,
716
+ 'Fallback Author',
717
+ null,
718
+ { strictTargets: true }
719
+ );
720
+ ```
721
+
722
+ Preflight is read-only and uses strict targeting by default. It reports
723
+ `AMBIGUOUS_TARGET` with candidates instead of selecting the first duplicate,
724
+ does not use fuzzy fallback, checks comment/highlight anchors and existing
725
+ revision policy, identifies same-paragraph operation conflicts, and reports
726
+ authors plus required comments/numbering artifacts.
727
+
728
+ Application currently defaults to permissive targeting for backward compatibility,
729
+ but will default to `strictTargets: true` in v1.0.0. When permissive resolution
730
+ chooses among multiple candidate paragraphs heuristically, it emits an
731
+ `AMBIGUOUS_TARGET_HEURISTIC_USED` warning containing candidate count and migration
732
+ guidance. Callers should pass `{ strictTargets: true }` and use strict descriptors
733
+ (`paragraphId`, `index`, `occurrence`, or `fingerprint`) to prepare for v1.0.0.
734
+
735
+ ### Reject All verification
736
+
737
+ A tracked edit only adds revisions, so rejecting every revision in the output
738
+ must give back the source document. After each operation the library projects
739
+ Word's Reject All and Accept All of the output (`core/word-revision-semantics.js`,
740
+ validated against recorded desktop Word results) and checks these invariants:
741
+
742
+ - `REJECT_PARAGRAPH_COUNT`, `REJECT_PARAGRAPH_TEXT` (including tabs and breaks),
743
+ `REJECT_PARAGRAPH_PROPS`, `REJECT_NUMBERING_REFS` (list membership `numId`/`ilvl`)
744
+ and `REJECT_WRAPPERS` (hyperlink, field and content-control membership):
745
+ Reject All of the output equals Reject All of the source (under accept-first
746
+ `existingRevisions` policies the target range is compared with the source's
747
+ accepted view). `REJECT_NUMBERING_REFS` also refuses a `numId` that the output
748
+ binds, or Reject All restores (including inside `w:pPrChange`), when the source
749
+ did not already use it and no `w:num` -> `w:abstractNum` -> `w:lvl` in the
750
+ available numbering defines it.
751
+ - `ACCEPT_INTENT`: a tracked plain-text `redline` with complete `modified` (no
752
+ Markdown, list-marker or table syntax; a single paragraph or a `targetEnd`
753
+ range) is refused when Accept All of its target would not read as `modified`.
754
+ - `ACCEPT_NEIGHBORS`: an untracked edit (`generateRedlines: false`) may not
755
+ change a paragraph outside its target in the Accept All or Reject All view.
756
+ - `UNTRACKED_CREATED_REVISION`: an untracked edit may not create a tracked
757
+ revision that was not in the source (fragments of a sliced source revision
758
+ keep their element, author and date and are allowed).
759
+
760
+ A localized replacement is also checked before it is committed: the target's
761
+ accepted text must equal the compiled replacement text, otherwise the operation
762
+ is refused with `PATCH_ROUNDTRIP_MISMATCH` rather than dropping content.
763
+
764
+ By default a violation is refused with `REVISION_FIDELITY_VIOLATION`; the
765
+ document is unchanged, `status` is `error`, and the receipt is `refused`. The
766
+ error payload contains:
767
+
768
+ - `violations[]`, each with `invariant` (`REJECT_PARAGRAPH_COUNT`,
769
+ `REJECT_PARAGRAPH_TEXT`, `REJECT_PARAGRAPH_PROPS`, `REJECT_NUMBERING_REFS`,
770
+ `REJECT_WRAPPERS`, `ACCEPT_INTENT`, `ACCEPT_NEIGHBORS` or
771
+ `UNTRACKED_CREATED_REVISION`), a plain-language `explanation`,
772
+ `expected`/`actual` excerpts, and `source.sourceParagraphIndex` (the 1-based
773
+ paragraph index from extraction) with a text excerpt;
774
+ - `recovery.retryGuidance`, one retry instruction per reported invariant, for
775
+ example use `replacements` for a literal edit, keep breaks and hyperlink
776
+ boundaries, or move formatting and list changes into their own operation.
777
+
778
+ Control it with the `verifyRevisionFidelity` option (`'enforce'` default,
779
+ `'warn'`, `'off'`) or `--revision-fidelity enforce|warn|off` on the CLI. `warn`
780
+ applies the edit and appends the same text to `warnings`. The `--profile agent`
781
+ profile rejects `off`. A narrower retry that is also refused indicates an
782
+ unsupported shape; report it rather than disabling verification. Recovery
783
+ details are in the `REVISION_FIDELITY_VIOLATION` row of
784
+ [docs/AGENT_KNOWLEDGE_BASE.md](./docs/AGENT_KNOWLEDGE_BASE.md).
785
+
786
+ ### Mutation Receipts
787
+
788
+ Both single-operation (`applyOperationToDocumentXml`) and batch
789
+ (`applyOperationsToDocumentXml`) results expose commit-aware **Mutation Receipts**
790
+ (`result.receipt` on single results and per-item `results[i].receipt`, plus `result.receipts`
791
+ for the full batch).
792
+
793
+ This full receipt shape is retained by the Node facade and standalone runner.
794
+ Only compact CLI JSON removes duplicate `results[i].receipt` bodies and keeps
795
+ the ordered top-level `receipts` array.
796
+
797
+ ```js
798
+ const result = await applyOperationsToDocumentXml(documentXml, operations, 'Agent');
799
+ for (const receipt of result.receipts) {
800
+ console.log(receipt.operationIndex, receipt.finalDisposition, receipt.committed);
801
+ console.log('Revisions:', receipt.revisionItems);
802
+ console.log('Comments:', receipt.commentIds);
803
+ }
804
+ ```
805
+
806
+ Receipts report:
807
+ - `operationIndex` (1-based), `operationId`, and `authorUsed`
808
+ - `attemptedDisposition` and `finalDisposition` (`applied`, `refused`, `no_change`, `rolled_back`, or `not_attempted`)
809
+ - `committed` (boolean: verified committed into serialized package output)
810
+ - `revisionItems` (exact allocated revision IDs with kind and target part)
811
+ - `commentIds`, `numberingIds`, and `relationshipIds`
812
+ - `affectedTargets` (resolved target coordinates) and `warnings`
813
+
814
+ Before completing an operation or batch transaction, `reconcileReceiptsAgainstOutput`
815
+ verifies every reported committed durable ID against a fresh parse of the output OOXML.
816
+ Any discrepancy fails closed and triggers immediate rollback.
817
+
818
+ ### Output Shape Matrix
819
+
820
+ Different APIs return different OOXML shapes. Use this as a packaging safety check.
821
+
822
+ | API | Typical input scope | Output field | Possible root/output shape | Safe to write directly into `word/document.xml` |
823
+ |-----|----------------------|--------------|----------------------------|--------------------------------------------------|
824
+ | `applyRedlineToOxml(...)` | Paragraph, range, or table-scope OOXML | `result.oxml` | Fragment, `<w:document>`, or package payload (`<pkg:package>`) | No. Inspect first. |
825
+ | `applyRedlineToOxmlWithListFallback(...)` | Paragraph or range-scope OOXML | `result.oxml` | Fragment, `<w:document>`, or package payload (`<pkg:package>`) | No. Inspect first. |
826
+ | `reconcileMarkdownTableOoxml(...)` | Table or paragraph-scope OOXML | `result.oxml` | Same shapes as `applyRedlineToOxml(...)` for the supplied scope | No. Inspect first. |
827
+ | `applyOperationToDocumentXml(...)` | Full `word/document.xml` string | `result.documentXml` | `<w:document>` | Yes. This is the document-safe helper. |
828
+ | `applyOperationsToDocumentXml(...)` | Full `word/document.xml` plus an operation batch | `result.documentXml` | `<w:document>` | Yes. Atomic by default; comments are applied before text-changing operations. |
829
+ | `extractReplacementNodesFromOoxml(...)` | Any OOXML payload | `{ replacementNodes, numberingXml, sourceType }` | Normalized to `fragment`, `document`, or `package` | Yes. Use this when consuming `result.oxml`. |
830
+
831
+ ### Do / Don't for Packaging
832
+
833
+ - Do use `applyOperationToDocumentXml(...).documentXml` when your intent is to replace `word/document.xml`.
834
+ - Do use `applyOperationsToDocumentXml(...)` rather than an unsorted loop for batches containing comments and replacements that target the same original paragraph.
835
+ - 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.
836
+ - 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.
837
+ - Treat `w:numId w:val="0"` as numbering suppression, never as a reusable list
838
+ ID. Generated bullet and numbered paragraphs must reference a positive ID
839
+ whose definition is merged into `word/numbering.xml`.
840
+ - Do use `extractReplacementNodesFromOoxml(...)` when you are consuming `result.oxml` from paragraph/range/table APIs.
841
+ - Do merge numbering/comments artifacts with `ensureNumberingArtifactsInZip(...)` and `ensureCommentsArtifactsInZip(...)` when those parts are present. Numbering is always merged, never replaced; pass `numberingXml` or a shared `numberingIdState` to every call so generated ids cannot collide.
842
+ - Don't write payloads that start with `<pkg:package` directly into `word/document.xml`.
843
+ - Don't assume every `result.oxml` payload is a raw paragraph fragment.
844
+
845
+ ## Working With `.docx` Files
846
+
847
+ This package operates on OOXML strings (XML parts inside `.docx` zip archives), not raw `.docx` binaries.
848
+
849
+ Typical flow:
850
+
851
+ 1. Extract the `.docx` zip (for example with JSZip, fflate, or similar)
852
+ 2. Read `word/document.xml`
853
+ 3. Apply reconciliation APIs to XML strings
854
+ 4. Merge numbering/comments artifacts when needed
855
+ 5. Write the archive back to a `.docx` file
856
+
857
+ ```js
858
+ import JSZip from 'jszip';
859
+ import {
860
+ applyRedlineToOxml,
861
+ extractReplacementNodesFromOoxml,
862
+ ensureNumberingArtifactsInZip,
863
+ validateDocxPackage
864
+ } from '@ansonlai/docx-redline-js';
865
+ import { applyOperationToDocumentXml } from '@ansonlai/docx-redline-js/standalone-runner';
866
+
867
+ const zip = await JSZip.loadAsync(docxBuffer);
868
+ const documentXml = await zip.file('word/document.xml').async('string');
869
+
870
+ const opResult = await applyOperationToDocumentXml(
871
+ documentXml,
872
+ { type: 'redline', target: 'old text', modified: 'new text' },
873
+ 'Editor'
874
+ );
875
+
876
+ // Restoring another reviewer's pending whole-paragraph deletion requires
877
+ // explicit intent. The restored counterproposal becomes a separately tracked
878
+ // sibling paragraph; a normal redline operation remains fail-closed.
879
+ const restoration = await applyOperationToDocumentXml(
880
+ documentXml,
881
+ {
882
+ type: 'restore',
883
+ // restore targets default to the rejected view, where deleted text exists
884
+ target: { paragraphId: '1A2B3C4D', exactText: 'Original deleted paragraph text.' },
885
+ modified: 'Restored or adjusted paragraph text.'
886
+ },
887
+ 'Editor'
888
+ );
889
+
890
+ // A single restoration follows its deleted source paragraph. A range
891
+ // restoration follows the complete deleted source block. Unchanged legacy
892
+ // validation defects are retained as baseline issues; newly generated errors
893
+ // fail closed before commit.
894
+ // inspect/extract descriptors report their revisionView, and each fingerprint
895
+ // is computed from the same view as exactText. Keep those fields together.
896
+
897
+ // To insert run-level text at a location visible only in the rejected view,
898
+ // provide explicit rejected-view intent and an exact anchor-relative offset.
899
+ const deletedTextInsertion = await applyOperationToDocumentXml(
900
+ documentXml,
901
+ {
902
+ type: 'insert',
903
+ target: { paragraphId: '1A2B3C4D', revisionView: 'rejected' },
904
+ anchor: { exactText: 'must pay', occurrence: 1, offset: 5 },
905
+ modified: '[clarification] ',
906
+ existingRevisions: 'slice-cross-author'
907
+ },
908
+ 'Editor'
909
+ );
910
+
911
+ // applyOperationToDocumentXml(...) returns a full w:document payload.
912
+ zip.file('word/document.xml', opResult.documentXml);
913
+
914
+ const fragmentResult = await applyRedlineToOxml(
915
+ paragraphOoxml,
916
+ 'Item text',
917
+ '1. Item text',
918
+ { generateRedlines: true, author: 'Editor' }
919
+ );
920
+ const normalized = extractReplacementNodesFromOoxml(fragmentResult.oxml);
921
+
922
+ // If sourceType === 'package', merge extracted content/artifacts instead of
923
+ // writing the raw pkg:package payload into word/document.xml.
924
+ if (normalized.numberingXml) {
925
+ await ensureNumberingArtifactsInZip(zip, normalized.numberingXml);
926
+ }
927
+
928
+ await validateDocxPackage(zip);
929
+ const output = await zip.generateAsync({ type: 'nodebuffer' });
930
+ ```
931
+
932
+ ## Validating Output
933
+
934
+ For the test-lane design and instructions for adding regression, synthetic
935
+ Word, and real-corpus cases, see [docs/TESTING.md](./docs/TESTING.md).
936
+
937
+ Run the automated package checks:
938
+
939
+ ```bash
940
+ npm test
941
+ npm run test:isolation
942
+ npm run check:types
943
+ npm run lint
944
+ npm run test:coverage
945
+ ```
946
+
947
+ For release-time fixture export:
948
+
949
+ ```bash
950
+ node scripts/export-validation-fixtures.mjs
951
+ ```
952
+
953
+ On Windows with desktop Word installed, you can smoke-test a completed `.docx`:
954
+
955
+ ```bash
956
+ npm run smoke:word -- path/to/file.docx
957
+ ```
958
+
959
+ To validate against Word as an independent oracle (Word itself accepts and
960
+ rejects the generated revisions and the resulting text is compared to the
961
+ expected outcomes):
962
+
963
+ ```bash
964
+ npm run test:word
965
+ ```
966
+
967
+ This Windows-only test command generates an English legal/administrative task
968
+ suite under `tmp/word-validation/` and drives installed desktop Microsoft Word
969
+ through COM. Its 33 cases include targeted reliability checks for literal
970
+ content, multi-paragraph replacement, prior-revision no-op, atomic rollback,
971
+ hostile revision IDs, bookmarks, internal hyperlinks, mixed formatted runs,
972
+ content controls, table cells, structural tabs, locked complex fields,
973
+ comments, footnotes/endnotes, headers/footers, and external hyperlinks.
974
+ Structure-focused cases also assert required
975
+ OOXML elements before Word independently checks Accept All and Reject All. The
976
+ published library remains clean, host-independent JavaScript; Word automation
977
+ exists only in development scripts.
978
+
979
+ Use `npm run report:word:coverage` to print the validated task-by-structure
980
+ matrix across all 33 synthetic and 31 SuperDoc scenarios. Before a release,
981
+ `npm run review:word:prepare -- --cycle=0` creates a pending human-review
982
+ manifest with changed cases, a rotating 20% synthetic sample, and legal plus
983
+ administrative corpus representatives. See the [Word visual review guide in docs/TESTING.md](./docs/TESTING.md#microsoft-word-visual-review-guide);
984
+ preparation and AI preflight never count as human sign-off.
985
+
986
+ A nightly GitHub Actions workflow additionally validates generated fixtures
987
+ against the ECMA-376 transitional schemas (`xmllint`), opens them with
988
+ LibreOffice, and runs an extended fuzz sweep of the accept/reject round-trip
989
+ invariant with a fresh seed. See [Release validation in docs/TESTING.md](./docs/TESTING.md#release-validation-and-independent-oracles).
990
+
991
+ ## Architecture & Contributing
992
+
993
+ - **[ARCHITECTURE.md](./ARCHITECTURE.md)**: Detailed module layout, end-to-end reconciliation flow, and contributor fast orientation.
994
+ - **[AGENTS.md](./AGENTS.md)**: Fast-start routing and operational guardrails for AI coding agents.
995
+ - **[docs/AGENT_FAST_START.md](./docs/AGENT_FAST_START.md)**: Minimal ordinary-edit contract for agent integrations.
996
+ - **[docs/AGENT_KNOWLEDGE_BASE.md](./docs/AGENT_KNOWLEDGE_BASE.md)**: Full agent reference for APIs, operations, CLI automation, recovery, and gotchas.
997
+ - **[docs/TESTING.md](./docs/TESTING.md)**: Comprehensive testing model, test lanes, independent oracle checks, and visual review checklist.
998
+ - **[CHANGELOG.md](./CHANGELOG.md)**: Version history, migration guides, and deprecation schedules.
999
+
1000
+ ## Test Corpus Attribution
1001
+
1002
+ Real-document reliability testing uses selected references from
1003
+ [docx-corpus](https://docxcorp.us/), built by
1004
+ [SuperDoc](https://superdoc.dev/). The dataset is licensed under the
1005
+ [Open Data Commons Attribution License (ODC-By) 1.0](https://opendatacommons.org/licenses/by/1-0/).
1006
+
1007
+ Only explicitly pinned English legal and administrative documents are eligible
1008
+ for the initial corpus lane. References and provenance live in
1009
+ `tests/corpus/superdoc-english-legal-administrative.json`; downloaded documents
1010
+ are hash-verified and kept in ignored `tmp/` storage rather than committed. On
1011
+ Windows with desktop Word installed, run the reviewed 31-scenario/23-document lane with:
1012
+
1013
+ ```bash
1014
+ npm run test:corpus:word
1015
+ ```
1016
+
1017
+ ODC-By applies to the database; individual documents may carry additional
1018
+ rights, so each selected document must be reviewed before becoming a test case.