@ansonlai/docx-redline-js 0.8.4 → 0.10.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 (72) hide show
  1. package/ARCHITECTURE.md +679 -460
  2. package/CHANGELOG.md +208 -1
  3. package/README.md +152 -16
  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/core/word-xml.js +160 -118
  9. package/dist/docx-redline-js.esm.js +4338 -796
  10. package/dist/docx-redline-js.esm.js.map +4 -4
  11. package/dist/docx-redline-js.esm.min.js +111 -108
  12. package/dist/docx-redline-js.esm.min.js.map +4 -4
  13. package/dist/docx-redline.bundle.cjs +4463 -921
  14. package/dist/docx-redline.bundle.cjs.map +4 -4
  15. package/dist/docx-redline.bundle.js +4463 -921
  16. package/dist/docx-redline.bundle.js.map +4 -4
  17. package/docs/AGENT_FAST_START.md +2 -0
  18. package/docs/AGENT_KNOWLEDGE_BASE.md +49 -7
  19. package/docs/SKILL_AUTHORING.md +169 -150
  20. package/docs/TESTING.md +1146 -1059
  21. package/docs/schemas/document-operations.schema.json +55 -2
  22. package/document/docx-document.js +1022 -1010
  23. package/engine/oxml-engine.js +21 -22
  24. package/engine/paragraph-mark-revisions.js +292 -0
  25. package/engine/reconstruction-mapper.js +312 -311
  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 +53 -2
  32. package/index.js +13 -2
  33. package/node/cli-help.js +1 -0
  34. package/node/cli.js +21 -3
  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/markdown-processor.js +5 -2
  42. package/pipeline/pipeline.js +206 -204
  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 +23 -3
  52. package/services/capture-engine.js +228 -226
  53. package/services/comment-replies.js +1 -1
  54. package/services/document-operation-applier.js +214 -0
  55. package/services/document-operation-contract.js +76 -0
  56. package/services/document-operation-mutations.js +688 -51
  57. package/services/error-recovery.js +17 -0
  58. package/services/inspection-comparison.js +82 -0
  59. package/services/list-conversion-diagnostics.js +296 -0
  60. package/services/list-format-change.js +586 -0
  61. package/services/list-format-mutation.js +416 -0
  62. package/services/list-format.js +305 -0
  63. package/services/numbering-allocation.js +642 -0
  64. package/services/numbering-helpers.js +354 -524
  65. package/services/operation-batch-compiler.js +183 -11
  66. package/services/operation-preflight.js +43 -33
  67. package/services/receipt-collector.js +288 -288
  68. package/services/revision-comment-management.js +38 -5
  69. package/services/revision-fidelity-verifier.js +484 -0
  70. package/services/standalone-docx-plumbing.js +627 -631
  71. package/services/standalone-operation-runner.d.ts +182 -5
  72. package/services/table-reconciliation.js +59 -7
package/ARCHITECTURE.md CHANGED
@@ -1,460 +1,679 @@
1
- # Reconciliation Core Architecture
2
-
3
- This document describes the standalone, host-agnostic OOXML reconciliation package and how to work inside it safely.
4
-
5
- ## Scope
6
-
7
- This repository contains only the publishable package surface:
8
-
9
- - `adapters/`
10
- - `core/`
11
- - `document/` (universal complete-DOCX facade & ZIP archive layer)
12
- - `engine/`
13
- - `pipeline/`
14
- - `services/`
15
- - `orchestration/`
16
- - `node/` (Node CLI & backward-compatibility facade)
17
- - `index.js`
18
- - `index.d.ts`
19
- - `dist/`
20
-
21
- No Word add-in entrypoints or host-specific integration layers are part of this package.
22
-
23
- ## Goals
24
-
25
- - Preserve Word-compatible redlines by editing OOXML directly.
26
- - Keep core logic and document-level operations host-independent (no Office.js globals, no Word API calls, no runtime Node built-ins like `node:zlib` or `node:crypto`).
27
- - Standardize complete-document packages on `Uint8Array` binary payloads, pure-JS SHA-256 revision tokens, and `fflate` ZIP container manipulation.
28
- - Reuse the same engine across Node.js, browsers, Cloudflare Workers, Deno, Bun, and sandboxed runtimes (e.g. n8n Code nodes).
29
- - Keep generated OOXML schema-safe through shared Word element creation and
30
- centralized revision metadata helpers.
31
-
32
- ## Folder Layout
33
-
34
- ```text
35
- .
36
- ├── adapters/
37
- │ ├── config.js
38
- │ ├── logger.js
39
- │ └── xml-adapter.js
40
- ├── core/
41
- │ ├── paragraph-text.js
42
- │ ├── sha256.js
43
- │ └── word-xml.js
44
- ├── document/
45
- │ ├── docx-document.js
46
- │ ├── docx-document.d.ts
47
- │ └── zip-archive.js
48
- ├── engine/
49
- │ ├── oxml-engine.js
50
- │ ├── surgical-mode.js
51
- │ ├── reconstruction-mode.js
52
- │ ├── run-builders.js
53
- │ └── formatting-removal.js
54
- ├── orchestration/
55
- ├── pipeline/
56
- ├── services/
57
- │ ├── comment-engine.js
58
- │ ├── numbering-helpers.js
59
- │ ├── revision-comment-management.js
60
- │ ├── document-inspection.js
61
- │ ├── document-operation-session.js
62
- │ ├── document-operation-applier.js
63
- │ ├── document-operation-mutations.js
64
- │ ├── batch-operation-orchestrator.js
65
- │ ├── operation-heuristics.js
66
- │ ├── standalone-docx-plumbing.js
67
- │ └── standalone-operation-runner.js
68
- ├── node/
69
- │ ├── docx-document.js
70
- │ ├── cli.js
71
- │ ├── zip-archive.js
72
- │ └── index.js
73
- ├── dist/
74
- │ ├── docx-redline-js.esm.js
75
- │ ├── docx-redline.bundle.js
76
- │ └── docx-redline.bundle.cjs
77
- ├── index.js
78
- └── index.d.ts
79
- ```
80
-
81
- ## Entry Points
82
-
83
- - `index.js`: primary host-independent exports for OOXML reconciliation and universal complete-document operations (`openDocx`, `DocxDocument`, `computePackageRevisionToken`).
84
- - `document/docx-document.js`: universal `Uint8Array`-based document facade and package-level operation runner.
85
- - `dist/docx-redline.bundle.js` / `dist/docx-redline.bundle.cjs`: zero-dependency standalone sandbox bundles, exported via `@ansonlai/docx-redline-js/bundle`.
86
- - `services/standalone-operation-runner.js`: stable complete-document XML
87
- operations, exported as `@ansonlai/docx-redline-js/standalone-runner`.
88
- - `node/index.js`: Node-compatible complete-DOCX facade re-exports, exported as
89
- `@ansonlai/docx-redline-js/node`.
90
- - `bin/docx-redline.js`: CLI launcher; command implementation lives in
91
- `node/cli.js` and shared operation behavior lives in `services/`.
92
-
93
- ## Module Responsibilities
94
-
95
- - `adapters/config.js`
96
- - Runtime configuration for defaults (`setDefaultAuthor`, `getDefaultAuthor`, `setPlatform`, `getPlatform`).
97
- - `adapters/xml-adapter.js`
98
- - XML parser/serializer injection for browser or Node.js runtimes.
99
- - `adapters/logger.js`
100
- - Runtime logger injection and shared logging methods.
101
- - `core/*`
102
- - Shared types, OOXML identity helpers, target resolution, list/table targeting heuristics, and XML query helpers.
103
- - `core/word-xml.js`
104
- - Namespace-safe Word element creation, tracked-change detection, and OOXML payload source-shape helpers.
105
- - `core/types.js`
106
- - Shared model enums/types plus revision metadata generation and document-aware revision ID seeding.
107
- - `core/paragraph-text.js`
108
- - Canonical accepted/rejected/current-view text shared by targeting,
109
- ingestion, and inspection.
110
- - `core/paragraph-targeting.js`
111
- - Target descriptors and session-scoped paragraph metadata indexes. The
112
- index groups IDs and normalized text while retaining canonical text,
113
- fingerprints, document order, and table context for deterministic reuse.
114
- - `core/redline-validation.js`
115
- - Runtime structural validation (`validateRedlineOoxml`) mirroring the test-suite invariants: no nested revisions, `w:delText` inside `w:del`, complete revision metadata, unique revision ids, preserved boundary whitespace.
116
- - `core/validation-delta.js`
117
- - Stable issue signatures and multiset subtraction for classifying baseline
118
- versus generated validation issues without hiding added duplicate errors.
119
- - `core/revision-cloning.js`
120
- - Shared effective-property cloning strips historical revision descendants;
121
- intentional revision-bearing splits refresh cloned property-change IDs.
122
- - `engine/oxml-engine.js`
123
- - Main reconciliation router, mode selection, existing-revision policy gate, and status/error result handling.
124
- - `engine/route-selection.js`
125
- - Internal reconciliation capability matrix and opt-in diagnostic route
126
- instrumentation. It observes policy; it does not expose a route override.
127
- - `engine/run-builders.js`
128
- - Shared builders for insertion/deletion wrappers, paragraph-mark revisions, visible run content, and run-property changes.
129
- - `engine/surgical-*.js`
130
- - Surgical run splitting, diff application, and span helpers for localized edits that preserve surrounding markup. Plain-text edit groups execute from right to left with a fresh live span index, while space/NBSP-only replacement hunks are refined to character-local changes so unchanged hyperlink containers survive.
131
- The shared carrier splitter also handles explicit rejected-view deletion
132
- splits, retaining `w:delText`, tabs, breaks, soft/non-breaking hyphens,
133
- formatting, foreign metadata, and fresh trailing/property-change IDs.
134
- - `engine/formatting-removal.js`
135
- - Shared formatting removal and highlight helpers.
136
- - `pipeline/list-markers.js`
137
- - Dependency-light canonical list-marker grammar, classification, numbering
138
- vocabulary, and parsed list-item representation.
139
- - `pipeline/list-generation.js`
140
- - Structural paragraph-to-list generation, paragraph-mark revisions, and
141
- selective inheritance of source typography for inserted list runs.
142
- - `pipeline/structured-content.js`
143
- - Strict decomposition and planning for atomic mixed heading/paragraph/list/
144
- table replacements. It rejects ambiguous table-like pipe text rather than
145
- allowing a fallback to literal paragraphs.
146
- - `pipeline/*`
147
- - Ingestion, markdown preprocessing, diffing, patching, and serialization stages. Ingestion treats deleted and moved-from content as non-visible text and inserted/moved-to content as visible text.
148
- - `services/comment-engine.js`
149
- - Comment creation and package-level comment XML handling.
150
- - `services/numbering-helpers.js`
151
- - Dynamic numbering ID allocation, numbering payload remapping, and schema-order-safe numbering merges.
152
- - `services/standalone-docx-plumbing.js`
153
- - Package-level extraction/wiring/validation for `word/document.xml`, `word/numbering.xml`, and `word/comments.xml`.
154
- - `services/revision-comment-management.js`
155
- - OOXML transforms for accepting/rejecting insertion, deletion, move, paragraph-mark, and property-change revisions by author/all-authors, plus deleting comments by author/all-authors.
156
- - `services/standalone-operation-runner.js`
157
- - Stable compatibility facade for full-document operations. It only re-exports
158
- preflight, single-operation application, batch application, and scheduling.
159
- - `services/document-operation-applier.js`
160
- - Canonical single-operation validation, author resolution, dispatch, and
161
- result metadata assembly. Before commit it validates the entire live
162
- document against the operation savepoint and refuses newly generated
163
- structural errors, including duplicate revision IDs.
164
- - `services/document-operation-mutations.js`
165
- - Coupled OOXML mutation implementations for redline, highlight, and comment
166
- operations. For text-bearing edits, the resolved paragraph's exact
167
- accepted-view text is the source coordinate system even when target
168
- selection used normalized whitespace;
169
- bounded match-mode/code-point diagnostics are attached to resolved target
170
- metadata. Explicit rejected-view insertion splits a supported direct
171
- foreign `w:del` at an anchor-relative offset into sibling deletion,
172
- insertion, and deletion carriers. Paragraph restoration emits its inserted
173
- block after the untouched source range and verifies baseline-delta,
174
- mutation-envelope, and lifecycle postconditions. These use leaf-module
175
- imports and never import the root entry.
176
- - `services/batch-operation-orchestrator.js`
177
- - Comment-first stable scheduling, atomic policy, artifact aggregation,
178
- per-operation results, one final document serialization, and deferred
179
- runtime-context commit.
180
- - `services/document-operation-session.js`
181
- - One live document DOM and revision allocator per invocation, immutable
182
- start-of-batch target snapshot, lazy paragraph metadata, per-operation DOM
183
- and allocator savepoints, exact original XML rollback, index invalidation,
184
- artifact/result accumulation, and isolated runtime-context helpers.
185
- - `services/operation-heuristics.js`
186
- - DOM-light decisions for list/plain adjacency insertion and explicit-range
187
- list insertion. Canonical list/table targeting remains in `core/*`.
188
- - `services/document-operation-contract.js`
189
- - Compatibility normalization, canonical operation kinds, author precedence,
190
- and stable runtime validation for document operations.
191
- - `services/operation-preflight.js`
192
- - Read-only strict target and anchor resolution, revision-policy diagnostics,
193
- artifact prediction, and same-paragraph conflict reporting.
194
- - `services/document-inspection.js`
195
- - Read-only paragraphs/comments inventory with target identity, headings,
196
- revision authors, table context, and advisory visible numbering.
197
- - `document/docx-document.js`
198
- - Universal transactional whole-DOCX editing, artifact wiring, validation, and rollback.
199
- Standardized on `Uint8Array` binary payloads and pure-JS SHA-256 revision tokens.
200
- Exposes `openDocx`, `DocxDocument`, and `computePackageRevisionToken` directly across all runtimes.
201
- - `document/zip-archive.js`
202
- - Universal, pure-JS ZIP archive reader and writer based on `fflate`. Operates on
203
- `Uint8Array` byte arrays with zero Node built-ins (`node:zlib`, `Buffer`).
204
- - `core/sha256.js`
205
- - Pure-JS synchronous implementation of FIPS 180-4 SHA-256 with zero dependencies,
206
- enabling deterministic revision tokens in sandboxed runtimes without `node:crypto`.
207
- - `node/docx-document.js` and `node/zip-archive.js`
208
- - Node-compatible re-exports maintaining `Buffer` compatibility for `@ansonlai/docx-redline-js/node`.
209
- - `node/cli.js` and `bin/docx-redline.js`
210
- - Cross-platform, JSON-only agent command boundary. Read commands never
211
- mutate; write commands require attribution, use package transactions, and
212
- only overwrite source files under explicit `--in-place` authorization.
213
- Mutation commands expose a compact contract: package/XML payloads and full
214
- validation arrays remain internal, while stdout contains durability fields,
215
- per-operation evidence, validation counts, and a derived completion flag.
216
- - `orchestration/*`
217
- - Route planning and list fallback orchestration utilities.
218
-
219
- ## End-to-End Flow
220
-
221
- 1. Caller imports from `index.js`.
222
- 2. Caller configures XML provider/logger/defaults when needed via `adapters/*`.
223
- 3. Caller invokes reconciliation APIs (`applyRedlineToOxml`, operation runner, ingestion/export helpers).
224
- 4. A full-document batch parses once, captures its initial target snapshot, and
225
- creates an operation savepoint before each mutation.
226
- 5. `engine/oxml-engine.js` routes each selected scope to format, table, list,
227
- surgical, or reconstruction flows; successful scoped output is imported
228
- into the live document.
229
- 6. Failed and no-op operations restore their savepoint. A successful changed
230
- batch serializes the full document once; atomic failures and all-no-op
231
- batches return the exact input string without serialization.
232
- 7. Pipeline/services return OOXML, optional package artifacts (`numberingXml`, comments payloads), and non-breaking `status`/`error` fields where applicable.
233
- 8. Optional revision/comment management transforms can accept/reject revisions, including move revisions, or delete comments by author.
234
- 9. Caller writes resulting XML back to package/document boundaries.
235
-
236
- ## Public Surfaces
237
-
238
- - Primary: `index.js`
239
- - Types: `index.d.ts`
240
- - Stable XML operation runner: `@ansonlai/docx-redline-js/standalone-runner`
241
- - Node-only DOCX facade: `@ansonlai/docx-redline-js/node`
242
-
243
- Keep public exports centralized through `index.js`; deep imports are supported by
244
- the package `exports` map for advanced consumers, but new public APIs should
245
- still be re-exported from `index.js`.
246
-
247
- ## Reliability Guardrails
248
-
249
- - Create Word namespace elements with `createWordElement(xmlDoc, 'w:...')`.
250
- Avoid direct `document.createElement('w:*')` or `createElementNS(NS_W, 'w:*')`
251
- outside `core/word-xml.js`.
252
- - Generate tracked-change metadata through `createRevisionMetadata(author)` so
253
- `w:id`, `w:author`, and `w:date` stay consistent and document-unique.
254
- - Seed revision IDs from parsed input with `seedRevisionIdsFromDocument(xmlDoc)`
255
- before emitting new tracked changes.
256
- - Treat revision metadata inside cloned content as identity-bearing state, not
257
- ordinary formatting. When a run containing `w:rPrChange` is split or cloned,
258
- preserve an existing revision ID on at most one output run and allocate a
259
- fresh document-scoped ID for every additional copy.
260
- - Use `containsTrackedChanges(xmlDoc)` and `getTrackedChangeAuthors(xmlDoc)`
261
- to inspect existing revisions. By default (`existingRevisions: 'merge-same-author'`),
262
- subsequent edits from the same author are merged against the pre-revision baseline
263
- while revisions from different authors fail closed with `EXISTING_REVISIONS`.
264
- Revised paragraphs containing comment anchors fail closed with
265
- `COMMENTED_CONTENT_MERGE`; never discard anchors as a side effect of restoring
266
- the pre-revision baseline.
267
- Pass `existingRevisions: 'accept-all-first'` to normalize prior revisions explicitly.
268
- - Do not write unknown `result.oxml` payloads directly into `word/document.xml`;
269
- normalize with `extractReplacementNodesFromOoxml(...)` or use
270
- `applyOperationToDocumentXml(...).documentXml` for full-document replacement.
271
- - Run `validateRedlineOoxml(oxml)` on generated output before packaging it;
272
- it reports structural invariant violations as `{ valid, issues }`.
273
-
274
- ## Integration Contracts
275
-
276
- ### Targeting and text fidelity
277
-
278
- - Paragraph targeting and replacement have different contracts. Targeting may
279
- use normalized text and fallback heuristics; replacement content is literal
280
- and must preserve tabs, line breaks, non-breaking spaces, repeated spaces,
281
- and boundary whitespace.
282
- - Text extraction used for targeting represents the visible accepted view of
283
- tracked content: insertions are visible and deletions are excluded.
284
- - Paragraph indexes are transient integration references, not user-facing
285
- document identifiers. Prefer paragraph IDs plus exact text where available.
286
- - Ambiguous matches are unsafe for document mutation. New targeting surfaces
287
- should report candidate matches or `AMBIGUOUS_TARGET` rather than silently
288
- choosing the first paragraph.
289
- - Do not remove operation-session savepoints merely to improve throughput.
290
- Any replacement must prove that a thrown error or false no-op cannot leak a
291
- partial DOM mutation or revision-ID allocation into later operations.
292
-
293
- ### Structural paragraph-to-list replacement
294
-
295
- - In WordprocessingML, `w:numId w:val="0"` suppresses numbering. It represents
296
- no reusable list context and must never be returned by numbering allocation
297
- or assigned to a generated list item. Generated lists use a positive `numId`
298
- bound to a compatible `w:abstractNum` definition.
299
- - Expanding one non-list paragraph into multiple list paragraphs is a block
300
- replacement, not an inline replacement. The original content remains in its
301
- own paragraph with a deleted paragraph mark; every replacement item occupies
302
- its own paragraph with an inserted paragraph mark and inserted text. This is
303
- required so Word can accept the edit as the new paragraphs or reject it as
304
- the exact original paragraph without joining the old heading to item one.
305
- - Inserted list runs selectively inherit source typography (`w:rFonts`, size,
306
- language, and related script properties). Semantic emphasis such as bold,
307
- underline, italic, or strike is emitted only when requested by the replacement
308
- markup; heading emphasis must not leak into ordinary list body text.
309
- - Low-level list output can carry a package-fragment sentinel needed by its
310
- standalone payload shape. Full-document mutation removes that sentinel while
311
- importing replacement nodes, so it cannot become an empty document paragraph.
312
- - Tests for this route must verify positive numbering IDs, separate physical
313
- paragraphs, formatting inheritance/non-inheritance, valid revision markup,
314
- and exact accepted and rejected paragraph sequences.
315
-
316
- ### Atomic mixed-content replacement
317
-
318
- - `planStructuredReplacement(...)` converts agent-authored Markdown into one
319
- replacement operation carrying `structuredContent: true`. The block plan is
320
- diagnostic metadata; execution remains one mutation so replacing the anchor
321
- cannot invalidate later blocks.
322
- - Mixed-content parsing recognizes explicit Markdown headings, blank-line
323
- paragraph boundaries, adjacent list items, and contiguous table rows. Tables
324
- require a header separator and consistent column counts. Invalid input fails
325
- with `STRUCTURED_CONTENT_INVALID`; silently degrading table pipes into visible
326
- paragraph text is forbidden on this route.
327
- - Newly inserted tables inside a mixed replacement are tracked once at block
328
- scope. Cell runs are not nested in additional `w:ins` wrappers, preserving the
329
- no-nested-revisions invariant while allowing Reject All to remove the table.
330
- - Agents must use explicit heading markers and valid table grammar rather than
331
- relying on capitalization or layout inference. The engine preserves the
332
- declared block types and does not guess legal-document semantics.
333
-
334
- ### Package artifacts and transactions
335
-
336
- - A real `.docx` stores document markup, comments, numbering, relationships,
337
- and content types in separate parts. APIs operating on one XML string cannot
338
- claim to update artifacts held in another part.
339
- - Comment IDs must be allocated against both document anchors and the existing
340
- `word/comments.xml` part. Revision IDs must be allocated against the complete
341
- document revision scope.
342
- - Numbering payloads must be merged with `mergeNumberingXmlBySchemaOrder` when
343
- a package already contains numbering definitions; replacement is not a merge.
344
- - Safe package mutation is transactional: retain the original package, apply
345
- operations, merge all artifacts, validate redline markup and package wiring,
346
- and commit only if every required check succeeds.
347
- - `openDocx(buffer)` implements this transaction for Node without adding a ZIP
348
- dependency to browser or XML-only consumers. Unmodified part contents remain
349
- byte-identical after extraction, although the ZIP container is reserialized.
350
-
351
- ### Operation results
352
-
353
- - `hasChanges: false` does not imply success. Callers must check `status`,
354
- `error`, and warnings to distinguish errors, missing anchors, and true no-ops.
355
- - Batch results must preserve the caller's original operation indexes even when
356
- execution is reordered for stable anchors.
357
- - Author attribution is externally visible document data. Agent-facing APIs
358
- should require an explicit author and report the author used for every
359
- operation rather than relying silently on a configured fallback.
360
- - Operation-level authors override the batch author. Runtime results expose
361
- `authorUsed`, `authorsUsed`, `operationType`, `resolvedBy`, and resolved target
362
- metadata so integrations can audit what the engine actually selected.
363
- - Target fingerprints are revision-view scoped. Inspection returns a
364
- `revisionView` beside every paragraph and computes its fingerprint from the
365
- same text/view pair. Restore normalization defaults omitted target and range
366
- endpoint views to `rejected`; all other operations default to `accepted`.
367
- Explicit views remain authoritative, and cross-view text/fingerprint mistakes
368
- return an actionable mismatch hint rather than silently weakening targeting.
369
- - A normalized target match does not become an edit coordinate system for a
370
- text-bearing mutation. Mutation uses canonical accepted-view source text, and `resolvedTarget.targetTextMatch`
371
- records `exact`, `space_equivalent`, or `normalized` selection plus bounded
372
- invisible-character diagnostics.
373
- - CLI contract version 3 is deliberately narrower than library result objects.
374
- `apply`, `accept`, `reject`, and `delete-comments` omit `documentXml`, package
375
- parts, inspection text, and full issue arrays. `completion` is true only when
376
- `written === true`, top-level status is neither error nor partial, and every
377
- operation result is non-error. The `validate` command remains the full issue
378
- reporting surface.
379
- - `preflightOperations` is the read-only safety boundary for agent-generated
380
- batches. It uses strict targeting by default; mutation APIs retain permissive
381
- legacy targeting unless `strictTargets: true` is requested. In v1.0.0,
382
- application will default to strict targeting; in the current warning cycle,
383
- permissive resolution that chooses among multiple identical paragraphs emits
384
- `AMBIGUOUS_TARGET_HEURISTIC_USED` with candidate count and migration guidance.
385
- Both preflight and mutation runners share the same candidate resolver
386
- (`resolveTargetParagraph`).
387
-
388
- ### Mutation receipts and output reconciliation oracle
389
-
390
- - `ReceiptCollector` tracks exact allocations (revision IDs with kind and target part,
391
- comment IDs, numbering IDs, relationship IDs, affected targets, and warnings)
392
- directly at the point of allocation/attachment during operation execution.
393
- - Collector state is snapshotted within operation savepoints. Failed or rolled-back
394
- operations cleanly restore prior collector state without leaking orphaned allocations.
395
- - Single operations expose `result.receipt`; batches expose per-item `results[i].receipt`
396
- and top-level `result.receipts`, with dispositions (`applied`, `refused`, `no_change`,
397
- `rolled_back`, `not_attempted`) and `committed: true/false`.
398
- - The output reconciliation oracle (`reconcileReceiptsAgainstOutput`) performs an
399
- independent, non-negotiable verification: every durable ID reported as committed
400
- is parsed from the serialized output XML parts (`word/document.xml`, `word/comments.xml`,
401
- `word/numbering.xml`). Any discrepancy immediately fails the transaction and triggers
402
- atomic rollback.
403
-
404
-
405
- ## Build Output
406
-
407
- `npm run build` generates CDN-ready ESM bundles under `dist/`:
408
-
409
- - `dist/docx-redline-js.esm.js`
410
- - `dist/docx-redline-js.esm.js.map`
411
- - `dist/docx-redline-js.esm.min.js`
412
- - `dist/docx-redline-js.esm.min.js.map`
413
-
414
- The bundle inlines `diff-match-patch` and keeps `@xmldom/xmldom` external.
415
-
416
- ## Testing
417
-
418
- - `npm test`
419
- - Runs all `tests/*.mjs` except setup helpers in separate Node processes with
420
- a conservative four-worker cap. Use `DOCX_TEST_CONCURRENCY=1` to reproduce
421
- the same sorted suite serially.
422
- - `npm run benchmark:tests`
423
- - Compares paired serial and bounded-parallel suite runs and records JSON
424
- under ignored `tmp/benchmarks/`.
425
- - `npm run test:isolation`
426
- - Runs boundary checks for Word API markers and dependency-graph isolation.
427
- - `npm run check:types`
428
- - Smoke-checks `index.d.ts`.
429
- - `node scripts/export-validation-fixtures.mjs`
430
- - Writes release-time validation fixtures to `tmp/validation-docx/` as
431
- `word/document.xml` parts, assembled `.docx` files, and expected-text sidecars.
432
- - `tests/roundtrip_fuzz_tests.mjs` (part of `npm test`)
433
- - Seeded fuzz sweep of the accept/reject round-trip invariant; tune with
434
- `FUZZ_SEED` / `FUZZ_ITERATIONS`.
435
- - `npm run smoke:word -- path/to/file.docx`
436
- - Optional Windows/Word COM smoke test for a completed `.docx`.
437
- - `npm run smoke:word:diff`
438
- - Windows/Word COM differential test: Word itself accepts/rejects the
439
- generated fixtures and the resulting text is compared to expectations.
440
- - `bash scripts/validate-fixtures-xsd.sh`
441
- - Validates exported fixtures against the ECMA-376 transitional `wml.xsd`.
442
- - `.github/workflows/validation.yml`
443
- - Nightly independent-oracle validation: XSD schema check, LibreOffice
444
- conversion, and an extended 20k-case fuzz sweep with a fresh seed.
445
-
446
- Use these checks before publishing or tagging. See [docs/TESTING.md](./docs/TESTING.md#release-validation-and-independent-oracles).
447
-
448
- ## Fast Orientation For Contributors
449
-
450
- Use this sequence to understand or modify behavior without reading everything:
451
-
452
- 1. Start at `index.js` to locate the exported API.
453
- 2. Follow exports into `engine/oxml-engine.js` or relevant `services/*` module.
454
- 3. For targeting bugs, inspect `core/paragraph-targeting.js`, `core/list-targeting.js`, and `core/table-targeting.js`.
455
- 4. For package wiring issues, inspect `services/standalone-docx-plumbing.js`.
456
- 5. For revision/comment cleanup behavior, inspect `services/revision-comment-management.js`.
457
- 6. For numbering/list issues, inspect `services/numbering-helpers.js` and orchestration list-fallback modules.
458
- 7. For reliability regressions, start with `tests/roundtrip_invariant_tests.mjs`,
459
- `tests/engine_reliability_tests.mjs`, `tests/paragraph_mark_revision_tests.mjs`,
460
- `tests/move_revision_tests.mjs`, and `tests/hardening_status_tests.mjs`.
1
+ # Reconciliation Core Architecture
2
+
3
+ This document describes the standalone, host-agnostic OOXML reconciliation package and how to work inside it safely.
4
+
5
+ ## Scope
6
+
7
+ This repository contains only the publishable package surface:
8
+
9
+ - `adapters/`
10
+ - `core/`
11
+ - `document/` (universal complete-DOCX facade & ZIP archive layer)
12
+ - `engine/`
13
+ - `pipeline/`
14
+ - `services/`
15
+ - `orchestration/`
16
+ - `node/` (Node CLI & backward-compatibility facade)
17
+ - `index.js`
18
+ - `index.d.ts`
19
+ - `dist/`
20
+
21
+ No Word add-in entrypoints or host-specific integration layers are part of this package.
22
+
23
+ ## Goals
24
+
25
+ - Preserve Word-compatible redlines by editing OOXML directly.
26
+ - Keep core logic and document-level operations host-independent (no Office.js globals, no Word API calls, no runtime Node built-ins like `node:zlib` or `node:crypto`).
27
+ - Standardize complete-document packages on `Uint8Array` binary payloads, pure-JS SHA-256 revision tokens, and `fflate` ZIP container manipulation.
28
+ - Reuse the same engine across Node.js, browsers, Cloudflare Workers, Deno, Bun, and sandboxed runtimes (e.g. n8n Code nodes).
29
+ - Keep generated OOXML schema-safe through shared Word element creation and
30
+ centralized revision metadata helpers.
31
+
32
+ ## Folder Layout
33
+
34
+ ```text
35
+ .
36
+ ├── adapters/
37
+ │ ├── config.js
38
+ │ ├── logger.js
39
+ │ └── xml-adapter.js
40
+ ├── core/
41
+ │ ├── current-properties.js
42
+ │ ├── paragraph-text.js
43
+ │ ├── sha256.js
44
+ │ ├── word-revision-semantics.js
45
+ │ └── word-xml.js
46
+ ├── document/
47
+ │ ├── docx-document.js
48
+ │ ├── docx-document.d.ts
49
+ │ └── zip-archive.js
50
+ ├── engine/
51
+ │ ├── oxml-engine.js
52
+ │ ├── surgical-mode.js
53
+ │ ├── reconstruction-mode.js
54
+ │ ├── run-builders.js
55
+ │ ├── paragraph-mark-revisions.js
56
+ │ └── formatting-removal.js
57
+ ├── orchestration/
58
+ ├── pipeline/
59
+ ├── services/
60
+ │ ├── comment-engine.js
61
+ │ ├── numbering-helpers.js
62
+ │ ├── numbering-allocation.js
63
+ │ ├── list-conversion-diagnostics.js
64
+ │ ├── list-format.js
65
+ │ ├── list-format-mutation.js
66
+ │ ├── list-format-change.js
67
+ │ ├── operation-batch-compiler.js
68
+ │ ├── localized-replacement-compiler.js
69
+ │ ├── revision-comment-management.js
70
+ │ ├── document-inspection.js
71
+ │ ├── inspection-comparison.js
72
+ │ ├── document-operation-session.js
73
+ │ ├── document-operation-applier.js
74
+ │ ├── document-operation-mutations.js
75
+ │ ├── batch-operation-orchestrator.js
76
+ │ ├── revision-fidelity-verifier.js
77
+ │ ├── operation-heuristics.js
78
+ │ ├── standalone-docx-plumbing.js
79
+ │ └── standalone-operation-runner.js
80
+ ├── node/
81
+ │ ├── docx-document.js
82
+ │ ├── cli.js
83
+ │ ├── zip-archive.js
84
+ │ └── index.js
85
+ ├── dist/
86
+ │ ├── docx-redline-js.esm.js
87
+ │ ├── docx-redline.bundle.js
88
+ │ └── docx-redline.bundle.cjs
89
+ ├── index.js
90
+ └── index.d.ts
91
+ ```
92
+
93
+ ## Entry Points
94
+
95
+ - `index.js`: primary host-independent exports for OOXML reconciliation and universal complete-document operations (`openDocx`, `DocxDocument`, `computePackageRevisionToken`).
96
+ - `document/docx-document.js`: universal `Uint8Array`-based document facade and package-level operation runner.
97
+ - `dist/docx-redline.bundle.js` / `dist/docx-redline.bundle.cjs`: zero-dependency standalone sandbox bundles, exported via `@ansonlai/docx-redline-js/bundle`.
98
+ - `services/standalone-operation-runner.js`: stable complete-document XML
99
+ operations, exported as `@ansonlai/docx-redline-js/standalone-runner`.
100
+ - `node/index.js`: Node-compatible complete-DOCX facade re-exports, exported as
101
+ `@ansonlai/docx-redline-js/node`.
102
+ - `bin/docx-redline.js`: CLI launcher; command implementation lives in
103
+ `node/cli.js` and shared operation behavior lives in `services/`.
104
+
105
+ ## Module Responsibilities
106
+
107
+ - `adapters/config.js`
108
+ - Runtime configuration for defaults (`setDefaultAuthor`, `getDefaultAuthor`, `setPlatform`, `getPlatform`).
109
+ - `adapters/xml-adapter.js`
110
+ - XML parser/serializer injection for browser or Node.js runtimes.
111
+ - `adapters/logger.js`
112
+ - Runtime logger injection and shared logging methods.
113
+ - `core/*`
114
+ - Shared types, OOXML identity helpers, target resolution, list/table targeting heuristics, and XML query helpers.
115
+ - `core/word-xml.js`
116
+ - Namespace-safe Word element creation, tracked-change detection, OOXML payload source-shape helpers, and
117
+ `resolveExistingRevisionsPolicy`, the single place `existingRevisions: 'auto'` is resolved (per target
118
+ paragraph or per `targetEnd` range; unattributed changes count as another author's).
119
+ - `core/types.js`
120
+ - Shared model enums/types plus revision metadata generation and document-aware revision ID seeding.
121
+ - `core/paragraph-text.js`
122
+ - Canonical accepted/rejected/current-view text shared by targeting,
123
+ ingestion, and inspection.
124
+ - `core/paragraph-targeting.js`
125
+ - Target descriptors and session-scoped paragraph metadata indexes. The
126
+ index groups IDs and normalized text while retaining canonical text,
127
+ fingerprints, document order, and table context for deterministic reuse.
128
+ - `core/redline-validation.js`
129
+ - Runtime structural validation (`validateRedlineOoxml`) mirroring the test-suite invariants: no nested revisions, `w:delText` inside `w:del`, complete revision metadata, unique revision ids, preserved boundary whitespace.
130
+ - `core/validation-delta.js`
131
+ - Stable issue signatures and multiset subtraction for classifying baseline
132
+ versus generated validation issues without hiding added duplicate errors.
133
+ - `core/word-revision-semantics.js`
134
+ - Pure DOM projection of what Word shows after Reject All or Accept All
135
+ (`projectWordRevisionView`, `wordRevisionParagraphTexts`,
136
+ `paragraphMarkRevision`). It encodes Word's paragraph-mark rules: a
137
+ tracked-inserted mark vanishes on Reject and its content merges into the
138
+ following paragraph of the same container, a tracked-deleted mark does the
139
+ same on Accept, a table cell keeps its last paragraph, rows marked inserted
140
+ or deleted vanish, and Reject restores `w:pPrChange` snapshots. It never
141
+ mutates or serializes. The rules are validated against recorded desktop
142
+ Word 16 COM results (`tests/fixtures/word-revision-semantics/`).
143
+ - `core/current-properties.js`
144
+ - Direct-child accessors for the current paragraph/run properties
145
+ (`getCurrentParagraphProperties`, `getCurrentRunProperties`,
146
+ `getCurrentChild`, `getCurrentNumIdElements`, ...). Descendant lookups cross
147
+ `w:pPrChange` / `w:rPrChange`, so a historical `numPr` or `pStyle` would look
148
+ current; the snapshot is reachable only through the explicit
149
+ `getHistoricalParagraphProperties` / `getHistoricalRunProperties` and
150
+ `getAllReferencedNumIdElements`. Depends only on `core/types.js`.
151
+ - `core/revision-cloning.js`
152
+ - Shared effective-property cloning strips historical revision descendants;
153
+ intentional revision-bearing splits refresh cloned property-change IDs.
154
+ - `engine/oxml-engine.js`
155
+ - Main reconciliation router, mode selection, existing-revision policy gate, and status/error result handling.
156
+ - `engine/route-selection.js`
157
+ - Internal reconciliation capability matrix and opt-in diagnostic route
158
+ instrumentation. It observes policy; it does not expose a route override.
159
+ - `engine/run-builders.js`
160
+ - Shared builders for insertion/deletion wrappers, visible run content, and run-property changes. It does not build paragraph-mark revisions.
161
+ - `engine/paragraph-mark-revisions.js`
162
+ - The single construction site for a tracked paragraph mark (`w:ins` / `w:del`
163
+ under `w:pPr/w:rPr`). DOM entry points (`markParagraphInserted`,
164
+ `markParagraphDeleted`, `setParagraphPropertiesMarkRevision`,
165
+ `clearParagraphMarkRevision`, `restoreParagraphMarkInserted`) and string
166
+ builders (`paragraphMarkRevisionXml`, `paragraphMarkPPrXml`) share one
167
+ metadata path, so ids are allocated through the document's allocator and
168
+ recorded in receipts. `planParagraphMarks` is a pure check that an
169
+ alignment gives every source paragraph exactly one untracked or deleted
170
+ mark and every new target paragraph an inserted mark, so Reject All yields
171
+ the source paragraph count and a deleted paragraph is never collapsed into
172
+ another. Reconstruction, list and table generation, and document-level
173
+ shortcuts all call it. Depends only on `core/`.
174
+ - `engine/surgical-*.js`
175
+ - Surgical run splitting, diff application, and span helpers for localized edits that preserve surrounding markup. Plain-text edit groups execute from right to left with a fresh live span index, while space/NBSP-only replacement hunks are refined to character-local changes so unchanged hyperlink containers survive.
176
+ The shared carrier splitter also handles explicit rejected-view deletion
177
+ splits, retaining `w:delText`, tabs, breaks, soft/non-breaking hyphens,
178
+ formatting, foreign metadata, and fresh trailing/property-change IDs.
179
+ - `engine/formatting-removal.js`
180
+ - Shared formatting removal and highlight helpers.
181
+ - `pipeline/list-markers.js`
182
+ - Dependency-light canonical list-marker grammar, classification, numbering
183
+ vocabulary, and parsed list-item representation.
184
+ - `pipeline/list-generation.js`
185
+ - Structural paragraph-to-list generation, paragraph-mark revisions, and
186
+ selective inheritance of source typography for inserted list runs.
187
+ - `pipeline/structured-content.js`
188
+ - Strict decomposition and planning for atomic mixed heading/paragraph/list/
189
+ table replacements. It rejects ambiguous table-like pipe text rather than
190
+ allowing a fallback to literal paragraphs.
191
+ - `pipeline/*`
192
+ - Ingestion, markdown preprocessing, diffing, patching, and serialization stages. Ingestion treats deleted and moved-from content as non-visible text and inserted/moved-to content as visible text.
193
+ - `services/comment-engine.js`
194
+ - Comment creation and package-level comment XML handling.
195
+ - `services/numbering-helpers.js`
196
+ - Document-scoped numbering ID state (`createDynamicNumberingIdState`,
197
+ `reserveNextNumberingIdPair`), numbering payload remapping, and shared DOM
198
+ predicates. It no longer owns a merge.
199
+ - `services/numbering-allocation.js`
200
+ - The single place that creates, continues, merges and validates numbering
201
+ definitions. `allocateNumberingDefinition` builds a new `w:abstractNum` +
202
+ `w:num` (source levels copied, chosen levels redefined, start overrides)
203
+ from ids reserved through the document state; `allocateNumberingForReplacement`
204
+ holds the reuse-source-list policy (`continue-source`, `allocate`,
205
+ `passthrough`) so no call site decides it; `continueBatchListSequence` lets
206
+ separate operations of one batch that generate items of the same kind,
207
+ format and level continue one numbering instance (reused only while its
208
+ `numId` is still bound in the live document). `mergeNumberingParts` /
209
+ `mergeNumberingXmlBySchemaOrder` are the one numbering merge, used by the
210
+ runner's consumers and the `openDocx` facade; an incoming `w:abstractNum` /
211
+ `w:num` id that exists with different content throws `NUMBERING_ID_CONFLICT`.
212
+ `checkNumberingBinding` validates a direct `{ numId, level }` binding and
213
+ `validateNumberingIntegrity` reports dangling `numId`, `abstractNumId` and
214
+ `ilvl` (`NUMBERING_*` issues) for the facade's baseline-delta validation.
215
+ Old definitions are never deleted or overwritten, and tracked-deleted source
216
+ paragraphs keep their `numPr`.
217
+ - `services/list-conversion-diagnostics.js`
218
+ - Safety diagnostics for text edits that retype list markers
219
+ (`LIST_MARKER_NOT_BOUND` / `UNSUPPORTED_LIST_CONVERSION`,
220
+ `LIST_FORMAT_NOT_APPLIED` / `UNSUPPORTED_LIST_FORMAT_CHANGE`; warning by
221
+ default, error under `strictListConversion`, silenced by
222
+ `listMarkers: 'literal'`), the closed `paragraph-format` key set (unknown
223
+ keys are `INVALID_OPERATION`) and the `numbering` shape check behind
224
+ `paragraph-format` numbering binding. Recovery points at `list-format`
225
+ through `recovery.suggestedOperation`.
226
+ - `services/standalone-docx-plumbing.js`
227
+ - Package-level extraction/wiring/validation for `word/document.xml`, `word/numbering.xml`, and `word/comments.xml`.
228
+ - `services/revision-comment-management.js`
229
+ - OOXML transforms for accepting/rejecting insertion, deletion, move, paragraph-mark, and property-change revisions by author/all-authors, plus deleting comments by author/all-authors.
230
+ - `services/standalone-operation-runner.js`
231
+ - Stable compatibility facade for full-document operations. It only re-exports
232
+ preflight, single-operation application, batch application, and scheduling.
233
+ - `services/document-operation-applier.js`
234
+ - Canonical single-operation validation, author resolution, dispatch, and
235
+ result metadata assembly. Before commit it validates the entire live
236
+ document against the operation savepoint and refuses newly generated
237
+ structural errors, including duplicate revision IDs. For localized
238
+ `replacements` it also checks, before commit, that the target's accepted text
239
+ (read from the successor paragraph via `SourceTargetRegistry.previewSuccessor`,
240
+ or the restored paragraph) equals the compiled desired text; otherwise the
241
+ savepoint is restored and `PATCH_ROUNDTRIP_MISMATCH` is returned, because a
242
+ tracked whole-paragraph deletion rejects back to the source and the Reject
243
+ All verifier alone cannot see it.
244
+ - `services/document-operation-mutations.js`
245
+ - Coupled OOXML mutation implementations for redline, highlight, and comment
246
+ operations. For text-bearing edits, the resolved paragraph's exact
247
+ accepted-view text is the source coordinate system even when target
248
+ selection used normalized whitespace;
249
+ bounded match-mode/code-point diagnostics are attached to resolved target
250
+ metadata. Explicit rejected-view insertion splits a supported direct
251
+ foreign `w:del` at an anchor-relative offset into sibling deletion,
252
+ insertion, and deletion carriers. Paragraph restoration emits its inserted
253
+ block after the untouched source range and verifies baseline-delta,
254
+ mutation-envelope, and lifecycle postconditions. These use leaf-module
255
+ imports and never import the root entry.
256
+ - `services/batch-operation-orchestrator.js`
257
+ - Comment-first stable scheduling, atomic policy, artifact aggregation,
258
+ per-operation results, one final document serialization, and deferred
259
+ runtime-context commit.
260
+ - `services/document-operation-session.js`
261
+ - One live document DOM and revision allocator per invocation, immutable
262
+ start-of-batch target snapshot, lazy paragraph metadata, per-operation DOM
263
+ and allocator savepoints, exact original XML rollback, index invalidation,
264
+ artifact/result accumulation, and isolated runtime-context helpers.
265
+ - `services/operation-heuristics.js`
266
+ - DOM-light decisions for list/plain adjacency insertion and explicit-range
267
+ list insertion. Canonical list/table targeting remains in `core/*`.
268
+ - `services/document-operation-contract.js`
269
+ - Compatibility normalization, canonical operation kinds, author precedence,
270
+ and stable runtime validation for document operations.
271
+ - `services/list-format.js` and `services/list-format-mutation.js`
272
+ - The `list-format` operation (`convert-plain`, plus the shared request shape
273
+ for `change-format` and `join`): contract shape, the narrow
274
+ typed-marker grammar (letter versus roman disambiguation by sequence or an
275
+ explicit `format`), and the in-place DOM core. Every refusal is decided
276
+ before mutation; the binding reuses the `paragraph-format.numbering` path
277
+ (`snapshotAndAttachPPrChange` + `applyParagraphPropertiesToPPr`), the marker
278
+ becomes one tracked `w:del`, definitions come from
279
+ `allocateNumberingDefinition`, and batch continuity uses
280
+ `runtimeContext.listFallbackSequenceState` (an entry is reused only while
281
+ its `numId` is bound in the live document). Text-equal redline header
282
+ conversions for letters, roman numerals and bullets call the same core.
283
+ The `paragraph-format` numbering binding (`properties.numbering`,
284
+ `{ numId, level }` or `null`) lives in `document-operation-mutations.js`: it
285
+ snapshots the earliest same-author `w:pPrChange`, refuses another author's
286
+ pending snapshot (`PARAGRAPH_FORMAT_REVISION_CONFLICT`), and checks the
287
+ binding with `checkNumberingBinding`. `list-format` is body-only; `openDocx`
288
+ refuses it with a `part` selector.
289
+ - `services/list-format-change.js`
290
+ - `list-format` `change-format` and `join` (`scope: 'list'` or `'range'`): rebind existing list items with
291
+ one tracked `w:pPrChange` each, never editing a shared definition.
292
+ `change-format` allocates a new definition through
293
+ `allocateNumberingDefinition` (source levels copied, one redefined, start
294
+ values on the new abstract); `join` reuses the earlier list's `numId`.
295
+ Planning models Word's counters (shared abstracts continue each other, a
296
+ level start re-applies on restart, deeper items start unused levels) and
297
+ refuses splits, joins and style-linked or instance-overridden definitions
298
+ it cannot reproduce exactly. Definitions are read from the source part plus
299
+ the batch's pending numbering parts.
300
+ - `services/operation-batch-compiler.js`
301
+ - Binds every operation to batch-start source paragraphs
302
+ (`SourceTargetRegistry`), compiles localized `replacements`, and detects
303
+ overlap before any mutation. Composition rule for `redline` + `list-format`:
304
+ exactly one single-paragraph `redline` (no `targetEnd`, anchor, part or
305
+ structured content) that keeps the typed marker and changes the text after
306
+ it composes with one `list-format` on the same paragraph; the compiler fixes
307
+ the order by setting `_compiledRunAfter` on the list-format, which the batch
308
+ orchestrator honours, so the redline always runs first. Everything else on a
309
+ list-format target is refused with `LIST_FORMAT_COMPOSITION_CONFLICT`
310
+ (`reason`: `marker-changed`, `redline-unchanged`, `redline-shape`, or
311
+ `list-rebind` when a `scope: 'list'` `change-format`/`join` rebinds an item
312
+ that another operation also writes). `list.continueFrom` is bound at batch
313
+ start as a read-only target. `previewSuccessor` lets the applier find a
314
+ target's replacement paragraph before commit.
315
+ - `services/operation-preflight.js`
316
+ - Read-only strict target and anchor resolution, revision-policy diagnostics
317
+ (reporting the policy `existingRevisions: 'auto'` resolves to per target via
318
+ `resolveExistingRevisionsPolicy` in `core/word-xml.js`),
319
+ artifact prediction, and same-paragraph conflict reporting.
320
+ - `services/document-inspection.js`
321
+ - Read-only paragraphs/comments inventory with target identity, headings,
322
+ revision authors, table context, and advisory visible numbering.
323
+ - `services/inspection-comparison.js`
324
+ - Pure comparison of two inspections by text, table position, list label,
325
+ merged review segments and comments, ignoring ids, dates and run boundaries.
326
+ Hosts that write a prepared package through their own API use it to verify
327
+ the committed result. Imports nothing.
328
+ - `document/docx-document.js`
329
+ - Universal transactional whole-DOCX editing, artifact wiring, validation, and rollback.
330
+ Standardized on `Uint8Array` binary payloads and pure-JS SHA-256 revision tokens.
331
+ Exposes `openDocx`, `DocxDocument`, and `computePackageRevisionToken` directly across all runtimes.
332
+ - `document/zip-archive.js`
333
+ - Universal, pure-JS ZIP archive reader and writer based on `fflate`. Operates on
334
+ `Uint8Array` byte arrays with zero Node built-ins (`node:zlib`, `Buffer`).
335
+ - `core/sha256.js`
336
+ - Pure-JS synchronous implementation of FIPS 180-4 SHA-256 with zero dependencies,
337
+ enabling deterministic revision tokens in sandboxed runtimes without `node:crypto`.
338
+ - `node/docx-document.js` and `node/zip-archive.js`
339
+ - Node-compatible re-exports maintaining `Buffer` compatibility for `@ansonlai/docx-redline-js/node`.
340
+ - `node/cli.js` and `bin/docx-redline.js`
341
+ - Cross-platform, JSON-only agent command boundary. Read commands never
342
+ mutate; write commands require attribution, use package transactions, and
343
+ only overwrite source files under explicit `--in-place` authorization.
344
+ Mutation commands expose a compact contract: package/XML payloads and full
345
+ validation arrays remain internal, while stdout contains durability fields,
346
+ per-operation evidence, validation counts, and a derived completion flag.
347
+ - `services/revision-fidelity-verifier.js`
348
+ - Post-apply invariant check built on `core/word-revision-semantics.js`.
349
+ `verifyRevisionFidelity` compares Reject All of the output with Reject All
350
+ of the savepoint (both reject every author's revisions) and reports
351
+ `REJECT_PARAGRAPH_COUNT`, `REJECT_PARAGRAPH_TEXT`, `REJECT_PARAGRAPH_PROPS`,
352
+ `REJECT_NUMBERING_REFS` and `REJECT_WRAPPERS`. Under accept-first
353
+ existing-revision policies the baseline is the accepted view, scoped to the
354
+ resolved target range when known (otherwise the changed region between the
355
+ unchanged prefix and suffix). `REJECT_NUMBERING_REFS`
356
+ (`verifyNumberingReferences`) also refuses a numId that the output binds or
357
+ Reject All restores when the source did not use it and no `w:num` ->
358
+ `w:abstractNum` -> `w:lvl` defines it in the numbering available to the
359
+ operation (numbering existence). `verifyUntrackedLocality` covers
360
+ `generateRedlines: false` single-target operations with `ACCEPT_NEIGHBORS`:
361
+ no paragraph outside the target may change in either view.
362
+ `verifyNoCreatedRevisions` adds `UNTRACKED_CREATED_REVISION`: an untracked
363
+ operation may not introduce a revision id that was not in the source
364
+ (fragments of a sliced source revision, same element, author and date, are
365
+ allowed). `verifyAcceptIntent` adds `ACCEPT_INTENT`: a tracked plain-text
366
+ `redline` with complete `modified` (single paragraph or `targetEnd` range)
367
+ must read as `modified` in the Accept All view of its target; accept-first
368
+ range targets use the exact accepted scope.
369
+ `buildRevisionFidelityError` produces the `REVISION_FIDELITY_VIOLATION`
370
+ payload (`violations[].explanation`, `source.sourceParagraphIndex`,
371
+ `recovery.retryGuidance`). Modes are `enforce` (default, refuse and roll
372
+ back), `warn` (apply and append a warning) and `off`, resolved by
373
+ `resolveRevisionFidelityMode`: explicit `verifyRevisionFidelity` option,
374
+ then the diagnostic `DOCX_REDLINE_REVISION_FIDELITY` environment variable,
375
+ then `enforce`. The view memo is `createFidelityViews(document)`: lazily
376
+ filled `accepted` / `rejected` projections, revision-id signatures and
377
+ numbering references for one DOM. The applier builds a memo for the savepoint
378
+ (taking over `session._fidelityViews` when it still describes the live
379
+ document) and one for the result; after a committed tracked or untracked
380
+ operation the result's memo is stored back in `session._fidelityViews` as
381
+ the next operation's baseline, and a refused operation restores the prior
382
+ memo, so each view is computed at most once per DOM.
383
+ - `orchestration/*`
384
+ - Route planning and list fallback orchestration utilities.
385
+
386
+ ## End-to-End Flow
387
+
388
+ 1. Caller imports from `index.js`.
389
+ 2. Caller configures XML provider/logger/defaults when needed via `adapters/*`.
390
+ 3. Caller invokes reconciliation APIs (`applyRedlineToOxml`, operation runner, ingestion/export helpers).
391
+ 4. A full-document batch parses once, captures its initial target snapshot,
392
+ compiles the operations against it (`operation-batch-compiler.js`: batch-start
393
+ source binding, localized replacements, overlap and list-format composition
394
+ checks, all before any mutation), and creates an operation savepoint before
395
+ each mutation.
396
+ 5. `engine/oxml-engine.js` routes each selected scope to format, table, list,
397
+ surgical, or reconstruction flows; successful scoped output is imported
398
+ into the live document.
399
+ 6. After each mutation the applier (`services/document-operation-applier.js`)
400
+ validates the generated OOXML against the savepoint, checks the localized
401
+ replacement intent, then runs the revision fidelity verifier (Reject All,
402
+ Accept All intent, neighbor, created-revision and numbering-existence
403
+ invariants); a violation in `enforce` mode restores the savepoint and
404
+ refuses the operation before anything is committed. Failed and no-op
405
+ operations restore their savepoint. A successful changed
406
+ batch serializes the full document once; atomic failures and all-no-op
407
+ batches return the exact input string without serialization.
408
+ 7. Pipeline/services return OOXML, optional package artifacts (`numberingXml`, comments payloads; numbering parts are merged by `mergeNumberingParts` and
409
+ checked by `validateNumberingIntegrity`), and non-breaking `status`/`error` fields where applicable.
410
+ 8. Optional revision/comment management transforms can accept/reject revisions, including move revisions, or delete comments by author.
411
+ 9. Caller writes resulting XML back to package/document boundaries.
412
+
413
+ ## Public Surfaces
414
+
415
+ - Primary: `index.js`
416
+ - Types: `index.d.ts`
417
+ - Stable XML operation runner: `@ansonlai/docx-redline-js/standalone-runner`
418
+ - Node-only DOCX facade: `@ansonlai/docx-redline-js/node`
419
+
420
+ Keep public exports centralized through `index.js`; deep imports are supported by
421
+ the package `exports` map for advanced consumers, but new public APIs should
422
+ still be re-exported from `index.js`.
423
+
424
+ ## Reliability Guardrails
425
+
426
+ - Create Word namespace elements with `createWordElement(xmlDoc, 'w:...')`.
427
+ Avoid direct `document.createElement('w:*')` or `createElementNS(NS_W, 'w:*')`
428
+ outside `core/word-xml.js`.
429
+ - Generate tracked-change metadata through `createRevisionMetadata(author)` so
430
+ `w:id`, `w:author`, and `w:date` stay consistent and document-unique.
431
+ - Seed revision IDs from parsed input with `seedRevisionIdsFromDocument(xmlDoc)`
432
+ before emitting new tracked changes.
433
+ - Treat revision metadata inside cloned content as identity-bearing state, not
434
+ ordinary formatting. When a run containing `w:rPrChange` is split or cloned,
435
+ preserve an existing revision ID on at most one output run and allocate a
436
+ fresh document-scoped ID for every additional copy.
437
+ - Use `containsTrackedChanges(xmlDoc)` and `getTrackedChangeAuthors(xmlDoc)`
438
+ to inspect existing revisions. By default (`existingRevisions: 'merge-same-author'`),
439
+ subsequent edits from the same author are merged against the pre-revision baseline
440
+ while revisions from different authors fail closed with `EXISTING_REVISIONS`.
441
+ Revised paragraphs containing comment anchors fail closed with
442
+ `COMMENTED_CONTENT_MERGE`; never discard anchors as a side effect of restoring
443
+ the pre-revision baseline.
444
+ Pass `existingRevisions: 'accept-all-first'` to normalize prior revisions explicitly.
445
+ - Every operation is checked after it is applied
446
+ (`services/revision-fidelity-verifier.js`, default `enforce`): Word's Reject
447
+ All of a tracked output must equal Word's Reject All of the source (`REJECT_*`),
448
+ a plain-text `redline` must read as its `modified` text under Accept All
449
+ (`ACCEPT_INTENT`), and an untracked edit must neither change other paragraphs
450
+ (`ACCEPT_NEIGHBORS`) nor create a revision (`UNTRACKED_CREATED_REVISION`). A
451
+ violation restores the operation savepoint and returns
452
+ `REVISION_FIDELITY_VIOLATION`; `warn` applies and warns, `off` is for
453
+ diagnosis only.
454
+ - Allocate, continue, merge and validate numbering only through
455
+ `services/numbering-allocation.js`. Never edit a shared `w:abstractNum` or
456
+ `w:num` in place, never delete a definition Reject All may need, and keep
457
+ `numPr` on tracked-deleted source paragraphs.
458
+ - Construct paragraph-mark `w:ins` / `w:del` only through
459
+ `engine/paragraph-mark-revisions.js`, and read current paragraph/run
460
+ properties only through `core/current-properties.js`. Two source-scanning
461
+ tests enforce this: `tests/paragraph_mark_single_site_tests.mjs` rejects
462
+ paragraph-mark revision construction (templates opening an `rPr` with
463
+ `ins`/`del`, DOM creation attached to an `rPr`, retired helper names) outside
464
+ the helper, and `tests/current_properties_usage_tests.mjs` rejects descendant
465
+ lookups of property names (`numPr`, `pStyle`, `b`, ...) outside the accessor.
466
+ Exceptions need an entry with a written reason in each test's allowlist.
467
+ - Do not write unknown `result.oxml` payloads directly into `word/document.xml`;
468
+ normalize with `extractReplacementNodesFromOoxml(...)` or use
469
+ `applyOperationToDocumentXml(...).documentXml` for full-document replacement.
470
+ - Run `validateRedlineOoxml(oxml)` on generated output before packaging it;
471
+ it reports structural invariant violations as `{ valid, issues }`.
472
+
473
+ ## Integration Contracts
474
+
475
+ ### Targeting and text fidelity
476
+
477
+ - Paragraph targeting and replacement have different contracts. Targeting may
478
+ use normalized text and fallback heuristics; replacement content is literal
479
+ and must preserve tabs, line breaks, non-breaking spaces, repeated spaces,
480
+ and boundary whitespace.
481
+ - Text extraction used for targeting represents the visible accepted view of
482
+ tracked content: insertions are visible and deletions are excluded.
483
+ - Paragraph indexes are transient integration references, not user-facing
484
+ document identifiers. Prefer paragraph IDs plus exact text where available.
485
+ - Ambiguous matches are unsafe for document mutation. New targeting surfaces
486
+ should report candidate matches or `AMBIGUOUS_TARGET` rather than silently
487
+ choosing the first paragraph.
488
+ - Do not remove operation-session savepoints merely to improve throughput.
489
+ Any replacement must prove that a thrown error or false no-op cannot leak a
490
+ partial DOM mutation or revision-ID allocation into later operations.
491
+
492
+ ### Structural paragraph-to-list replacement
493
+
494
+ - In WordprocessingML, `w:numId w:val="0"` suppresses numbering. It represents
495
+ no reusable list context and must never be returned by numbering allocation
496
+ or assigned to a generated list item. Generated lists use a positive `numId`
497
+ bound to a compatible `w:abstractNum` definition.
498
+ - Expanding one non-list paragraph into multiple list paragraphs is a block
499
+ replacement, not an inline replacement. The original content remains in its
500
+ own paragraph with a deleted paragraph mark; every replacement item occupies
501
+ its own paragraph with an inserted paragraph mark and inserted text. This is
502
+ required so Word can accept the edit as the new paragraphs or reject it as
503
+ the exact original paragraph without joining the old heading to item one.
504
+ - Inserted list runs selectively inherit source typography (`w:rFonts`, size,
505
+ language, and related script properties). Semantic emphasis such as bold,
506
+ underline, italic, or strike is emitted only when requested by the replacement
507
+ markup; heading emphasis must not leak into ordinary list body text.
508
+ - Low-level list output can carry a package-fragment sentinel needed by its
509
+ standalone payload shape. Full-document mutation removes that sentinel while
510
+ importing replacement nodes, so it cannot become an empty document paragraph.
511
+ - Tests for this route must verify positive numbering IDs, separate physical
512
+ paragraphs, formatting inheritance/non-inheritance, valid revision markup,
513
+ and exact accepted and rejected paragraph sequences.
514
+
515
+ ### Atomic mixed-content replacement
516
+
517
+ - `planStructuredReplacement(...)` converts agent-authored Markdown into one
518
+ replacement operation carrying `structuredContent: true`. The block plan is
519
+ diagnostic metadata; execution remains one mutation so replacing the anchor
520
+ cannot invalidate later blocks.
521
+ - Mixed-content parsing recognizes explicit Markdown headings, blank-line
522
+ paragraph boundaries, adjacent list items, and contiguous table rows. Tables
523
+ require a header separator and consistent column counts. Invalid input fails
524
+ with `STRUCTURED_CONTENT_INVALID`; silently degrading table pipes into visible
525
+ paragraph text is forbidden on this route.
526
+ - Newly inserted tables inside a mixed replacement are tracked once at block
527
+ scope. Cell runs are not nested in additional `w:ins` wrappers, preserving the
528
+ no-nested-revisions invariant while allowing Reject All to remove the table.
529
+ - Agents must use explicit heading markers and valid table grammar rather than
530
+ relying on capitalization or layout inference. The engine preserves the
531
+ declared block types and does not guess legal-document semantics.
532
+
533
+ ### Package artifacts and transactions
534
+
535
+ - A real `.docx` stores document markup, comments, numbering, relationships,
536
+ and content types in separate parts. APIs operating on one XML string cannot
537
+ claim to update artifacts held in another part.
538
+ - Comment IDs must be allocated against both document anchors and the existing
539
+ `word/comments.xml` part. Revision IDs must be allocated against the complete
540
+ document revision scope.
541
+ - Numbering payloads are merged, never replaced, by the single merge in
542
+ `services/numbering-allocation.js` (`mergeNumberingParts`), which both the
543
+ runner's consumers and the `openDocx` facade call; conflicting ids fail with
544
+ `NUMBERING_ID_CONFLICT`.
545
+ - Safe package mutation is transactional: retain the original package, apply
546
+ operations, merge all artifacts, validate redline markup and package wiring,
547
+ and commit only if every required check succeeds.
548
+ - `openDocx(buffer)` implements this transaction for Node without adding a ZIP
549
+ dependency to browser or XML-only consumers. Unmodified part contents remain
550
+ byte-identical after extraction, although the ZIP container is reserialized.
551
+
552
+ ### Operation results
553
+
554
+ - `hasChanges: false` does not imply success. Callers must check `status`,
555
+ `error`, and warnings to distinguish errors, missing anchors, and true no-ops.
556
+ - Batch results must preserve the caller's original operation indexes even when
557
+ execution is reordered for stable anchors.
558
+ - Author attribution is externally visible document data. Agent-facing APIs
559
+ should require an explicit author and report the author used for every
560
+ operation rather than relying silently on a configured fallback.
561
+ - Operation-level authors override the batch author. Runtime results expose
562
+ `authorUsed`, `authorsUsed`, `operationType`, `resolvedBy`, and resolved target
563
+ metadata so integrations can audit what the engine actually selected.
564
+ - Target fingerprints are revision-view scoped. Inspection returns a
565
+ `revisionView` beside every paragraph and computes its fingerprint from the
566
+ same text/view pair. Restore normalization defaults omitted target and range
567
+ endpoint views to `rejected`; all other operations default to `accepted`.
568
+ Explicit views remain authoritative, and cross-view text/fingerprint mistakes
569
+ return an actionable mismatch hint rather than silently weakening targeting.
570
+ - A normalized target match does not become an edit coordinate system for a
571
+ text-bearing mutation. Mutation uses canonical accepted-view source text, and `resolvedTarget.targetTextMatch`
572
+ records `exact`, `space_equivalent`, or `normalized` selection plus bounded
573
+ invisible-character diagnostics.
574
+ - The CLI contract (version 9; `docx-redline version` lists the capabilities) is deliberately narrower than library result objects.
575
+ `apply`, `accept`, `reject`, and `delete-comments` omit `documentXml`, package
576
+ parts, inspection text, and full issue arrays. `completion` is true only when
577
+ `written === true`, top-level status is neither error nor partial, and every
578
+ operation result is non-error. The `validate` command remains the full issue
579
+ reporting surface.
580
+ - `preflightOperations` is the read-only safety boundary for agent-generated
581
+ batches. It uses strict targeting by default; mutation APIs retain permissive
582
+ legacy targeting unless `strictTargets: true` is requested. In v1.0.0,
583
+ application will default to strict targeting; in the current warning cycle,
584
+ permissive resolution that chooses among multiple identical paragraphs emits
585
+ `AMBIGUOUS_TARGET_HEURISTIC_USED` with candidate count and migration guidance.
586
+ Both preflight and mutation runners share the same candidate resolver
587
+ (`resolveTargetParagraph`).
588
+
589
+ ### Mutation receipts and output reconciliation oracle
590
+
591
+ - `ReceiptCollector` tracks exact allocations (revision IDs with kind and target part,
592
+ comment IDs, numbering IDs, relationship IDs, affected targets, and warnings)
593
+ directly at the point of allocation/attachment during operation execution.
594
+ - Collector state is snapshotted within operation savepoints. Failed or rolled-back
595
+ operations cleanly restore prior collector state without leaking orphaned allocations.
596
+ - Single operations expose `result.receipt`; batches expose per-item `results[i].receipt`
597
+ and top-level `result.receipts`, with dispositions (`applied`, `refused`, `no_change`,
598
+ `rolled_back`, `not_attempted`) and `committed: true/false`.
599
+ - The output reconciliation oracle (`reconcileReceiptsAgainstOutput`) performs an
600
+ independent, non-negotiable verification: every durable ID reported as committed
601
+ is parsed from the serialized output XML parts (`word/document.xml`, `word/comments.xml`,
602
+ `word/numbering.xml`). Any discrepancy immediately fails the transaction and triggers
603
+ atomic rollback.
604
+ - The oracle checks that durable IDs are committed; the revision fidelity
605
+ verifier checks what Reject All would restore. A refused operation restores
606
+ its savepoint and receipt state like any other refusal, so it contributes no
607
+ committed IDs (`finalDisposition: 'refused'`).
608
+
609
+
610
+ ## Build Output
611
+
612
+ `npm run build` generates CDN-ready ESM bundles under `dist/`:
613
+
614
+ - `dist/docx-redline-js.esm.js`
615
+ - `dist/docx-redline-js.esm.js.map`
616
+ - `dist/docx-redline-js.esm.min.js`
617
+ - `dist/docx-redline-js.esm.min.js.map`
618
+
619
+ The bundle inlines `diff-match-patch` and keeps `@xmldom/xmldom` external.
620
+
621
+ ## Testing
622
+
623
+ - `npm test`
624
+ - Runs all `tests/*.mjs` except setup helpers in separate Node processes with
625
+ a conservative four-worker cap. Use `DOCX_TEST_CONCURRENCY=1` to reproduce
626
+ the same sorted suite serially.
627
+ - `npm run benchmark:tests`
628
+ - Compares paired serial and bounded-parallel suite runs and records JSON
629
+ under ignored `tmp/benchmarks/`.
630
+ - `npm run test:isolation`
631
+ - Runs boundary checks for Word API markers and dependency-graph isolation.
632
+ - `npm run check:types`
633
+ - Smoke-checks `index.d.ts`.
634
+ - `node scripts/export-validation-fixtures.mjs`
635
+ - Writes release-time validation fixtures to `tmp/validation-docx/` as
636
+ `word/document.xml` parts, assembled `.docx` files, and expected-text sidecars.
637
+ - `tests/roundtrip_fuzz_tests.mjs` (part of `npm test`)
638
+ - Seeded fuzz sweep of the accept/reject round-trip invariant; tune with
639
+ `FUZZ_SEED` / `FUZZ_ITERATIONS`.
640
+ - `npm run smoke:word -- path/to/file.docx`
641
+ - Optional Windows/Word COM smoke test for a completed `.docx`.
642
+ - `npm run smoke:word:diff`
643
+ - Windows/Word COM differential test: Word itself accepts/rejects the
644
+ generated fixtures and the resulting text is compared to expectations.
645
+ Structural checks also compare paragraph counts, and list cases compare
646
+ `ListFormat.ListString` labels after `Revisions.AcceptAll()` /
647
+ `RejectAll()`.
648
+ - `bash scripts/validate-fixtures-xsd.sh`
649
+ - Validates exported fixtures against the ECMA-376 transitional `wml.xsd`.
650
+ - `tests/reject_roundtrip_matrix_tests.mjs` (part of `npm test`)
651
+ - Generated operation matrix over fixtures and operation kinds, asserting the
652
+ revision fidelity invariants and receipt reconciliation (a
653
+ `RECEIPT_RECONCILIATION_FAILED` fails the case). Replay with `FUZZ_SEED` /
654
+ `MATRIX_ITERATIONS`.
655
+ - `.github/workflows/validation.yml`
656
+ - Nightly independent-oracle validation: XSD schema check, LibreOffice
657
+ conversion, an extended 20k-case fuzz sweep with a fresh seed, and the
658
+ `matrix-extended` job (1000 reject-roundtrip matrix cases with a
659
+ date-derived seed; a failure prints its seed and replay command).
660
+
661
+ Use these checks before publishing or tagging. See [docs/TESTING.md](./docs/TESTING.md#release-validation-and-independent-oracles).
662
+
663
+ ## Fast Orientation For Contributors
664
+
665
+ Use this sequence to understand or modify behavior without reading everything:
666
+
667
+ 1. Start at `index.js` to locate the exported API.
668
+ 2. Follow exports into `engine/oxml-engine.js` or relevant `services/*` module.
669
+ 3. For targeting bugs, inspect `core/paragraph-targeting.js`, `core/list-targeting.js`, and `core/table-targeting.js`.
670
+ 4. For package wiring issues, inspect `services/standalone-docx-plumbing.js`.
671
+ 5. For revision/comment cleanup behavior, inspect `services/revision-comment-management.js`.
672
+ 6. For numbering/list issues, inspect `services/numbering-allocation.js`,
673
+ `services/list-format*.js`, `services/list-conversion-diagnostics.js` and
674
+ orchestration list-fallback modules; for batch composition,
675
+ `services/operation-batch-compiler.js`; for Reject All refusals,
676
+ `services/revision-fidelity-verifier.js`.
677
+ 7. For reliability regressions, start with `tests/roundtrip_invariant_tests.mjs`,
678
+ `tests/engine_reliability_tests.mjs`, `tests/paragraph_mark_revision_tests.mjs`,
679
+ `tests/move_revision_tests.mjs`, and `tests/hardening_status_tests.mjs`.