@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.
- package/ARCHITECTURE.md +221 -12
- package/CHANGELOG.md +176 -0
- package/README.md +125 -12
- package/core/current-properties.js +136 -0
- package/core/paragraph-text.js +211 -209
- package/core/types.js +9 -3
- package/core/word-revision-semantics.js +256 -0
- package/dist/docx-redline-js.esm.js +4189 -767
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +111 -108
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/dist/docx-redline.bundle.cjs +4292 -870
- package/dist/docx-redline.bundle.cjs.map +4 -4
- package/dist/docx-redline.bundle.js +4292 -870
- package/dist/docx-redline.bundle.js.map +4 -4
- package/docs/AGENT_FAST_START.md +2 -0
- package/docs/AGENT_KNOWLEDGE_BASE.md +19 -3
- package/docs/SKILL_AUTHORING.md +165 -150
- package/docs/TESTING.md +1142 -1059
- package/docs/schemas/document-operations.schema.json +54 -1
- package/document/docx-document.js +1022 -1010
- package/engine/oxml-engine.js +736 -749
- package/engine/paragraph-mark-revisions.js +292 -0
- package/engine/reconstruction-mapper.js +312 -311
- package/engine/reconstruction-writer.js +67 -10
- package/engine/rpr-helpers.js +42 -0
- package/engine/run-builders.js +0 -73
- package/engine/surgical-diff-application.js +604 -605
- package/engine/table-mode.js +63 -63
- package/index.d.ts +47 -1
- package/index.js +11 -2
- package/node/cli-help.js +1 -0
- package/node/cli.js +992 -975
- package/orchestration/list-structural-fallback.js +67 -9
- package/package.json +1 -1
- package/pipeline/diff-engine.js +130 -1
- package/pipeline/ingestion-export.js +227 -243
- package/pipeline/ingestion-paragraph.js +93 -98
- package/pipeline/list-generation.js +415 -419
- package/pipeline/pipeline.js +206 -204
- package/scripts/export-validation-fixtures.mjs +19 -6
- package/scripts/generate-word-revision-semantics-matrix.mjs +134 -0
- package/scripts/lib/word-coverage-catalogue.mjs +7 -2
- package/scripts/render-multilevel-bullet-visual.ps1 +2 -2
- package/scripts/word-com-corpus-visual-suite.ps1 +2 -2
- package/scripts/word-com-differential.ps1 +443 -335
- package/scripts/word-com-visual-suite.ps1 +2 -2
- package/scripts/word-revision-semantics-matrix.ps1 +108 -0
- package/services/batch-operation-orchestrator.js +23 -3
- package/services/capture-engine.js +228 -226
- package/services/document-operation-applier.js +213 -0
- package/services/document-operation-contract.js +75 -0
- package/services/document-operation-mutations.js +3346 -2721
- package/services/error-recovery.js +17 -0
- package/services/list-conversion-diagnostics.js +296 -0
- package/services/list-format-change.js +586 -0
- package/services/list-format-mutation.js +416 -0
- package/services/list-format.js +305 -0
- package/services/numbering-allocation.js +642 -0
- package/services/numbering-helpers.js +354 -524
- package/services/operation-batch-compiler.js +183 -11
- package/services/operation-preflight.js +1 -0
- package/services/receipt-collector.js +288 -288
- package/services/revision-comment-management.js +38 -5
- package/services/revision-fidelity-verifier.js +498 -0
- package/services/standalone-docx-plumbing.js +627 -631
- package/services/standalone-operation-runner.d.ts +182 -5
- 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,
|
|
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
|
-
-
|
|
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,
|
|
225
|
-
|
|
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.
|
|
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
|
|
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
|
|
343
|
-
|
|
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
|
|
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,
|
|
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-
|
|
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
|