@ansonlai/docx-redline-js 0.8.4 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/ARCHITECTURE.md +221 -12
  2. package/CHANGELOG.md +176 -0
  3. package/README.md +125 -12
  4. package/core/current-properties.js +136 -0
  5. package/core/paragraph-text.js +211 -209
  6. package/core/types.js +9 -3
  7. package/core/word-revision-semantics.js +256 -0
  8. package/dist/docx-redline-js.esm.js +4189 -767
  9. package/dist/docx-redline-js.esm.js.map +4 -4
  10. package/dist/docx-redline-js.esm.min.js +111 -108
  11. package/dist/docx-redline-js.esm.min.js.map +4 -4
  12. package/dist/docx-redline.bundle.cjs +4292 -870
  13. package/dist/docx-redline.bundle.cjs.map +4 -4
  14. package/dist/docx-redline.bundle.js +4292 -870
  15. package/dist/docx-redline.bundle.js.map +4 -4
  16. package/docs/AGENT_FAST_START.md +2 -0
  17. package/docs/AGENT_KNOWLEDGE_BASE.md +19 -3
  18. package/docs/SKILL_AUTHORING.md +165 -150
  19. package/docs/TESTING.md +1142 -1059
  20. package/docs/schemas/document-operations.schema.json +54 -1
  21. package/document/docx-document.js +1022 -1010
  22. package/engine/oxml-engine.js +736 -749
  23. package/engine/paragraph-mark-revisions.js +292 -0
  24. package/engine/reconstruction-mapper.js +312 -311
  25. package/engine/reconstruction-writer.js +67 -10
  26. package/engine/rpr-helpers.js +42 -0
  27. package/engine/run-builders.js +0 -73
  28. package/engine/surgical-diff-application.js +604 -605
  29. package/engine/table-mode.js +63 -63
  30. package/index.d.ts +47 -1
  31. package/index.js +11 -2
  32. package/node/cli-help.js +1 -0
  33. package/node/cli.js +992 -975
  34. package/orchestration/list-structural-fallback.js +67 -9
  35. package/package.json +1 -1
  36. package/pipeline/diff-engine.js +130 -1
  37. package/pipeline/ingestion-export.js +227 -243
  38. package/pipeline/ingestion-paragraph.js +93 -98
  39. package/pipeline/list-generation.js +415 -419
  40. package/pipeline/pipeline.js +206 -204
  41. package/scripts/export-validation-fixtures.mjs +19 -6
  42. package/scripts/generate-word-revision-semantics-matrix.mjs +134 -0
  43. package/scripts/lib/word-coverage-catalogue.mjs +7 -2
  44. package/scripts/render-multilevel-bullet-visual.ps1 +2 -2
  45. package/scripts/word-com-corpus-visual-suite.ps1 +2 -2
  46. package/scripts/word-com-differential.ps1 +443 -335
  47. package/scripts/word-com-visual-suite.ps1 +2 -2
  48. package/scripts/word-revision-semantics-matrix.ps1 +108 -0
  49. package/services/batch-operation-orchestrator.js +23 -3
  50. package/services/capture-engine.js +228 -226
  51. package/services/document-operation-applier.js +213 -0
  52. package/services/document-operation-contract.js +75 -0
  53. package/services/document-operation-mutations.js +3346 -2721
  54. package/services/error-recovery.js +17 -0
  55. package/services/list-conversion-diagnostics.js +296 -0
  56. package/services/list-format-change.js +586 -0
  57. package/services/list-format-mutation.js +416 -0
  58. package/services/list-format.js +305 -0
  59. package/services/numbering-allocation.js +642 -0
  60. package/services/numbering-helpers.js +354 -524
  61. package/services/operation-batch-compiler.js +183 -11
  62. package/services/operation-preflight.js +1 -0
  63. package/services/receipt-collector.js +288 -288
  64. package/services/revision-comment-management.js +38 -5
  65. package/services/revision-fidelity-verifier.js +498 -0
  66. package/services/standalone-docx-plumbing.js +627 -631
  67. package/services/standalone-operation-runner.d.ts +182 -5
  68. package/services/table-reconciliation.js +59 -7
package/ARCHITECTURE.md CHANGED
@@ -38,8 +38,10 @@ No Word add-in entrypoints or host-specific integration layers are part of this
38
38
  │ ├── logger.js
39
39
  │ └── xml-adapter.js
40
40
  ├── core/
41
+ │ ├── current-properties.js
41
42
  │ ├── paragraph-text.js
42
43
  │ ├── sha256.js
44
+ │ ├── word-revision-semantics.js
43
45
  │ └── word-xml.js
44
46
  ├── document/
45
47
  │ ├── docx-document.js
@@ -50,18 +52,27 @@ No Word add-in entrypoints or host-specific integration layers are part of this
50
52
  │ ├── surgical-mode.js
51
53
  │ ├── reconstruction-mode.js
52
54
  │ ├── run-builders.js
55
+ │ ├── paragraph-mark-revisions.js
53
56
  │ └── formatting-removal.js
54
57
  ├── orchestration/
55
58
  ├── pipeline/
56
59
  ├── services/
57
60
  │ ├── comment-engine.js
58
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
59
69
  │ ├── revision-comment-management.js
60
70
  │ ├── document-inspection.js
61
71
  │ ├── document-operation-session.js
62
72
  │ ├── document-operation-applier.js
63
73
  │ ├── document-operation-mutations.js
64
74
  │ ├── batch-operation-orchestrator.js
75
+ │ ├── revision-fidelity-verifier.js
65
76
  │ ├── operation-heuristics.js
66
77
  │ ├── standalone-docx-plumbing.js
67
78
  │ └── standalone-operation-runner.js
@@ -116,6 +127,24 @@ No Word add-in entrypoints or host-specific integration layers are part of this
116
127
  - `core/validation-delta.js`
117
128
  - Stable issue signatures and multiset subtraction for classifying baseline
118
129
  versus generated validation issues without hiding added duplicate errors.
130
+ - `core/word-revision-semantics.js`
131
+ - Pure DOM projection of what Word shows after Reject All or Accept All
132
+ (`projectWordRevisionView`, `wordRevisionParagraphTexts`,
133
+ `paragraphMarkRevision`). It encodes Word's paragraph-mark rules: a
134
+ tracked-inserted mark vanishes on Reject and its content merges into the
135
+ following paragraph of the same container, a tracked-deleted mark does the
136
+ same on Accept, a table cell keeps its last paragraph, rows marked inserted
137
+ or deleted vanish, and Reject restores `w:pPrChange` snapshots. It never
138
+ mutates or serializes. The rules are validated against recorded desktop
139
+ Word 16 COM results (`tests/fixtures/word-revision-semantics/`).
140
+ - `core/current-properties.js`
141
+ - Direct-child accessors for the current paragraph/run properties
142
+ (`getCurrentParagraphProperties`, `getCurrentRunProperties`,
143
+ `getCurrentChild`, `getCurrentNumIdElements`, ...). Descendant lookups cross
144
+ `w:pPrChange` / `w:rPrChange`, so a historical `numPr` or `pStyle` would look
145
+ current; the snapshot is reachable only through the explicit
146
+ `getHistoricalParagraphProperties` / `getHistoricalRunProperties` and
147
+ `getAllReferencedNumIdElements`. Depends only on `core/types.js`.
119
148
  - `core/revision-cloning.js`
120
149
  - Shared effective-property cloning strips historical revision descendants;
121
150
  intentional revision-bearing splits refresh cloned property-change IDs.
@@ -125,7 +154,20 @@ No Word add-in entrypoints or host-specific integration layers are part of this
125
154
  - Internal reconciliation capability matrix and opt-in diagnostic route
126
155
  instrumentation. It observes policy; it does not expose a route override.
127
156
  - `engine/run-builders.js`
128
- - Shared builders for insertion/deletion wrappers, paragraph-mark revisions, visible run content, and run-property changes.
157
+ - Shared builders for insertion/deletion wrappers, visible run content, and run-property changes. It does not build paragraph-mark revisions.
158
+ - `engine/paragraph-mark-revisions.js`
159
+ - The single construction site for a tracked paragraph mark (`w:ins` / `w:del`
160
+ under `w:pPr/w:rPr`). DOM entry points (`markParagraphInserted`,
161
+ `markParagraphDeleted`, `setParagraphPropertiesMarkRevision`,
162
+ `clearParagraphMarkRevision`, `restoreParagraphMarkInserted`) and string
163
+ builders (`paragraphMarkRevisionXml`, `paragraphMarkPPrXml`) share one
164
+ metadata path, so ids are allocated through the document's allocator and
165
+ recorded in receipts. `planParagraphMarks` is a pure check that an
166
+ alignment gives every source paragraph exactly one untracked or deleted
167
+ mark and every new target paragraph an inserted mark, so Reject All yields
168
+ the source paragraph count and a deleted paragraph is never collapsed into
169
+ another. Reconstruction, list and table generation, and document-level
170
+ shortcuts all call it. Depends only on `core/`.
129
171
  - `engine/surgical-*.js`
130
172
  - 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
173
  The shared carrier splitter also handles explicit rejected-view deletion
@@ -148,7 +190,36 @@ No Word add-in entrypoints or host-specific integration layers are part of this
148
190
  - `services/comment-engine.js`
149
191
  - Comment creation and package-level comment XML handling.
150
192
  - `services/numbering-helpers.js`
151
- - Dynamic numbering ID allocation, numbering payload remapping, and schema-order-safe numbering merges.
193
+ - Document-scoped numbering ID state (`createDynamicNumberingIdState`,
194
+ `reserveNextNumberingIdPair`), numbering payload remapping, and shared DOM
195
+ predicates. It no longer owns a merge.
196
+ - `services/numbering-allocation.js`
197
+ - The single place that creates, continues, merges and validates numbering
198
+ definitions. `allocateNumberingDefinition` builds a new `w:abstractNum` +
199
+ `w:num` (source levels copied, chosen levels redefined, start overrides)
200
+ from ids reserved through the document state; `allocateNumberingForReplacement`
201
+ holds the reuse-source-list policy (`continue-source`, `allocate`,
202
+ `passthrough`) so no call site decides it; `continueBatchListSequence` lets
203
+ separate operations of one batch that generate items of the same kind,
204
+ format and level continue one numbering instance (reused only while its
205
+ `numId` is still bound in the live document). `mergeNumberingParts` /
206
+ `mergeNumberingXmlBySchemaOrder` are the one numbering merge, used by the
207
+ runner's consumers and the `openDocx` facade; an incoming `w:abstractNum` /
208
+ `w:num` id that exists with different content throws `NUMBERING_ID_CONFLICT`.
209
+ `checkNumberingBinding` validates a direct `{ numId, level }` binding and
210
+ `validateNumberingIntegrity` reports dangling `numId`, `abstractNumId` and
211
+ `ilvl` (`NUMBERING_*` issues) for the facade's baseline-delta validation.
212
+ Old definitions are never deleted or overwritten, and tracked-deleted source
213
+ paragraphs keep their `numPr`.
214
+ - `services/list-conversion-diagnostics.js`
215
+ - Safety diagnostics for text edits that retype list markers
216
+ (`LIST_MARKER_NOT_BOUND` / `UNSUPPORTED_LIST_CONVERSION`,
217
+ `LIST_FORMAT_NOT_APPLIED` / `UNSUPPORTED_LIST_FORMAT_CHANGE`; warning by
218
+ default, error under `strictListConversion`, silenced by
219
+ `listMarkers: 'literal'`), the closed `paragraph-format` key set (unknown
220
+ keys are `INVALID_OPERATION`) and the `numbering` shape check behind
221
+ `paragraph-format` numbering binding. Recovery points at `list-format`
222
+ through `recovery.suggestedOperation`.
152
223
  - `services/standalone-docx-plumbing.js`
153
224
  - Package-level extraction/wiring/validation for `word/document.xml`, `word/numbering.xml`, and `word/comments.xml`.
154
225
  - `services/revision-comment-management.js`
@@ -160,7 +231,13 @@ No Word add-in entrypoints or host-specific integration layers are part of this
160
231
  - Canonical single-operation validation, author resolution, dispatch, and
161
232
  result metadata assembly. Before commit it validates the entire live
162
233
  document against the operation savepoint and refuses newly generated
163
- structural errors, including duplicate revision IDs.
234
+ structural errors, including duplicate revision IDs. For localized
235
+ `replacements` it also checks, before commit, that the target's accepted text
236
+ (read from the successor paragraph via `SourceTargetRegistry.previewSuccessor`,
237
+ or the restored paragraph) equals the compiled desired text; otherwise the
238
+ savepoint is restored and `PATCH_ROUNDTRIP_MISMATCH` is returned, because a
239
+ tracked whole-paragraph deletion rejects back to the source and the Reject
240
+ All verifier alone cannot see it.
164
241
  - `services/document-operation-mutations.js`
165
242
  - Coupled OOXML mutation implementations for redline, highlight, and comment
166
243
  operations. For text-bearing edits, the resolved paragraph's exact
@@ -188,6 +265,50 @@ No Word add-in entrypoints or host-specific integration layers are part of this
188
265
  - `services/document-operation-contract.js`
189
266
  - Compatibility normalization, canonical operation kinds, author precedence,
190
267
  and stable runtime validation for document operations.
268
+ - `services/list-format.js` and `services/list-format-mutation.js`
269
+ - The `list-format` operation (`convert-plain`, plus the shared request shape
270
+ for `change-format` and `join`): contract shape, the narrow
271
+ typed-marker grammar (letter versus roman disambiguation by sequence or an
272
+ explicit `format`), and the in-place DOM core. Every refusal is decided
273
+ before mutation; the binding reuses the `paragraph-format.numbering` path
274
+ (`snapshotAndAttachPPrChange` + `applyParagraphPropertiesToPPr`), the marker
275
+ becomes one tracked `w:del`, definitions come from
276
+ `allocateNumberingDefinition`, and batch continuity uses
277
+ `runtimeContext.listFallbackSequenceState` (an entry is reused only while
278
+ its `numId` is bound in the live document). Text-equal redline header
279
+ conversions for letters, roman numerals and bullets call the same core.
280
+ The `paragraph-format` numbering binding (`properties.numbering`,
281
+ `{ numId, level }` or `null`) lives in `document-operation-mutations.js`: it
282
+ snapshots the earliest same-author `w:pPrChange`, refuses another author's
283
+ pending snapshot (`PARAGRAPH_FORMAT_REVISION_CONFLICT`), and checks the
284
+ binding with `checkNumberingBinding`. `list-format` is body-only; `openDocx`
285
+ refuses it with a `part` selector.
286
+ - `services/list-format-change.js`
287
+ - `list-format` `change-format` and `join` (`scope: 'list'` or `'range'`): rebind existing list items with
288
+ one tracked `w:pPrChange` each, never editing a shared definition.
289
+ `change-format` allocates a new definition through
290
+ `allocateNumberingDefinition` (source levels copied, one redefined, start
291
+ values on the new abstract); `join` reuses the earlier list's `numId`.
292
+ Planning models Word's counters (shared abstracts continue each other, a
293
+ level start re-applies on restart, deeper items start unused levels) and
294
+ refuses splits, joins and style-linked or instance-overridden definitions
295
+ it cannot reproduce exactly. Definitions are read from the source part plus
296
+ the batch's pending numbering parts.
297
+ - `services/operation-batch-compiler.js`
298
+ - Binds every operation to batch-start source paragraphs
299
+ (`SourceTargetRegistry`), compiles localized `replacements`, and detects
300
+ overlap before any mutation. Composition rule for `redline` + `list-format`:
301
+ exactly one single-paragraph `redline` (no `targetEnd`, anchor, part or
302
+ structured content) that keeps the typed marker and changes the text after
303
+ it composes with one `list-format` on the same paragraph; the compiler fixes
304
+ the order by setting `_compiledRunAfter` on the list-format, which the batch
305
+ orchestrator honours, so the redline always runs first. Everything else on a
306
+ list-format target is refused with `LIST_FORMAT_COMPOSITION_CONFLICT`
307
+ (`reason`: `marker-changed`, `redline-unchanged`, `redline-shape`, or
308
+ `list-rebind` when a `scope: 'list'` `change-format`/`join` rebinds an item
309
+ that another operation also writes). `list.continueFrom` is bound at batch
310
+ start as a read-only target. `previewSuccessor` lets the applier find a
311
+ target's replacement paragraph before commit.
191
312
  - `services/operation-preflight.js`
192
313
  - Read-only strict target and anchor resolution, revision-policy diagnostics,
193
314
  artifact prediction, and same-paragraph conflict reporting.
@@ -213,6 +334,42 @@ No Word add-in entrypoints or host-specific integration layers are part of this
213
334
  Mutation commands expose a compact contract: package/XML payloads and full
214
335
  validation arrays remain internal, while stdout contains durability fields,
215
336
  per-operation evidence, validation counts, and a derived completion flag.
337
+ - `services/revision-fidelity-verifier.js`
338
+ - Post-apply invariant check built on `core/word-revision-semantics.js`.
339
+ `verifyRevisionFidelity` compares Reject All of the output with Reject All
340
+ of the savepoint (both reject every author's revisions) and reports
341
+ `REJECT_PARAGRAPH_COUNT`, `REJECT_PARAGRAPH_TEXT`, `REJECT_PARAGRAPH_PROPS`,
342
+ `REJECT_NUMBERING_REFS` and `REJECT_WRAPPERS`. Under accept-first
343
+ existing-revision policies the baseline is the accepted view, scoped to the
344
+ resolved target range when known (otherwise the changed region between the
345
+ unchanged prefix and suffix). `REJECT_NUMBERING_REFS`
346
+ (`verifyNumberingReferences`) also refuses a numId that the output binds or
347
+ Reject All restores when the source did not use it and no `w:num` ->
348
+ `w:abstractNum` -> `w:lvl` defines it in the numbering available to the
349
+ operation (numbering existence). `verifyUntrackedLocality` covers
350
+ `generateRedlines: false` single-target operations with `ACCEPT_NEIGHBORS`:
351
+ no paragraph outside the target may change in either view.
352
+ `verifyNoCreatedRevisions` adds `UNTRACKED_CREATED_REVISION`: an untracked
353
+ operation may not introduce a revision id that was not in the source
354
+ (fragments of a sliced source revision, same element, author and date, are
355
+ allowed). `verifyAcceptIntent` adds `ACCEPT_INTENT`: a tracked plain-text
356
+ `redline` with complete `modified` (single paragraph or `targetEnd` range)
357
+ must read as `modified` in the Accept All view of its target; accept-first
358
+ range targets use the exact accepted scope.
359
+ `buildRevisionFidelityError` produces the `REVISION_FIDELITY_VIOLATION`
360
+ payload (`violations[].explanation`, `source.sourceParagraphIndex`,
361
+ `recovery.retryGuidance`). Modes are `enforce` (default, refuse and roll
362
+ back), `warn` (apply and append a warning) and `off`, resolved by
363
+ `resolveRevisionFidelityMode`: explicit `verifyRevisionFidelity` option,
364
+ then the diagnostic `DOCX_REDLINE_REVISION_FIDELITY` environment variable,
365
+ then `enforce`. The view memo is `createFidelityViews(document)`: lazily
366
+ filled `accepted` / `rejected` projections, revision-id signatures and
367
+ numbering references for one DOM. The applier builds a memo for the savepoint
368
+ (taking over `session._fidelityViews` when it still describes the live
369
+ document) and one for the result; after a committed tracked or untracked
370
+ operation the result's memo is stored back in `session._fidelityViews` as
371
+ the next operation's baseline, and a refused operation restores the prior
372
+ memo, so each view is computed at most once per DOM.
216
373
  - `orchestration/*`
217
374
  - Route planning and list fallback orchestration utilities.
218
375
 
@@ -221,15 +378,25 @@ No Word add-in entrypoints or host-specific integration layers are part of this
221
378
  1. Caller imports from `index.js`.
222
379
  2. Caller configures XML provider/logger/defaults when needed via `adapters/*`.
223
380
  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.
381
+ 4. A full-document batch parses once, captures its initial target snapshot,
382
+ compiles the operations against it (`operation-batch-compiler.js`: batch-start
383
+ source binding, localized replacements, overlap and list-format composition
384
+ checks, all before any mutation), and creates an operation savepoint before
385
+ each mutation.
226
386
  5. `engine/oxml-engine.js` routes each selected scope to format, table, list,
227
387
  surgical, or reconstruction flows; successful scoped output is imported
228
388
  into the live document.
229
- 6. Failed and no-op operations restore their savepoint. A successful changed
389
+ 6. After each mutation the applier (`services/document-operation-applier.js`)
390
+ validates the generated OOXML against the savepoint, checks the localized
391
+ replacement intent, then runs the revision fidelity verifier (Reject All,
392
+ Accept All intent, neighbor, created-revision and numbering-existence
393
+ invariants); a violation in `enforce` mode restores the savepoint and
394
+ refuses the operation before anything is committed. Failed and no-op
395
+ operations restore their savepoint. A successful changed
230
396
  batch serializes the full document once; atomic failures and all-no-op
231
397
  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.
398
+ 7. Pipeline/services return OOXML, optional package artifacts (`numberingXml`, comments payloads; numbering parts are merged by `mergeNumberingParts` and
399
+ checked by `validateNumberingIntegrity`), and non-breaking `status`/`error` fields where applicable.
233
400
  8. Optional revision/comment management transforms can accept/reject revisions, including move revisions, or delete comments by author.
234
401
  9. Caller writes resulting XML back to package/document boundaries.
235
402
 
@@ -265,6 +432,28 @@ still be re-exported from `index.js`.
265
432
  `COMMENTED_CONTENT_MERGE`; never discard anchors as a side effect of restoring
266
433
  the pre-revision baseline.
267
434
  Pass `existingRevisions: 'accept-all-first'` to normalize prior revisions explicitly.
435
+ - Every operation is checked after it is applied
436
+ (`services/revision-fidelity-verifier.js`, default `enforce`): Word's Reject
437
+ All of a tracked output must equal Word's Reject All of the source (`REJECT_*`),
438
+ a plain-text `redline` must read as its `modified` text under Accept All
439
+ (`ACCEPT_INTENT`), and an untracked edit must neither change other paragraphs
440
+ (`ACCEPT_NEIGHBORS`) nor create a revision (`UNTRACKED_CREATED_REVISION`). A
441
+ violation restores the operation savepoint and returns
442
+ `REVISION_FIDELITY_VIOLATION`; `warn` applies and warns, `off` is for
443
+ diagnosis only.
444
+ - Allocate, continue, merge and validate numbering only through
445
+ `services/numbering-allocation.js`. Never edit a shared `w:abstractNum` or
446
+ `w:num` in place, never delete a definition Reject All may need, and keep
447
+ `numPr` on tracked-deleted source paragraphs.
448
+ - Construct paragraph-mark `w:ins` / `w:del` only through
449
+ `engine/paragraph-mark-revisions.js`, and read current paragraph/run
450
+ properties only through `core/current-properties.js`. Two source-scanning
451
+ tests enforce this: `tests/paragraph_mark_single_site_tests.mjs` rejects
452
+ paragraph-mark revision construction (templates opening an `rPr` with
453
+ `ins`/`del`, DOM creation attached to an `rPr`, retired helper names) outside
454
+ the helper, and `tests/current_properties_usage_tests.mjs` rejects descendant
455
+ lookups of property names (`numPr`, `pStyle`, `b`, ...) outside the accessor.
456
+ Exceptions need an entry with a written reason in each test's allowlist.
268
457
  - Do not write unknown `result.oxml` payloads directly into `word/document.xml`;
269
458
  normalize with `extractReplacementNodesFromOoxml(...)` or use
270
459
  `applyOperationToDocumentXml(...).documentXml` for full-document replacement.
@@ -339,8 +528,10 @@ still be re-exported from `index.js`.
339
528
  - Comment IDs must be allocated against both document anchors and the existing
340
529
  `word/comments.xml` part. Revision IDs must be allocated against the complete
341
530
  document revision scope.
342
- - Numbering payloads must be merged with `mergeNumberingXmlBySchemaOrder` when
343
- a package already contains numbering definitions; replacement is not a merge.
531
+ - Numbering payloads are merged, never replaced, by the single merge in
532
+ `services/numbering-allocation.js` (`mergeNumberingParts`), which both the
533
+ runner's consumers and the `openDocx` facade call; conflicting ids fail with
534
+ `NUMBERING_ID_CONFLICT`.
344
535
  - Safe package mutation is transactional: retain the original package, apply
345
536
  operations, merge all artifacts, validate redline markup and package wiring,
346
537
  and commit only if every required check succeeds.
@@ -370,7 +561,7 @@ still be re-exported from `index.js`.
370
561
  text-bearing mutation. Mutation uses canonical accepted-view source text, and `resolvedTarget.targetTextMatch`
371
562
  records `exact`, `space_equivalent`, or `normalized` selection plus bounded
372
563
  invisible-character diagnostics.
373
- - CLI contract version 3 is deliberately narrower than library result objects.
564
+ - The CLI contract (version 9; `docx-redline version` lists the capabilities) is deliberately narrower than library result objects.
374
565
  `apply`, `accept`, `reject`, and `delete-comments` omit `documentXml`, package
375
566
  parts, inspection text, and full issue arrays. `completion` is true only when
376
567
  `written === true`, top-level status is neither error nor partial, and every
@@ -400,6 +591,10 @@ still be re-exported from `index.js`.
400
591
  is parsed from the serialized output XML parts (`word/document.xml`, `word/comments.xml`,
401
592
  `word/numbering.xml`). Any discrepancy immediately fails the transaction and triggers
402
593
  atomic rollback.
594
+ - The oracle checks that durable IDs are committed; the revision fidelity
595
+ verifier checks what Reject All would restore. A refused operation restores
596
+ its savepoint and receipt state like any other refusal, so it contributes no
597
+ committed IDs (`finalDisposition: 'refused'`).
403
598
 
404
599
 
405
600
  ## Build Output
@@ -437,11 +632,21 @@ The bundle inlines `diff-match-patch` and keeps `@xmldom/xmldom` external.
437
632
  - `npm run smoke:word:diff`
438
633
  - Windows/Word COM differential test: Word itself accepts/rejects the
439
634
  generated fixtures and the resulting text is compared to expectations.
635
+ Structural checks also compare paragraph counts, and list cases compare
636
+ `ListFormat.ListString` labels after `Revisions.AcceptAll()` /
637
+ `RejectAll()`.
440
638
  - `bash scripts/validate-fixtures-xsd.sh`
441
639
  - Validates exported fixtures against the ECMA-376 transitional `wml.xsd`.
640
+ - `tests/reject_roundtrip_matrix_tests.mjs` (part of `npm test`)
641
+ - Generated operation matrix over fixtures and operation kinds, asserting the
642
+ revision fidelity invariants and receipt reconciliation (a
643
+ `RECEIPT_RECONCILIATION_FAILED` fails the case). Replay with `FUZZ_SEED` /
644
+ `MATRIX_ITERATIONS`.
442
645
  - `.github/workflows/validation.yml`
443
646
  - Nightly independent-oracle validation: XSD schema check, LibreOffice
444
- conversion, and an extended 20k-case fuzz sweep with a fresh seed.
647
+ conversion, an extended 20k-case fuzz sweep with a fresh seed, and the
648
+ `matrix-extended` job (1000 reject-roundtrip matrix cases with a
649
+ date-derived seed; a failure prints its seed and replay command).
445
650
 
446
651
  Use these checks before publishing or tagging. See [docs/TESTING.md](./docs/TESTING.md#release-validation-and-independent-oracles).
447
652
 
@@ -454,7 +659,11 @@ Use this sequence to understand or modify behavior without reading everything:
454
659
  3. For targeting bugs, inspect `core/paragraph-targeting.js`, `core/list-targeting.js`, and `core/table-targeting.js`.
455
660
  4. For package wiring issues, inspect `services/standalone-docx-plumbing.js`.
456
661
  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.
662
+ 6. For numbering/list issues, inspect `services/numbering-allocation.js`,
663
+ `services/list-format*.js`, `services/list-conversion-diagnostics.js` and
664
+ orchestration list-fallback modules; for batch composition,
665
+ `services/operation-batch-compiler.js`; for Reject All refusals,
666
+ `services/revision-fidelity-verifier.js`.
458
667
  7. For reliability regressions, start with `tests/roundtrip_invariant_tests.mjs`,
459
668
  `tests/engine_reliability_tests.mjs`, `tests/paragraph_mark_revision_tests.mjs`,
460
669
  `tests/move_revision_tests.mjs`, and `tests/hardening_status_tests.mjs`.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,181 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Changed
6
+
7
+ - **Unknown `paragraph-format.properties` keys are rejected:** `INVALID_OPERATION` with `field: properties.<key>`, `properties[]`
8
+ and `recovery.action: change_request`; nothing is applied. The 0.8.x `UNKNOWN_PARAGRAPH_FORMAT_PROPERTY` warning no longer fires.
9
+ - **CLI contract version 9** (`list-format` operation type, capability `list-format-v1`). Integrations that do not send
10
+ `list-format` can keep requiring version 8.
11
+ - **`UNSUPPORTED_LIST_CONVERSION` / `LIST_MARKER_NOT_BOUND` now point at `list-format`:** `recovery.action` is
12
+ `use_list_format_convert_plain` (was `apply_plain_text_or_convert_in_host`) and, when the original paragraph has no marker,
13
+ `recovery.suggestedOperation` carries the `list-format` request to send with the same target (for "Header" -> "C. Header":
14
+ `{ mode: 'convert-plain', format: 'upperLetter', strip: 'none', start: 3 }`). `listMarkers: 'literal'` is unchanged.
15
+ - **`UNSUPPORTED_LIST_FORMAT_CHANGE` / `LIST_FORMAT_NOT_APPLIED` now point at `list-format` `change-format`:** `recovery.action`
16
+ is `use_list_format_change_format` (was `change_list_format_in_host`) and `recovery.suggestedOperation` carries
17
+ `{ type: 'list-format', list: { mode: 'change-format', format, level, scope: 'list' } }`; add a target on one item of the list.
18
+ `list-change` still never applies a format change itself.
19
+ - **A `list-format` with `scope: 'list'` counts as a write to every item it rebinds:** another write to one of those items in the
20
+ same batch (`paragraph-format`, `highlight`, `format`, a second `list-format` of the same list) now fails before mutation with
21
+ `LIST_FORMAT_COMPOSITION_CONFLICT` (`reason: list-rebind`); in WP-5 it ran in batch order. Send it in a separate apply or use
22
+ `scope: 'range'`. A single-paragraph `redline` of an item still composes (see Added).
23
+ - **Text-equal letter, roman and bullet header conversions are made in place:** a `redline` whose `modified` equals the plain
24
+ paragraph's typed-marker text (`B. Header` -> `B. Header`) now strips the marker as a tracked deletion and binds the list with a
25
+ tracked `w:pPrChange` in the same paragraph, like the decimal path. The previous whole-paragraph delete plus insert lost
26
+ `w14:paraId`, bookmarks and direct formatting and reviewed as a deleted and an inserted line; it remains only for shapes the
27
+ in-place conversion refuses. (It was reject-exact for plain runs but not for markers inside hyperlinks or smart tags, where the
28
+ Reject All verifier refuses it.)
29
+ - **Operations that would break Reject All are refused by default:** the revision fidelity verifier now runs in
30
+ `enforce` mode. A refused operation leaves the document unchanged and returns `REVISION_FIDELITY_VIOLATION` with a
31
+ one-line message naming the invariant, the source paragraph (extraction index and excerpt), expected vs actual, an
32
+ explanation and a retry instruction; `violations[]`, `invariants` and `recovery.retryGuidance` carry the same detail for
33
+ agents, the message is also sent to `onWarn`, and compact CLI output keeps it. Opt out per call with
34
+ `verifyRevisionFidelity: 'warn'` (apply and warn) or `'off'` (diagnosis). CLI: `--revision-fidelity <enforce|warn|off>`,
35
+ shown in `effectiveOptions`; the agent profile rejects `off`. Capability `revision-fidelity-v1`. A single-target untracked
36
+ operation (`generateRedlines: false`) is also refused when it changes a paragraph outside its target in the Accept All or
37
+ Reject All view (invariant `ACCEPT_NEIGHBORS`).
38
+ - **`ACCEPT_INTENT` and numbering-part existence in the revision fidelity check:** a tracked plain-text `redline` with complete
39
+ `modified` (no Markdown, list-marker or table syntax; single-paragraph targets and `targetEnd` ranges) is refused when Accept All of
40
+ its target would not read as `modified`. `REJECT_NUMBERING_REFS` also refuses a numId that the output binds or Reject All restores
41
+ (including inside `w:pPrChange`) when the source did not already use it and no `w:num` -> `w:abstractNum` -> `w:lvl` defines it in the
42
+ numbering available to the operation. Accept-first range targets now use the exact accepted scope.
43
+ - **`ensureNumberingArtifactsInZip` always merges numbering, and refuses id conflicts:** the optional `mergeNumberingXml`
44
+ callback is ignored (the single merge is `mergeNumberingParts` in `services/numbering-allocation.js`), an existing
45
+ `numbering.xml` is no longer replaced when no callback is given, and an incoming `w:abstractNum` / `w:num` whose id exists with
46
+ different content throws `NUMBERING_ID_CONFLICT` instead of silently binding new paragraphs to another list. `openDocx` also
47
+ rejects output that introduces a dangling `numId`, `abstractNumId` or `ilvl` (`NUMBERING_*` issues).
48
+ - **Receipt kinds:** a whole paragraph deleted by an empty `modified` reports its deleted paragraph mark with receipt kind `del`
49
+ (was `structural`); the explicit `1. Header` list conversion now receipts its `w:pPrChange`.
50
+ - **`paragraph-format` keeps the earliest same-author `w:pPrChange` snapshot** and refuses to overwrite another author's pending
51
+ snapshot (`PARAGRAPH_FORMAT_REVISION_CONFLICT`); see Added.
52
+
53
+ ### Added
54
+
55
+ - **`redline` + `list-format` on one paragraph in one batch:** a changed-text header (`1. Old` -> `1. New`) is a `redline` that keeps
56
+ the typed marker exactly (`modified: '1. New'` or `replacements`) plus a `list-format` `convert-plain` of the same paragraph. The
57
+ batch compiler allows exactly this pair (both bind at batch start) and fixes the order whatever the batch order: the redline runs
58
+ first, then the list-format strips the unchanged marker, so Accept All shows label `1.` with `New` and Reject All the literal
59
+ `1. Old` (verified in Word with `ListFormat.ListString`, including a `3.` start value). A single-paragraph `redline` of a list item
60
+ also composes with a `change-format`/`join` that rebinds it. Incompatible pairs are refused before mutation with
61
+ `LIST_FORMAT_COMPOSITION_CONFLICT` (`reason`: `marker-changed` with `sourceMarker`/`modifiedMarker`, `redline-unchanged`,
62
+ `redline-shape`; `recovery.action: split_text_and_list_changes`); other kinds keep `OVERLAPPING_SOURCE_TARGETS` /
63
+ `REVISION_ORDER_CONFLICT`. No new capability: it is part of `list-format-v1` behavior.
64
+ - **`list-format` `change-format` and `join` modes (`list-format-change-v1`, contract version stays 9):** `change-format` changes
65
+ the kind or numbering format of an existing list (`format`, `lvlText`, `start`, `level`, default the target's level). It never
66
+ edits the shared definition: a NEW `w:abstractNum` + `w:num` is allocated with the source levels, indentation and fonts copied
67
+ and one level redefined (`w:isLgl`, picture bullets and a bullet's symbol font are dropped on a format change), and each item is
68
+ rebound with a tracked `w:pPrChange`, so Reject All restores the original labels and Accept All shows the new ones (verified in
69
+ Word with `ListFormat.ListString` after `Revisions.AcceptAll()`/`RejectAll()`: bullets to `1.`, `1.` to `A.`, a nested level to
70
+ `(i)`, and two range splits). `scope: 'list'` (default) rebinds every live item of the list (`items[].inTarget` marks those outside
71
+ the target); `scope: 'range'` rebinds a `target`..`targetEnd` range that runs to the end of the list and continues its count
72
+ (start values on the new definition, including a split that starts at a nested item). `join` binds the target list (or a range
73
+ running to its end) to the `numId` of `list.continueFrom`, an item of an earlier list, so it continues that count; `continueFrom` is
74
+ bound at batch start. Tracked-deleted paragraphs keep their `w:numPr`; a same-author earlier `w:pPrChange` snapshot is kept;
75
+ later operations in a batch see definitions earlier ones allocated. Refused before any write: `LIST_FORMAT_TARGET_NOT_LIST`,
76
+ `LIST_FORMAT_MIXED_RANGE` (plain paragraphs, several lists, or several levels without `list.level`), `LIST_FORMAT_STYLE_LINKED`
77
+ (heading/list-style numbering), `LIST_FORMAT_STYLE_NUMBERED`, `LIST_FORMAT_SPLIT_UNSUPPORTED` (`mid-sequence` range,
78
+ `nested-restart`, `shared-definition` abstract used by another list, `interleaved` join), `LIST_FORMAT_DEFINITION_UNSUPPORTED`
79
+ (missing definition, `w:lvlOverride/w:lvl`, `w:lvlRestart` in a split), `LIST_FORMAT_JOIN_INVALID` and
80
+ `PARAGRAPH_FORMAT_REVISION_CONFLICT`; an untracked rebinding outside the target is refused; a no-op is `ok` with a
81
+ `LIST_FORMAT_UNCHANGED` warning. Results carry `list` (`sourceNumId`, `numId`, `items[]`, `levelStarts` or `continueFrom`).
82
+ An older CLI refuses the new modes with `INVALID_OPERATION` (`field: list.mode`), so negotiate the capability.
83
+ - **`list-format` operation, `convert-plain` mode (`list-format-v1`):** binds plain paragraphs (a target, or `target`..`targetEnd`
84
+ in one container) whose text starts with a typed marker (`1.`/`1)`, `A.`/`a)`, `IV.`/`iv.`, `-`/`•`) to a NEW list definition in
85
+ place. The marker is removed with a tracked `w:del` of exactly the marker text (`strip: 'marker'`, default) and the `w:numPr`
86
+ binding is a tracked `w:pPrChange`, so Reject All restores the literal marker and the unnumbered paragraph and Accept All shows
87
+ the list label (verified in Word: `ListFormat.ListString` after `Revisions.AcceptAll()`/`RejectAll()` for `A.`, `1.`, `I.` and a
88
+ `C.` start value). Each marker's value is the item's value (`startOverride` on the new `w:num`; the label keeps the typed `.`/`)`);
89
+ consecutive values share one list, a gap starts a new list at the typed value, body paragraphs in the range stay plain, and a
90
+ later `list-format` in the same batch whose first value continues the previous list joins it. `strip: 'none'` with `format`
91
+ (and optional `start`) binds without touching the text. Ambiguous or unsupported shapes are refused with recovery envelopes:
92
+ `LIST_MARKER_NOT_FOUND`, `LIST_MARKER_AMBIGUOUS` (a lone `I.`/`C.` reads as letter or roman; set `list.format`),
93
+ `LIST_MARKER_UNSUPPORTED` (mixed markers, `2.1.`, `(a)`, a marker inside a hyperlink/field/revision, format mismatch),
94
+ `LIST_FORMAT_TARGET_IS_LIST`, `LIST_FORMAT_STYLE_NUMBERED` (style-linked numbering; needs `stylesXml` on the bare runner, read from
95
+ the package by `openDocx`) and `PARAGRAPH_FORMAT_REVISION_CONFLICT`. Body only. Results carry `list` (`format`, `items[]` with
96
+ `numId`/`value`, `skipped[]`). Schema `$defs.listFormat`, `ListFormatDocumentOperation` in `index.d.ts`.
97
+ - **`paragraph-format.numbering` binding (`paragraph-numbering-binding-v1`, no contract bump):** `properties.numbering` is
98
+ `{ numId, level? }` (bind the paragraph to an existing list) or `null` (remove its direct list binding), recorded as a tracked
99
+ `w:pPrChange` so Reject All restores the previous `w:numPr` exactly and Accept All shows the new one (verified in Word with
100
+ `Document.Revisions.RejectAll()/AcceptAll()` and `ListFormat.ListString`). `w:numPr` is inserted at its CT_PPr position
101
+ (after `widowControl`, before `suppressLineNumbers`/`spacing`/`ind`; `ilvl` before `numId`). A binding never allocates: an unknown
102
+ `numId` or undefined level is refused `PARAGRAPH_NUMBERING_NOT_FOUND`. `paragraph-format` now keeps the earliest `w:pPrChange`
103
+ snapshot for repeated same-author edits (it used to replace it, so Reject All restored the intermediate state) and refuses to
104
+ overwrite another author's pending snapshot (`PARAGRAPH_FORMAT_REVISION_CONFLICT`). Letter and roman literal-marker headers
105
+ (`C.`, `iv.`) now convert with their start value instead of restarting at the first item.
106
+ - **List conversion diagnostics (`list-conversion-diagnostics-v1`, no contract bump):** text edits no longer report a silent `ok`
107
+ when they cannot create list structure. A plain paragraph whose new text starts a list marker returns the warning
108
+ `LIST_MARKER_NOT_BOUND` (`diagnostics[]` on the operation result, `CODE: message` in `warnings`), or `UNSUPPORTED_LIST_CONVERSION`
109
+ under the strict policy (`--profile agent`, `--atomic`, or `strictListConversion: true`). A list item whose retained marker asks for a
110
+ different kind or format than its numbering definition returns `LIST_FORMAT_NOT_APPLIED` / `UNSUPPORTED_LIST_FORMAT_CHANGE`.
111
+ `listMarkers: 'literal'` on an operation opts out. `paragraph-format.properties` is now a closed schema; unknown keys (such as
112
+ `spacingAfter`) first produced an `UNKNOWN_PARAGRAPH_FORMAT_PROPERTY` warning (superseded in 0.9.0, see Changed).
113
+ Independent of `REVISION_FIDELITY_VIOLATION`; compact CLI output keeps `diagnostics` and the error fields.
114
+
115
+ - **Numbering allocator:** new `services/numbering-allocation.js` (exported from the package root): `allocateNumberingDefinition`
116
+ (new abstractNum + num with fresh ids, copied level geometry, per-level `startOverride`), `allocateNumberingForReplacement`
117
+ (same-kind source-list continuation or fresh allocation; tracked-deleted source paragraphs never change `numPr`),
118
+ `mergeNumberingParts` / `mergeNumberingXmlBySchemaOrder` (now with an `onConflict` option), `canonicalNumberingDefinition` and
119
+ `validateNumberingIntegrity`. The standalone runner's consumers and the `openDocx` facade merge through the same function;
120
+ `reuseSourceListNumbering` is replaced by allocator policy.
121
+ - **Revision fidelity verifier:** every tracked operation now checks, on the live DOM and without extra
122
+ serialization, that Word's Reject All of its output equals Word's Reject All of the source (paragraph count and order,
123
+ canonical text including breaks and tabs, paragraph properties, `numPr`, hyperlink/field/content-control membership).
124
+ A mismatch refuses the operation (see Changed); in `warn` mode it is applied with the message in `warnings` and
125
+ `revisionFidelityViolations`. The Word-semantics simulator is `core/word-revision-semantics.js`, validated against
126
+ recorded Word 16 Reject All / Accept All results (`tests/fixtures/word-revision-semantics/expectations.json`).
127
+
128
+ - **`UNTRACKED_CREATED_REVISION` invariant:** an operation with `generateRedlines: false` is refused (default `enforce`) when its
129
+ output contains a revision id (`w:ins`, `w:del`, moves, `*PrChange`, paragraph-mark and row revisions) that the source did not
130
+ have. A foreign revision sliced around the edit keeps its element, author and date and is not counted. It reports as
131
+ `REVISION_FIDELITY_VIOLATION` with `violations[].invariant: 'UNTRACKED_CREATED_REVISION'`, also for range targets.
132
+
133
+ ### Internal
134
+
135
+ - **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.
136
+ - **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`).
137
+
138
+ ### Fixed
139
+
140
+ - **A `targetEnd` given as a compact handle (`paragraphId` + `fingerprint`, no numeric `index`) was silently ignored** for a
141
+ plain `redline`, so only the first paragraph of the range was replaced and the rest stayed in the accepted text. The range is now
142
+ resolved from the end handle; a range that crosses a table or other container boundary is refused `TARGET_RANGE_INVALID`.
143
+ - **Range replace over empty paragraphs no longer fails receipt reconciliation:** a replacement whose diff deletes only paragraph
144
+ breaks recorded a paired deletion revision id in the receipt that was never written, so the operation was refused with
145
+ `RECEIPT_RECONCILIATION_FAILED`. Paired replacement ids are now recorded only when written. The reject-roundtrip matrix
146
+ treats that code as a failure, not a skipped refusal.
147
+ - **Plain `redline` keeps `w14:paraId` / `w14:textId`:** reconstruction used to rebuild the paragraph without its identity
148
+ attributes; the rebuilt paragraph that survives for a source paragraph (not tracked-deleted) now keeps them, once (an inserted
149
+ split gives them to the paragraph whose mark Reject All keeps). A composed `redline` + `list-format` pair keeps identity.
150
+ - **Text-changed header conversion with direct `keepNext` / spacing is no longer refused `REJECT_PARAGRAPH_PROPS`:** the list
151
+ fallback's tracked-deleted source paragraph keeps its original `pPr`; the new list paragraph carries the direct properties and
152
+ the source `w14:paraId`.
153
+ - **`ReconciliationPipeline.executeTableGeneration` inserts a tracked table block:** the public pipeline method generated
154
+ tracked cell text in untracked rows (the defect `engine/table-mode.js` had); rows now carry `w:trPr/w:ins` so Reject All
155
+ removes the table.
156
+ - **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.)
157
+ - **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.
158
+ - **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.
159
+ - **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.
160
+ - **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).
161
+ - **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.
162
+ - **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`.
163
+ - **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.
164
+ - **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.
165
+ - **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.
166
+ - **`generateRedlines: false` stays untracked under `slice-cross-author`:** the surgical insert computed its nested-revision flag as
167
+ `!(generateRedlines && ...)`, which is true whenever `generateRedlines` is false, so an inserted word was wrapped in a new tracked
168
+ `w:ins` (`applyRedlineToOxml` and `applyOperationsToDocumentXml`; `redline`/`replace`, `modified`/`replacements`). It now writes
169
+ plain runs, like `merge-same-author`, and leaves other authors' revisions intact (a foreign insertion is sliced around the edit).
170
+ - **Table appended after the final paragraph of a pretty-printed document keeps its tracked trailing paragraph:** a whitespace
171
+ text node between the last paragraph and `w:sectPr` made the retained-paragraph table append believe the table did not end
172
+ the body, so the body ended with a table and Word added an untracked empty paragraph that Reject All left behind (plugin
173
+ report R5; Word differential 5 vs 4 paragraphs). The append now skips non-element nodes when deciding. The Word-semantics
174
+ simulator (and therefore the post-apply verifier) counts the untracked paragraph Word supplies after a body-final table, so
175
+ the old output is refused with `REVISION_FIDELITY_VIOLATION`. Documents without whitespace between blocks (the plugin's
176
+ own) were already correct on `Revisions.RejectAll()` and the ribbon; `Document.RejectAllRevisions()` still leaves the
177
+ tracked-inserted last mark behind (a Word API quirk, see the fidelity plan).
178
+
3
179
  ## 0.8.4
4
180
 
5
181
  ### Added