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