@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/CHANGELOG.md CHANGED
@@ -1,5 +1,212 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0
4
+
5
+ ### Added
6
+
7
+ - **`existingRevisions: 'auto'`:** per target, `merge-same-author` when the target has no tracked changes or only the operation
8
+ author's, otherwise `slice-cross-author`. Unattributed changes count as another author's, and a `targetEnd` range resolves
9
+ over every paragraph in it. Accepted per operation, per batch, by `applyRedlineToOxml` and as `--existing-revisions auto`;
10
+ preflight reports the resolved policy. `resolveExistingRevisionsPolicy` is exported and accepts a node or an array of
11
+ paragraphs. CLI capability `existing-revisions-auto-v1`; the contract version stays 9.
12
+ - **`compareDocumentInspections(expected, actual)`** (and `inspectionReviewShape`): compare two inspections by text, table position,
13
+ list label, review segments and comments, ignoring ids, dates and run boundaries. For verifying what a host committed.
14
+
15
+ ### Fixed
16
+
17
+ - **Literal underscore runs are no longer Markdown emphasis:** `__x__` and `_x_` follow CommonMark flanking rules (not intraword,
18
+ not inside a longer underscore run, not around whitespace). Signature lines such as `_________________________` survive a
19
+ table reconciliation and a paragraph rewrite; `snake_case` stays literal.
20
+ - **`structuredContent: false` is plain text:** a caller's plain-text `redline`/`replace` keeps inline Markdown markers literally
21
+ instead of turning them into formatting. Internal plain-structure calls are unchanged.
22
+ - **Comment reply paraIds no longer collide with body paragraphs:** `applyCommentReplyToParts` (and the `comment_reply`
23
+ operation) now collects body `w14:paraId` values before allocating a reply's id. The scan's regex contained a literal
24
+ backspace instead of `\b`, so body ids were never seen.
25
+ - **Internal:** the Reject All verifier's numbering check uses its memoized definition parser (it was never wired in), and the
26
+ reject-roundtrip matrix's revision-identity cross-check and `RECEIPT_RECONCILIATION_FAILED` gate now actually run (the
27
+ same backspace-for-`\b` defect had made them no-ops).
28
+ - **Same-author checks count unattributed tracked changes as another author's:** a target holding the operation author's
29
+ revisions plus a change with no or a blank `w:author` passed as all same-author. `merge-same-author` then refused with
30
+ `UNSAFE_REVISION_NESTING` (`manual_resolution`) instead of `EXISTING_REVISIONS`, and `slice-cross-author` refused instead of
31
+ slicing; no revision was lost. Merge now refuses with `EXISTING_REVISIONS` (`revisionAuthors` lists named authors only; the
32
+ message adds `unattributed`), and slicing applies while Reject All still restores the unattributed change.
33
+
34
+ ## 0.9.0
35
+
36
+ ### Changed
37
+
38
+ - **Unknown `paragraph-format.properties` keys are rejected:** `INVALID_OPERATION` with `field: properties.<key>`, `properties[]`
39
+ and `recovery.action: change_request`; nothing is applied. The 0.8.x `UNKNOWN_PARAGRAPH_FORMAT_PROPERTY` warning no longer fires.
40
+ - **CLI contract version 9** (`list-format` operation type, capability `list-format-v1`). Integrations that do not send
41
+ `list-format` can keep requiring version 8.
42
+ - **`UNSUPPORTED_LIST_CONVERSION` / `LIST_MARKER_NOT_BOUND` now point at `list-format`:** `recovery.action` is
43
+ `use_list_format_convert_plain` (was `apply_plain_text_or_convert_in_host`) and, when the original paragraph has no marker,
44
+ `recovery.suggestedOperation` carries the `list-format` request to send with the same target (for "Header" -> "C. Header":
45
+ `{ mode: 'convert-plain', format: 'upperLetter', strip: 'none', start: 3 }`). `listMarkers: 'literal'` is unchanged.
46
+ - **`UNSUPPORTED_LIST_FORMAT_CHANGE` / `LIST_FORMAT_NOT_APPLIED` now point at `list-format` `change-format`:** `recovery.action`
47
+ is `use_list_format_change_format` (was `change_list_format_in_host`) and `recovery.suggestedOperation` carries
48
+ `{ type: 'list-format', list: { mode: 'change-format', format, level, scope: 'list' } }`; add a target on one item of the list.
49
+ `list-change` still never applies a format change itself.
50
+ - **A `list-format` with `scope: 'list'` counts as a write to every item it rebinds:** another write to one of those items in the
51
+ same batch (`paragraph-format`, `highlight`, `format`, a second `list-format` of the same list) now fails before mutation with
52
+ `LIST_FORMAT_COMPOSITION_CONFLICT` (`reason: list-rebind`); in WP-5 it ran in batch order. Send it in a separate apply or use
53
+ `scope: 'range'`. A single-paragraph `redline` of an item still composes (see Added).
54
+ - **Text-equal letter, roman and bullet header conversions are made in place:** a `redline` whose `modified` equals the plain
55
+ paragraph's typed-marker text (`B. Header` -> `B. Header`) now strips the marker as a tracked deletion and binds the list with a
56
+ tracked `w:pPrChange` in the same paragraph, like the decimal path. The previous whole-paragraph delete plus insert lost
57
+ `w14:paraId`, bookmarks and direct formatting and reviewed as a deleted and an inserted line; it remains only for shapes the
58
+ in-place conversion refuses. (It was reject-exact for plain runs but not for markers inside hyperlinks or smart tags, where the
59
+ Reject All verifier refuses it.)
60
+ - **Operations that would break Reject All are refused by default:** the revision fidelity verifier now runs in
61
+ `enforce` mode. A refused operation leaves the document unchanged and returns `REVISION_FIDELITY_VIOLATION` with a
62
+ one-line message naming the invariant, the source paragraph (extraction index and excerpt), expected vs actual, an
63
+ explanation and a retry instruction; `violations[]`, `invariants` and `recovery.retryGuidance` carry the same detail for
64
+ agents, the message is also sent to `onWarn`, and compact CLI output keeps it. Opt out per call with
65
+ `verifyRevisionFidelity: 'warn'` (apply and warn) or `'off'` (diagnosis). CLI: `--revision-fidelity <enforce|warn|off>`,
66
+ shown in `effectiveOptions`; the agent profile rejects `off`. Capability `revision-fidelity-v1`. A single-target untracked
67
+ operation (`generateRedlines: false`) is also refused when it changes a paragraph outside its target in the Accept All or
68
+ Reject All view (invariant `ACCEPT_NEIGHBORS`).
69
+ - **`ACCEPT_INTENT` and numbering-part existence in the revision fidelity check:** a tracked plain-text `redline` with complete
70
+ `modified` (no Markdown, list-marker or table syntax; single-paragraph targets and `targetEnd` ranges) is refused when Accept All of
71
+ its target would not read as `modified`. `REJECT_NUMBERING_REFS` also refuses a numId that the output binds or Reject All restores
72
+ (including inside `w:pPrChange`) when the source did not already use it and no `w:num` -> `w:abstractNum` -> `w:lvl` defines it in the
73
+ numbering available to the operation. Accept-first range targets now use the exact accepted scope.
74
+ - **`ensureNumberingArtifactsInZip` always merges numbering, and refuses id conflicts:** the optional `mergeNumberingXml`
75
+ callback is ignored (the single merge is `mergeNumberingParts` in `services/numbering-allocation.js`), an existing
76
+ `numbering.xml` is no longer replaced when no callback is given, and an incoming `w:abstractNum` / `w:num` whose id exists with
77
+ different content throws `NUMBERING_ID_CONFLICT` instead of silently binding new paragraphs to another list. `openDocx` also
78
+ rejects output that introduces a dangling `numId`, `abstractNumId` or `ilvl` (`NUMBERING_*` issues).
79
+ - **Receipt kinds:** a whole paragraph deleted by an empty `modified` reports its deleted paragraph mark with receipt kind `del`
80
+ (was `structural`); the explicit `1. Header` list conversion now receipts its `w:pPrChange`.
81
+ - **`paragraph-format` keeps the earliest same-author `w:pPrChange` snapshot** and refuses to overwrite another author's pending
82
+ snapshot (`PARAGRAPH_FORMAT_REVISION_CONFLICT`); see Added.
83
+
84
+ ### Added
85
+
86
+ - **`redline` + `list-format` on one paragraph in one batch:** a changed-text header (`1. Old` -> `1. New`) is a `redline` that keeps
87
+ the typed marker exactly (`modified: '1. New'` or `replacements`) plus a `list-format` `convert-plain` of the same paragraph. The
88
+ batch compiler allows exactly this pair (both bind at batch start) and fixes the order whatever the batch order: the redline runs
89
+ first, then the list-format strips the unchanged marker, so Accept All shows label `1.` with `New` and Reject All the literal
90
+ `1. Old` (verified in Word with `ListFormat.ListString`, including a `3.` start value). A single-paragraph `redline` of a list item
91
+ also composes with a `change-format`/`join` that rebinds it. Incompatible pairs are refused before mutation with
92
+ `LIST_FORMAT_COMPOSITION_CONFLICT` (`reason`: `marker-changed` with `sourceMarker`/`modifiedMarker`, `redline-unchanged`,
93
+ `redline-shape`; `recovery.action: split_text_and_list_changes`); other kinds keep `OVERLAPPING_SOURCE_TARGETS` /
94
+ `REVISION_ORDER_CONFLICT`. No new capability: it is part of `list-format-v1` behavior.
95
+ - **`list-format` `change-format` and `join` modes (`list-format-change-v1`, contract version stays 9):** `change-format` changes
96
+ the kind or numbering format of an existing list (`format`, `lvlText`, `start`, `level`, default the target's level). It never
97
+ edits the shared definition: a NEW `w:abstractNum` + `w:num` is allocated with the source levels, indentation and fonts copied
98
+ and one level redefined (`w:isLgl`, picture bullets and a bullet's symbol font are dropped on a format change), and each item is
99
+ rebound with a tracked `w:pPrChange`, so Reject All restores the original labels and Accept All shows the new ones (verified in
100
+ Word with `ListFormat.ListString` after `Revisions.AcceptAll()`/`RejectAll()`: bullets to `1.`, `1.` to `A.`, a nested level to
101
+ `(i)`, and two range splits). `scope: 'list'` (default) rebinds every live item of the list (`items[].inTarget` marks those outside
102
+ the target); `scope: 'range'` rebinds a `target`..`targetEnd` range that runs to the end of the list and continues its count
103
+ (start values on the new definition, including a split that starts at a nested item). `join` binds the target list (or a range
104
+ running to its end) to the `numId` of `list.continueFrom`, an item of an earlier list, so it continues that count; `continueFrom` is
105
+ bound at batch start. Tracked-deleted paragraphs keep their `w:numPr`; a same-author earlier `w:pPrChange` snapshot is kept;
106
+ later operations in a batch see definitions earlier ones allocated. Refused before any write: `LIST_FORMAT_TARGET_NOT_LIST`,
107
+ `LIST_FORMAT_MIXED_RANGE` (plain paragraphs, several lists, or several levels without `list.level`), `LIST_FORMAT_STYLE_LINKED`
108
+ (heading/list-style numbering), `LIST_FORMAT_STYLE_NUMBERED`, `LIST_FORMAT_SPLIT_UNSUPPORTED` (`mid-sequence` range,
109
+ `nested-restart`, `shared-definition` abstract used by another list, `interleaved` join), `LIST_FORMAT_DEFINITION_UNSUPPORTED`
110
+ (missing definition, `w:lvlOverride/w:lvl`, `w:lvlRestart` in a split), `LIST_FORMAT_JOIN_INVALID` and
111
+ `PARAGRAPH_FORMAT_REVISION_CONFLICT`; an untracked rebinding outside the target is refused; a no-op is `ok` with a
112
+ `LIST_FORMAT_UNCHANGED` warning. Results carry `list` (`sourceNumId`, `numId`, `items[]`, `levelStarts` or `continueFrom`).
113
+ An older CLI refuses the new modes with `INVALID_OPERATION` (`field: list.mode`), so negotiate the capability.
114
+ - **`list-format` operation, `convert-plain` mode (`list-format-v1`):** binds plain paragraphs (a target, or `target`..`targetEnd`
115
+ in one container) whose text starts with a typed marker (`1.`/`1)`, `A.`/`a)`, `IV.`/`iv.`, `-`/`•`) to a NEW list definition in
116
+ place. The marker is removed with a tracked `w:del` of exactly the marker text (`strip: 'marker'`, default) and the `w:numPr`
117
+ binding is a tracked `w:pPrChange`, so Reject All restores the literal marker and the unnumbered paragraph and Accept All shows
118
+ the list label (verified in Word: `ListFormat.ListString` after `Revisions.AcceptAll()`/`RejectAll()` for `A.`, `1.`, `I.` and a
119
+ `C.` start value). Each marker's value is the item's value (`startOverride` on the new `w:num`; the label keeps the typed `.`/`)`);
120
+ consecutive values share one list, a gap starts a new list at the typed value, body paragraphs in the range stay plain, and a
121
+ later `list-format` in the same batch whose first value continues the previous list joins it. `strip: 'none'` with `format`
122
+ (and optional `start`) binds without touching the text. Ambiguous or unsupported shapes are refused with recovery envelopes:
123
+ `LIST_MARKER_NOT_FOUND`, `LIST_MARKER_AMBIGUOUS` (a lone `I.`/`C.` reads as letter or roman; set `list.format`),
124
+ `LIST_MARKER_UNSUPPORTED` (mixed markers, `2.1.`, `(a)`, a marker inside a hyperlink/field/revision, format mismatch),
125
+ `LIST_FORMAT_TARGET_IS_LIST`, `LIST_FORMAT_STYLE_NUMBERED` (style-linked numbering; needs `stylesXml` on the bare runner, read from
126
+ the package by `openDocx`) and `PARAGRAPH_FORMAT_REVISION_CONFLICT`. Body only. Results carry `list` (`format`, `items[]` with
127
+ `numId`/`value`, `skipped[]`). Schema `$defs.listFormat`, `ListFormatDocumentOperation` in `index.d.ts`.
128
+ - **`paragraph-format.numbering` binding (`paragraph-numbering-binding-v1`, no contract bump):** `properties.numbering` is
129
+ `{ numId, level? }` (bind the paragraph to an existing list) or `null` (remove its direct list binding), recorded as a tracked
130
+ `w:pPrChange` so Reject All restores the previous `w:numPr` exactly and Accept All shows the new one (verified in Word with
131
+ `Document.Revisions.RejectAll()/AcceptAll()` and `ListFormat.ListString`). `w:numPr` is inserted at its CT_PPr position
132
+ (after `widowControl`, before `suppressLineNumbers`/`spacing`/`ind`; `ilvl` before `numId`). A binding never allocates: an unknown
133
+ `numId` or undefined level is refused `PARAGRAPH_NUMBERING_NOT_FOUND`. `paragraph-format` now keeps the earliest `w:pPrChange`
134
+ snapshot for repeated same-author edits (it used to replace it, so Reject All restored the intermediate state) and refuses to
135
+ overwrite another author's pending snapshot (`PARAGRAPH_FORMAT_REVISION_CONFLICT`). Letter and roman literal-marker headers
136
+ (`C.`, `iv.`) now convert with their start value instead of restarting at the first item.
137
+ - **List conversion diagnostics (`list-conversion-diagnostics-v1`, no contract bump):** text edits no longer report a silent `ok`
138
+ when they cannot create list structure. A plain paragraph whose new text starts a list marker returns the warning
139
+ `LIST_MARKER_NOT_BOUND` (`diagnostics[]` on the operation result, `CODE: message` in `warnings`), or `UNSUPPORTED_LIST_CONVERSION`
140
+ under the strict policy (`--profile agent`, `--atomic`, or `strictListConversion: true`). A list item whose retained marker asks for a
141
+ different kind or format than its numbering definition returns `LIST_FORMAT_NOT_APPLIED` / `UNSUPPORTED_LIST_FORMAT_CHANGE`.
142
+ `listMarkers: 'literal'` on an operation opts out. `paragraph-format.properties` is now a closed schema; unknown keys (such as
143
+ `spacingAfter`) first produced an `UNKNOWN_PARAGRAPH_FORMAT_PROPERTY` warning (superseded in 0.9.0, see Changed).
144
+ Independent of `REVISION_FIDELITY_VIOLATION`; compact CLI output keeps `diagnostics` and the error fields.
145
+
146
+ - **Numbering allocator:** new `services/numbering-allocation.js` (exported from the package root): `allocateNumberingDefinition`
147
+ (new abstractNum + num with fresh ids, copied level geometry, per-level `startOverride`), `allocateNumberingForReplacement`
148
+ (same-kind source-list continuation or fresh allocation; tracked-deleted source paragraphs never change `numPr`),
149
+ `mergeNumberingParts` / `mergeNumberingXmlBySchemaOrder` (now with an `onConflict` option), `canonicalNumberingDefinition` and
150
+ `validateNumberingIntegrity`. The standalone runner's consumers and the `openDocx` facade merge through the same function;
151
+ `reuseSourceListNumbering` is replaced by allocator policy.
152
+ - **Revision fidelity verifier:** every tracked operation now checks, on the live DOM and without extra
153
+ serialization, that Word's Reject All of its output equals Word's Reject All of the source (paragraph count and order,
154
+ canonical text including breaks and tabs, paragraph properties, `numPr`, hyperlink/field/content-control membership).
155
+ A mismatch refuses the operation (see Changed); in `warn` mode it is applied with the message in `warnings` and
156
+ `revisionFidelityViolations`. The Word-semantics simulator is `core/word-revision-semantics.js`, validated against
157
+ recorded Word 16 Reject All / Accept All results (`tests/fixtures/word-revision-semantics/expectations.json`).
158
+
159
+ - **`UNTRACKED_CREATED_REVISION` invariant:** an operation with `generateRedlines: false` is refused (default `enforce`) when its
160
+ output contains a revision id (`w:ins`, `w:del`, moves, `*PrChange`, paragraph-mark and row revisions) that the source did not
161
+ have. A foreign revision sliced around the edit keeps its element, author and date and is not counted. It reports as
162
+ `REVISION_FIDELITY_VIOLATION` with `violations[].invariant: 'UNTRACKED_CREATED_REVISION'`, also for range targets.
163
+
164
+ ### Internal
165
+
166
+ - **Revision fidelity verifier cost cut from about +39% to about +8% per batch:** each verified operation's projections (accepted and rejected views, revision-id signatures, numbering references) are reused as the next operation's baseline, numbering definitions are parsed once per part, and the revision-view projection no longer builds live node lists. No check was weakened.
167
+ - **One paragraph-mark revision helper:** `engine/paragraph-mark-revisions.js` is now the only module that constructs a tracked paragraph mark (`w:ins`/`w:del` under `w:pPr/w:rPr`): `markParagraphInserted`, `markParagraphDeleted`, `setParagraphPropertiesMarkRevision`, `restoreParagraphMarkInserted`, string builders `paragraphMarkRevisionXml` / `paragraphMarkPPrXml`, and `planParagraphMarks` (asserts the Reject All paragraph-count invariants). Document-level shortcuts, list generation, reconstruction, text-to-table and generated tables use it, and `tests/paragraph_mark_single_site_tests.mjs` fails on any other construction site. Output is unchanged except that a new mark `rPr` is placed before `sectPr`/`pPrChange`, an existing mark is replaced rather than duplicated, and the empty-modified shortcut's deleted-mark receipt kind is `del` (was `structural`).
168
+
169
+ ### Fixed
170
+
171
+ - **A `targetEnd` given as a compact handle (`paragraphId` + `fingerprint`, no numeric `index`) was silently ignored** for a
172
+ plain `redline`, so only the first paragraph of the range was replaced and the rest stayed in the accepted text. The range is now
173
+ resolved from the end handle; a range that crosses a table or other container boundary is refused `TARGET_RANGE_INVALID`.
174
+ - **Range replace over empty paragraphs no longer fails receipt reconciliation:** a replacement whose diff deletes only paragraph
175
+ breaks recorded a paired deletion revision id in the receipt that was never written, so the operation was refused with
176
+ `RECEIPT_RECONCILIATION_FAILED`. Paired replacement ids are now recorded only when written. The reject-roundtrip matrix
177
+ treats that code as a failure, not a skipped refusal.
178
+ - **Plain `redline` keeps `w14:paraId` / `w14:textId`:** reconstruction used to rebuild the paragraph without its identity
179
+ attributes; the rebuilt paragraph that survives for a source paragraph (not tracked-deleted) now keeps them, once (an inserted
180
+ split gives them to the paragraph whose mark Reject All keeps). A composed `redline` + `list-format` pair keeps identity.
181
+ - **Text-changed header conversion with direct `keepNext` / spacing is no longer refused `REJECT_PARAGRAPH_PROPS`:** the list
182
+ fallback's tracked-deleted source paragraph keeps its original `pPr`; the new list paragraph carries the direct properties and
183
+ the source `w14:paraId`.
184
+ - **`ReconciliationPipeline.executeTableGeneration` inserts a tracked table block:** the public pipeline method generated
185
+ tracked cell text in untracked rows (the defect `engine/table-mode.js` had); rows now carry `w:trPr/w:ins` so Reject All
186
+ removes the table.
187
+ - **Separate list conversions in one batch continue one list:** two `redline` operations that turned non-adjacent headers into continuing markers (`A. Header`, `B. Header`) each allocated their own numbering instance, so Word showed `A.`, `B.` (start values) but as two lists, and a later item inserted between them would not renumber. The structural list fallback now uses the batch sequence state `list-format convert-plain` uses: same kind, format and level with a continuing value reuses the previous operation's live `numId` (a `redline` and a `convert-plain` of the same format join one list too); a gap (`A.` then `C.`) or a different format allocates a new instance. Ids still come from `services/numbering-allocation.js`. (Reported against 0.8.4.)
188
+ - **Weak-target localized replacements no longer delete soft-break paragraphs:** a `replacements` edit targeted only by `index` + `exactText` (no `paragraphId`/`fingerprint`) in a paragraph containing `<w:br/>` returned `ok` but tracked the whole paragraph as deleted and inserted nothing, because the batch compiler treated the line break in `exactText` as a multi-paragraph target and skipped replacement compilation. Localized replacements now always bind at batch start, so weak and strong targets behave identically. Every localized replacement also has its accepted result checked against the compiled text before commit (`PATCH_ROUNDTRIP_MISMATCH`, nothing applied), and uncompiled replacements are refused rather than applied.
189
+ - **Reconstruction keeps wrappers and deleted placeholders:** a text edit in a paragraph with a `w:fldSimple` no longer drops the field (Reject All lost its result text); a deleted manual break, tab or inline object is a tracked deletion instead of disappearing; and converting a paragraph to a list keeps hyperlink and content-control containers around the deleted runs, so Reject All keeps the text inside its link. Found by the reject-roundtrip matrix.
190
+ - **Punctuation edits stay surgical:** leading and trailing punctuation are separate diff tokens and unchanged substantive words anchor the re-diff, so "agents and attorneys" -> "agents, attorneys, accountants and other professional advisors" marks only " and" -> "," and the appended items.
191
+ - **Table reconciliation keeps untouched cell paragraphs:** editing one cell of a Markdown table reconciliation collapsed a multi-paragraph cell (for example `LS` plus a trailing empty paragraph) to one paragraph, untracked, so Reject All lost a paragraph. Cells whose only difference is trailing empty paragraphs are now unchanged, and a modified multi-paragraph cell is reconciled paragraph by paragraph (inserted paragraphs carry tracked marks).
192
+ - **Text-to-table redlines insert tracked table rows:** a paragraph range converted to a Markdown table carried tracked cell text but untracked rows, so Reject All left an empty table behind. Rows now carry `w:trPr/w:ins` and tracked cell paragraph marks like other inserted tables.
193
+ - **Explicit-start list conversion is reversible with Reject All:** converting a `1. Header` paragraph to a list bound `numPr` without recording a `w:pPrChange`, so rejecting the tracked deletion of the marker left a numbered item. The binding now records the original paragraph properties (empty `<w:pPr/>` when none) in a receipted `w:pPrChange`.
194
+ - **Reject/Accept All match Word for table cells and `pPrChange`:** rejecting an inserted paragraph mark keeps the cell's last paragraph (empty) instead of dropping it, accepting a fully deleted sole-paragraph table removes the table, and rejecting a `pPrChange` keeps the paragraph-mark `rPr` and `sectPr` instead of discarding them.
195
+ - **Property readers ignore `pPrChange`/`rPrChange` snapshots:** a new `core/current-properties.js` accessor reads only direct `w:pPr`/`w:rPr` children, so a tracked property change no longer makes its historical `numPr`, `pStyle`, `outlineLvl` or `b`/`i` look current. Markdown export, numbering-context detection, list-numbering reuse/overwrite, list-shape checks and captured-entity kind now use it; a usage test fails on new descendant property lookups.
196
+ - **Explicit `1. Header` list conversion works on a bare `document.xml`:** with no numbering part the batch had no numbering id state, so the conversion aborted after recording tracked deletions in the receipt and failed with `RECEIPT_RECONCILIATION_FAILED`. The bare runner now always seeds numbering ids as `openDocx` does, and an aborted conversion no longer records receipt revisions.
197
+ - **`generateRedlines: false` stays untracked under `slice-cross-author`:** the surgical insert computed its nested-revision flag as
198
+ `!(generateRedlines && ...)`, which is true whenever `generateRedlines` is false, so an inserted word was wrapped in a new tracked
199
+ `w:ins` (`applyRedlineToOxml` and `applyOperationsToDocumentXml`; `redline`/`replace`, `modified`/`replacements`). It now writes
200
+ plain runs, like `merge-same-author`, and leaves other authors' revisions intact (a foreign insertion is sliced around the edit).
201
+ - **Table appended after the final paragraph of a pretty-printed document keeps its tracked trailing paragraph:** a whitespace
202
+ text node between the last paragraph and `w:sectPr` made the retained-paragraph table append believe the table did not end
203
+ the body, so the body ended with a table and Word added an untracked empty paragraph that Reject All left behind (plugin
204
+ report R5; Word differential 5 vs 4 paragraphs). The append now skips non-element nodes when deciding. The Word-semantics
205
+ simulator (and therefore the post-apply verifier) counts the untracked paragraph Word supplies after a body-final table, so
206
+ the old output is refused with `REVISION_FIDELITY_VIOLATION`. Documents without whitespace between blocks (the plugin's
207
+ own) were already correct on `Revisions.RejectAll()` and the ribbon; `Document.RejectAllRevisions()` still leaves the
208
+ tracked-inserted last mark behind (a Word API quirk, see the fidelity plan).
209
+
3
210
  ## 0.8.4
4
211
 
5
212
  ### Added
@@ -71,7 +278,7 @@
71
278
  ### Known limitations
72
279
 
73
280
  - Plain paragraph to list conversion and list format changes are not yet supported canonically; see
74
- `docs/plans/2026-09-30-canonical-list-operations.md`.
281
+ `docs/plans/completed/2026-09-30-canonical-list-operations.md`.
75
282
  - `1. Header` to `1. Header` on a bare `document.xml` (not a packaged `.docx`) can fail with `RECEIPT_RECONCILIATION_FAILED`.
76
283
 
77
284
  ## 0.8.2
package/README.md CHANGED
@@ -15,14 +15,25 @@ Converts AI-generated or programmatic text/markdown edits into valid Office Open
15
15
  - Comment management: delete comments by author or for all authors
16
16
  - Highlights: apply highlight colors to runs
17
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
18
19
  - Status/error result fields for parse, targeting, and existing-revision failures
19
20
  - Localized exact replacements against inspected targets, plus optional fail-closed speculative CLI edits
20
21
  - Package plumbing helpers for numbering.xml, comments.xml, content types, and relationships
21
22
  - Zero host dependencies: works in Node.js, browsers, Deno, and similar JS runtimes with DOM parsing support
22
23
 
23
- ## What's New in 0.8.x
24
+ ## What's New in 0.10.0, 0.9.0 and 0.8.x
24
25
 
25
- The current release is 0.8.4. The CLI contract remains version 8 throughout the 0.8 line.
26
+ 0.10.0 adds `existingRevisions: 'auto'` (capability `existing-revisions-auto-v1`, contract version stays 9), which
27
+ merges a target's own-author history and slices around another reviewer's, per target. It also adds
28
+ `compareDocumentInspections` for checking a committed document against a prepared one, and fixes underscore runs such as
29
+ signature lines (`_____`) collapsing to one `_`. A caller's `structuredContent: false` now keeps inline Markdown markers
30
+ literal. See the [0.10.0 release notes](./docs/releases/0.10.0.md).
31
+
32
+ 0.9.0 moves the CLI contract to version 9 and adds the `list-format` operation (`convert-plain`, `change-format`, `join`),
33
+ `paragraph-format` numbering binding, list-conversion diagnostics, a single numbering allocator and merge, and a Reject All
34
+ revision fidelity verifier that refuses by default (`REVISION_FIDELITY_VIOLATION`). See the
35
+ [0.9.0 release notes](./docs/releases/0.9.0.md) and the [CHANGELOG](./CHANGELOG.md). The CLI contract was version 8
36
+ throughout the 0.8 line.
26
37
 
27
38
  - **0.8.0, universal document facade:** `openDocx` and `DocxDocument` are exported from the primary entry point and work
28
39
  on `Uint8Array` input in any JavaScript runtime. ZIP handling uses `fflate` and SHA-256 is pure JavaScript, so there are no
@@ -287,7 +298,7 @@ docx-redline preflight contract.docx --operations operations.json --author "Edit
287
298
  All commands emit JSON on stdout. `apply` defaults:
288
299
  - **Author**: Defaults to `'AI Redliner'` (or `DOCX_REDLINE_AUTHOR` environment variable).
289
300
  - **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.
290
- - **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.
301
+ - **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, or `--existing-revisions auto` to choose per target (slicing only where another reviewer's changes are present).
291
302
  - **Transactionality**: Defaults to `atomic: false` (applies valid operations and reports any failures). Pass `--atomic` for all-or-nothing rollback on any operation error.
292
303
  - **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`.
293
304
  - **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`.
@@ -311,7 +322,9 @@ marker. Machine `index` and `ref` fields are for targeting and pagination, not
311
322
  user-facing Word locations; use `humanReference`, `provision`, or
312
323
  `nearestHeading` in reports.
313
324
 
314
- `docx-redline version` reports contract version 8 and the additive
325
+ `docx-redline version` reports contract version 9 (adds the `list-format` operation type). Contract 9 adds the
326
+ capabilities `revision-fidelity-v1`, `list-conversion-diagnostics-v1`, `paragraph-numbering-binding-v1`,
327
+ `list-format-v1` (`convert-plain`) and `list-format-change-v1` (`change-format` and `join`), alongside the additive
315
328
  `command-help-v1`, `inspection-context-v1`, `bounded-inspection-v1`,
316
329
  `human-document-references-v1`, `batch-start-source-binding`,
317
330
  `recovery-envelope-v1`, `require-complete-exit`, `operations-stdin`,
@@ -320,7 +333,10 @@ user-facing Word locations; use `humanReference`, `provision`, or
320
333
  `speculative-search-apply-v1`, `localized-change-summary-v1`, and
321
334
  `restore-shortcuts-v1`, plus `localized-space-equivalence-v1` capabilities. Header/footer
322
335
  `part` targeting, the `comment_resolve` operation and `delete-comments --comment-id` are advertised as
323
- `header-footer-parts-v1`, `comment-resolve-operation-v1` and `delete-comments-by-id-v1`.
336
+ `header-footer-parts-v1`, `comment-resolve-operation-v1` and `delete-comments-by-id-v1`. The remaining advertised
337
+ capabilities are `atomic-batch-results-on-package-failure`, `baseline-aware-validation`, `compact-mutation-results`,
338
+ `cross-author-revision-slicing`, `document-scoped-list-revision-ids` and `format-text-occurrence-v1`. 0.10.0 adds
339
+ `existing-revisions-auto-v1` (`existingRevisions: 'auto'`) without a contract bump.
324
340
  Wrappers should negotiate only the
325
341
  capabilities they use. Run `docx-redline <command> --help` for that command's
326
342
  machine-readable options, behavior, exit codes, canonical GitHub documentation links, and compact examples.
@@ -366,11 +382,14 @@ See the [compact agent fast start](./docs/AGENT_FAST_START.md), the
366
382
  | `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. |
367
383
  | `author` | `string` | `'AI Redliner'` | Reviewer/author name stamped on generated tracked changes and comments. Overridable via `DOCX_REDLINE_AUTHOR` env variable. |
368
384
  | `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. |
369
- | `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. |
385
+ | `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: inline Markdown markers (`**`, `__`, `_`, `~~`, `++`) are kept literally. With `true`, underscores follow CommonMark flanking rules, so signature lines (`_____`) and `snake_case` stay literal. |
370
386
  | `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. |
371
387
  | `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. |
372
- | `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. |
388
+ | `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. `'auto'` chooses per target: `'merge-same-author'` when the target has no tracked changes or only the operation author's, otherwise `'slice-cross-author'` (preflight reports the resolved policy). |
373
389
  | `removeFormatting` | `boolean` | `false` | When `true` and the text is unchanged with no Markdown hints, strips existing bold/italic/underline/strikethrough formatting. |
390
+ | `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`. |
391
+ | `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. |
392
+ | `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. |
374
393
  | `sanitizeInput` | `boolean` | `false` | Opt-in removal of standalone leading assistant-preface lines. Literal dollar signs and `\n` sequences are always preserved. |
375
394
 
376
395
  Same-author revision merging refuses paragraphs containing comment anchors with
@@ -410,6 +429,19 @@ The engine applies Microsoft Word Desktop-native tracked change structures:
410
429
  - **Straddle deletions**: Deletions spanning between baseline text and pending insertions cleanly partition across their respective container contexts without invalid coalescing.
411
430
  - **Lifecycle parity**: Accepting or rejecting either reviewer independently produces identical results to Microsoft Word Desktop's native review pane.
412
431
 
432
+ ### Comparing inspections
433
+
434
+ `compareDocumentInspections(expected, actual)` checks that two inspections describe the same reviewable document. It compares paragraph text, table position, list labels, review segments (adjacent segments of one kind and author are merged) and comments, and ignores ids, dates, rsids, paragraph ids and run boundaries. A host that writes a prepared package through another API can inspect what it prepared and what it committed, then compare them:
435
+
436
+ ```js
437
+ import { inspectDocumentParts, compareDocumentInspections } from '@ansonlai/docx-redline-js';
438
+
439
+ const check = compareDocumentInspections(inspectDocumentParts(preparedParts), inspectDocumentParts(committedParts));
440
+ if (!check.matches) console.warn(check.differences); // at most the first 10 differences
441
+ ```
442
+
443
+ `inspectionReviewShape(inspection)` returns the normalized shape the comparison uses.
444
+
413
445
  ### Replacing a heading with a tracked list
414
446
 
415
447
  The list route treats a one-paragraph heading expanded into multiple markdown
@@ -437,6 +469,58 @@ for it. For a complete `word/document.xml`, prefer
437
469
  `applyOperationToDocumentXml(...)` so replacement nodes and numbering artifacts
438
470
  are imported at the correct scope.
439
471
 
472
+ ### List operations (`list-format`)
473
+
474
+ Text edits never create or restyle list structure. Typed markers such as `1.` or `A.` stay literal text, and a
475
+ `list-change` that retypes markers keeps the old numbering (it warns `LIST_FORMAT_NOT_APPLIED`, or fails
476
+ `UNSUPPORTED_LIST_FORMAT_CHANGE` under the strict policy, with a `list-format` request in `recovery.suggestedOperation`).
477
+ Structure changes use the `list-format` operation. Every mode records each paragraph's previous `w:numPr` in a tracked
478
+ `w:pPrChange`, so Reject All restores the original labels exactly and Accept All shows the new ones. The shared numbering
479
+ definitions in `numbering.xml` are never edited in place: new definitions come from one allocator and are merged by one numbering
480
+ merge, and an id that already exists with different content fails with `NUMBERING_ID_CONFLICT`. `paragraph-format`
481
+ `properties.numbering` binds or removes a list on a paragraph the same way (tracked `w:pPrChange`). Unknown
482
+ `paragraph-format.properties` keys are rejected with `INVALID_OPERATION`. The CLI advertises `list-format-v1` (contract version 9) for
483
+ `convert-plain` and `list-format-change-v1` for `change-format` and `join`.
484
+
485
+ ```js
486
+ const doc = openDocx(bytes);
487
+ const result = await doc.applyOperations([
488
+ // Typed headers "A. Scope" .. "C. Fees" become a real A./B./C. list. Each marker is a tracked deletion, and body
489
+ // paragraphs in the range stay plain. A typed start value ("C.") is kept.
490
+ { type: 'list-format', target: { exactText: 'A. Scope' }, targetEnd: { exactText: 'C. Fees' },
491
+ list: { mode: 'convert-plain' } },
492
+ // Bullets become 1., 2., 3. A new definition copies the list's levels and indentation.
493
+ { type: 'list-format', target: { exactText: 'Photo identification' },
494
+ list: { mode: 'change-format', format: 'decimal' } },
495
+ // A later list continues an earlier one (1. 2. / 1. 2. becomes 1. 2. / 3. 4.).
496
+ { type: 'list-format', target: { exactText: 'Confidentiality' },
497
+ list: { mode: 'join', continueFrom: { exactText: 'Termination' } } }
498
+ ], { author: 'Editor' });
499
+ ```
500
+
501
+ A changed-text header (`1. Old Terms` -> `1. New Terms`) takes two operations on the same paragraph in one batch: a
502
+ `redline` of the text that keeps the typed marker exactly, and a `list-format` that converts the paragraph. The batch
503
+ compiler allows this pair and always runs the redline first, whatever the batch order. The list-format then strips the
504
+ unchanged marker. Accept All shows label `1.` with `New Terms`, and Reject All shows the literal `1. Old Terms`:
505
+
506
+ ```js
507
+ await doc.applyOperations([
508
+ { type: 'redline', target: { exactText: '1. Old Terms' }, replacements: [{ find: 'Old', replace: 'New' }] },
509
+ { type: 'list-format', target: { exactText: '1. Old Terms' }, list: { mode: 'convert-plain' } }
510
+ ], { author: 'Editor' });
511
+ ```
512
+
513
+ A redline in that pair that adds, removes or changes the marker is refused before anything is written, with
514
+ `LIST_FORMAT_COMPOSITION_CONFLICT` (`reason: 'marker-changed'`). So is a redline that spans paragraphs or leaves the text
515
+ unchanged. To give a paragraph without a typed marker a label, use `strip: 'none'` with `format` (and `start`). A
516
+ `change-format` or `join` with `scope: 'list'` (the default) also writes every other item of the list. In the same batch,
517
+ each of those items accepts only a single-paragraph `redline`; any other write to them fails with
518
+ `LIST_FORMAT_COMPOSITION_CONFLICT` (`reason: 'list-rebind'`).
519
+ Style-linked lists (heading numbering), mid-list splits, and markers inside hyperlinks or fields are refused with a
520
+ recovery envelope. They are not approximated. Removing a list binding is `paragraph-format` with `properties.numbering:
521
+ null`. A `list-format` `detach` mode is not available yet. See the
522
+ [knowledge base](docs/AGENT_KNOWLEDGE_BASE.md#operation-model-choose-by-output-shape) for every mode, field and refusal.
523
+
440
524
  ### Planning large mixed-content insertions
441
525
 
442
526
  For an attachment or schedule containing several kinds of content, use
@@ -496,6 +580,7 @@ an earlier block has already replaced.
496
580
  | `analyzeStructuredContent(markdown)` | Decompose mixed Markdown into typed blocks and report malformed table syntax without creating an operation. |
497
581
  | `planStructuredReplacement(target, markdown, options)` | Validate mixed content and return one atomic `structuredContent` replacement operation, or `operation: null` with issues. |
498
582
  | `containsTrackedChanges(xmlDoc)` | Detect `w:ins`, `w:del`, move revisions, property changes, and paragraph-mark revision markup in a parsed OOXML document/fragment. |
583
+ | `resolveExistingRevisionsPolicy(policy, node, author)` | Resolve `existingRevisions: 'auto'` for a parsed paragraph, scope element, or an array of range paragraphs: `'slice-cross-author'` when any tracked change is by another or an unattributed author, otherwise `'merge-same-author'`. Other policies are returned unchanged. |
499
584
  | `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. |
500
585
 
501
586
  ### Services
@@ -503,13 +588,15 @@ an earlier block has already replaced.
503
588
  | Function | Purpose |
504
589
  |----------|---------|
505
590
  | `injectCommentsIntoOoxml(oxml, comments, options)` | Add comments anchored to text ranges. |
506
- | `applyCommentReplyToParts(options)` | Build a threaded reply in `comments.xml` and `commentsExtended.xml` without adding a document-body anchor. |
591
+ | `applyCommentReplyToParts(options)` | Build a threaded reply in `comments.xml` and `commentsExtended.xml` without adding a document-body anchor. Pass `documentXml` so the reply's `w14:paraId` also avoids body paragraph ids. |
592
+ | `compareDocumentInspections(expected, actual)` | Compare two `inspectDocumentParts` / `inspect()` results by text, table position, list label, review segments and comments, ignoring ids, dates and run boundaries. Returns `{ matches, differences }` (at most 10). See [Comparing inspections](#comparing-inspections). |
593
+ | `inspectionReviewShape(inspection)` | The normalized, host-independent shape `compareDocumentInspections` compares. |
507
594
  | `acceptTrackedChangesInOoxml(oxml, { author?, allAuthors? })` | Accept `w:ins` / `w:del` / `w:moveFrom` / `w:moveTo` / `*PrChange` revisions for one author or all authors. |
508
595
  | `rejectTrackedChangesInOoxml(oxml, { author?, allAuthors? })` | Reject `w:ins` / `w:del` / `w:moveFrom` / `w:moveTo` / `*PrChange` revisions for one author or all authors. |
509
596
  | `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`. |
510
597
  | `generateTableOoxml(headers, rows, options)` | Generate a `w:tbl` from tabular data. |
511
598
  | `createDynamicNumberingIdState(numberingXml)` | Allocate numbering IDs without collisions. |
512
- | `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. |
599
+ | `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. |
513
600
  | `ensureCommentsArtifactsInZip(zip, commentsXml)` | Merge comments artifacts into a `.docx` package. |
514
601
  | `ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXml)` | Add or replace modern Word comment-thread metadata in a `.docx` package. |
515
602
  | `validateDocxPackage(zip)` | Validate `.docx` structural consistency. |
@@ -556,8 +643,9 @@ identities. Independent edits therefore do not need to be manually sorted when
556
643
  an earlier structural rewrite changes later paragraph indexes or fingerprints.
557
644
  Targets that deliberately refer to uniquely created paragraph text are compiled
558
645
  into an internal capture dependency. True overlap is not guessed: incompatible
559
- writes to one source fail before mutation with `OVERLAPPING_SOURCE_TARGETS` or
560
- `REVISION_ORDER_CONFLICT`, and mutating capture fan-out without distinct
646
+ writes to one source fail before mutation with `OVERLAPPING_SOURCE_TARGETS`,
647
+ `REVISION_ORDER_CONFLICT` or `LIST_FORMAT_COMPOSITION_CONFLICT` (the one composed pair, a
648
+ marker-preserving `redline` plus a `list-format`, runs redline first), and mutating capture fan-out without distinct
561
649
  selectors fails with `CAPTURE_FANOUT_CONFLICT`.
562
650
 
563
651
  Threaded replies use a comment operation with no body target:
@@ -667,6 +755,57 @@ chooses among multiple candidate paragraphs heuristically, it emits an
667
755
  guidance. Callers should pass `{ strictTargets: true }` and use strict descriptors
668
756
  (`paragraphId`, `index`, `occurrence`, or `fingerprint`) to prepare for v1.0.0.
669
757
 
758
+ ### Reject All verification
759
+
760
+ A tracked edit only adds revisions, so rejecting every revision in the output
761
+ must give back the source document. After each operation the library projects
762
+ Word's Reject All and Accept All of the output (`core/word-revision-semantics.js`,
763
+ validated against recorded desktop Word results) and checks these invariants:
764
+
765
+ - `REJECT_PARAGRAPH_COUNT`, `REJECT_PARAGRAPH_TEXT` (including tabs and breaks),
766
+ `REJECT_PARAGRAPH_PROPS`, `REJECT_NUMBERING_REFS` (list membership `numId`/`ilvl`)
767
+ and `REJECT_WRAPPERS` (hyperlink, field and content-control membership):
768
+ Reject All of the output equals Reject All of the source (under accept-first
769
+ `existingRevisions` policies the target range is compared with the source's
770
+ accepted view). `REJECT_NUMBERING_REFS` also refuses a `numId` that the output
771
+ binds, or Reject All restores (including inside `w:pPrChange`), when the source
772
+ did not already use it and no `w:num` -> `w:abstractNum` -> `w:lvl` in the
773
+ available numbering defines it.
774
+ - `ACCEPT_INTENT`: a tracked plain-text `redline` with complete `modified` (no
775
+ Markdown, list-marker or table syntax; a single paragraph or a `targetEnd`
776
+ range) is refused when Accept All of its target would not read as `modified`.
777
+ - `ACCEPT_NEIGHBORS`: an untracked edit (`generateRedlines: false`) may not
778
+ change a paragraph outside its target in the Accept All or Reject All view.
779
+ - `UNTRACKED_CREATED_REVISION`: an untracked edit may not create a tracked
780
+ revision that was not in the source (fragments of a sliced source revision
781
+ keep their element, author and date and are allowed).
782
+
783
+ A localized replacement is also checked before it is committed: the target's
784
+ accepted text must equal the compiled replacement text, otherwise the operation
785
+ is refused with `PATCH_ROUNDTRIP_MISMATCH` rather than dropping content.
786
+
787
+ By default a violation is refused with `REVISION_FIDELITY_VIOLATION`; the
788
+ document is unchanged, `status` is `error`, and the receipt is `refused`. The
789
+ error payload contains:
790
+
791
+ - `violations[]`, each with `invariant` (`REJECT_PARAGRAPH_COUNT`,
792
+ `REJECT_PARAGRAPH_TEXT`, `REJECT_PARAGRAPH_PROPS`, `REJECT_NUMBERING_REFS`,
793
+ `REJECT_WRAPPERS`, `ACCEPT_INTENT`, `ACCEPT_NEIGHBORS` or
794
+ `UNTRACKED_CREATED_REVISION`), a plain-language `explanation`,
795
+ `expected`/`actual` excerpts, and `source.sourceParagraphIndex` (the 1-based
796
+ paragraph index from extraction) with a text excerpt;
797
+ - `recovery.retryGuidance`, one retry instruction per reported invariant, for
798
+ example use `replacements` for a literal edit, keep breaks and hyperlink
799
+ boundaries, or move formatting and list changes into their own operation.
800
+
801
+ Control it with the `verifyRevisionFidelity` option (`'enforce'` default,
802
+ `'warn'`, `'off'`) or `--revision-fidelity enforce|warn|off` on the CLI. `warn`
803
+ applies the edit and appends the same text to `warnings`. The `--profile agent`
804
+ profile rejects `off`. A narrower retry that is also refused indicates an
805
+ unsupported shape; report it rather than disabling verification. Recovery
806
+ details are in the `REVISION_FIDELITY_VIOLATION` row of
807
+ [docs/AGENT_KNOWLEDGE_BASE.md](./docs/AGENT_KNOWLEDGE_BASE.md).
808
+
670
809
  ### Mutation Receipts
671
810
 
672
811
  Both single-operation (`applyOperationToDocumentXml`) and batch
@@ -722,7 +861,7 @@ Different APIs return different OOXML shapes. Use this as a packaging safety che
722
861
  ID. Generated bullet and numbered paragraphs must reference a positive ID
723
862
  whose definition is merged into `word/numbering.xml`.
724
863
  - Do use `extractReplacementNodesFromOoxml(...)` when you are consuming `result.oxml` from paragraph/range/table APIs.
725
- - Do merge numbering/comments artifacts with `ensureNumberingArtifactsInZip(...)` and `ensureCommentsArtifactsInZip(...)` when those parts are present. Supply `mergeNumberingXmlBySchemaOrder` when numbering already exists.
864
+ - 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.
726
865
  - Don't write payloads that start with `<pkg:package` directly into `word/document.xml`.
727
866
  - Don't assume every `result.oxml` payload is a raw paragraph fragment.
728
867
 
@@ -744,7 +883,6 @@ import {
744
883
  applyRedlineToOxml,
745
884
  extractReplacementNodesFromOoxml,
746
885
  ensureNumberingArtifactsInZip,
747
- mergeNumberingXmlBySchemaOrder,
748
886
  validateDocxPackage
749
887
  } from '@ansonlai/docx-redline-js';
750
888
  import { applyOperationToDocumentXml } from '@ansonlai/docx-redline-js/standalone-runner';
@@ -807,9 +945,7 @@ const normalized = extractReplacementNodesFromOoxml(fragmentResult.oxml);
807
945
  // If sourceType === 'package', merge extracted content/artifacts instead of
808
946
  // writing the raw pkg:package payload into word/document.xml.
809
947
  if (normalized.numberingXml) {
810
- await ensureNumberingArtifactsInZip(zip, normalized.numberingXml, {
811
- mergeNumberingXml: mergeNumberingXmlBySchemaOrder
812
- });
948
+ await ensureNumberingArtifactsInZip(zip, normalized.numberingXml);
813
949
  }
814
950
 
815
951
  await validateDocxPackage(zip);