@ansonlai/docx-redline-js 0.5.3 → 0.5.4
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/AGENTS.md +46 -12
- package/ARCHITECTURE.md +38 -3
- package/CHANGELOG.md +6 -0
- package/README.md +37 -10
- package/core/paragraph-revision-safety.js +10 -8
- package/core/redline-validation.js +7 -4
- package/core/revision-cloning.js +21 -0
- package/core/validation-delta.js +23 -0
- package/dist/docx-redline-js.esm.js +164 -14
- package/dist/docx-redline-js.esm.js.map +2 -2
- package/dist/docx-redline-js.esm.min.js +79 -79
- package/dist/docx-redline-js.esm.min.js.map +3 -3
- package/docs/plans/2026-09-08-cross-author-revision-slicing.md +580 -37
- package/docs/schemas/document-operations.schema.json +11 -1
- package/engine/surgical-mode.js +148 -3
- package/engine/surgical-run-splitting.js +19 -7
- package/engine/surgical-spans.js +2 -1
- package/node/cli.js +128 -13
- package/node/docx-document.js +17 -14
- package/package.json +1 -1
- package/pipeline/diff-engine.js +15 -0
- package/services/document-operation-applier.js +49 -4
- package/services/document-operation-contract.js +40 -0
- package/services/document-operation-mutations.js +353 -36
- package/services/operation-preflight.js +1 -1
- package/services/standalone-operation-runner.d.ts +18 -0
package/AGENTS.md
CHANGED
|
@@ -158,7 +158,7 @@ use explicit restoration intent. A normal `redline` remains fail-closed with
|
|
|
158
158
|
```js
|
|
159
159
|
const restoration = {
|
|
160
160
|
type: 'restore',
|
|
161
|
-
target: { paragraphId: '1A2B3C4D' },
|
|
161
|
+
target: { paragraphId: '1A2B3C4D', revisionView: 'rejected' },
|
|
162
162
|
modified: 'Restored or adjusted paragraph text.',
|
|
163
163
|
author: 'Editor'
|
|
164
164
|
};
|
|
@@ -166,8 +166,29 @@ const restoration = {
|
|
|
166
166
|
|
|
167
167
|
For a contiguous range, provide `targetEnd`/`targetEndRef` and one string per
|
|
168
168
|
source paragraph in `modified`. Restoration always uses tracked changes,
|
|
169
|
-
preserves the deleted source paragraph, and inserts the counterproposal
|
|
170
|
-
|
|
169
|
+
preserves the deleted source paragraph, and inserts the counterproposal after
|
|
170
|
+
the complete deleted source block with a fresh paragraph ID. Unchanged
|
|
171
|
+
pre-existing validation defects remain baseline diagnostics; a restore fails
|
|
172
|
+
with `GENERATED_OOXML_INVALID` only when it introduces a new validation error.
|
|
173
|
+
|
|
174
|
+
To insert run-level text inside content visible only in the rejected view, use
|
|
175
|
+
an explicit rejected-view `insert` operation:
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
const insertion = {
|
|
179
|
+
type: 'insert',
|
|
180
|
+
target: { paragraphId: '1A2B3C4D', revisionView: 'rejected' },
|
|
181
|
+
anchor: { exactText: 'must pay', occurrence: 1, offset: 5 },
|
|
182
|
+
modified: '[clarification] ',
|
|
183
|
+
author: 'Editor',
|
|
184
|
+
existingRevisions: 'slice-cross-author'
|
|
185
|
+
};
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The anchor offset is relative to `anchor.exactText`. Repeated anchors require an
|
|
189
|
+
explicit `occurrence`. The engine preserves the foreign deletion as sibling
|
|
190
|
+
`w:del` carriers around a top-level `w:ins`; unsupported comments, bookmarks,
|
|
191
|
+
fields, hyperlinks, moves, or non-text split boundaries fail closed.
|
|
171
192
|
|
|
172
193
|
`targetRef` is an optional 1-based paragraph reference used to disambiguate
|
|
173
194
|
duplicate text. An operation-level `author` overrides the batch author; batch
|
|
@@ -293,8 +314,9 @@ Key CLI defaults and behaviors:
|
|
|
293
314
|
- **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.
|
|
294
315
|
- **Overwrite behavior**: Destination files provided via `--output` overwrite by default. To protect existing destination files, pass `--no-overwrite` or `--no-clobber`. The source document is never overwritten unless `--in-place` is specified.
|
|
295
316
|
- **Tracked changes**: Defaults to `generateRedlines: true`. When clean direct text is needed, pass `--no-redlines`.
|
|
296
|
-
- **Atomic rollback (optional)**: Operations apply progressively by default (`atomic: false`). For all-or-nothing transactional rollback where any error halts and reverts all changes, pass `--atomic`.
|
|
297
|
-
-
|
|
317
|
+
- **Atomic rollback (optional)**: Operations apply progressively by default (`atomic: false`). For all-or-nothing transactional rollback where any error halts and reverts all changes, pass `--atomic`.
|
|
318
|
+
- **Compact mutation JSON**: `apply`, `accept`, `reject`, and `delete-comments` omit document/package XML and full validation arrays. `validation.originalIssues` and `validation.generatedIssues` are code/count summaries; run `validate` for full issue records.
|
|
319
|
+
- Check `completion: true`, `written: true`, and a non-null `outputPath` on stdout. `completion` is derived from the write result, top-level status, and every operation status, so failed, partial, and unwritten work cannot appear complete. If an error occurs, inspect `error.code` or `results[i].error.code` (e.g. `TARGET_NOT_FOUND`, `ANCHOR_NOT_FOUND`) before correcting the cause and re-applying.
|
|
298
320
|
|
|
299
321
|
For multi-clause or multi-page reviews, apply edits **section-by-section** or clause-by-clause (e.g., using `--in-place` on a working copy) rather than bundling dozens of edits into one massive batch. This keeps context compact, simplifies error diagnosis, and prevents cascading anchor drift.
|
|
300
322
|
|
|
@@ -412,12 +434,14 @@ When the CLI or runner returns an error code, follow these specific recovery act
|
|
|
412
434
|
|---|---|---|
|
|
413
435
|
| `TARGET_NOT_FOUND` | Target text did not match any paragraph. | **Do NOT retry with paraphrased text.** Re-run `extract`/`inspect`, copy `exactText` verbatim (including exact whitespace/punctuation), and add a discriminator (`paragraphId`, `fingerprint`, or `occurrence`). |
|
|
414
436
|
| `AMBIGUOUS_TARGET` | Multiple paragraphs match identical text. | Disambiguate by supplying `paragraphId`, `fingerprint`, `occurrence`, or `index` in the target descriptor. |
|
|
415
|
-
| `ANCHOR_NOT_FOUND` / `AMBIGUOUS_ANCHOR` |
|
|
437
|
+
| `ANCHOR_NOT_FOUND` / `AMBIGUOUS_ANCHOR` | A comment or rejected-view insertion anchor was not uniquely matched. | For comments, narrow `textToComment` or omit it to anchor the whole paragraph. For rejected-view insertion, copy exact rejected text and provide `anchor.occurrence`. |
|
|
416
438
|
| `OVERLAPPING_TEXT_EDITS` | Multiple operations target the same paragraph concurrently. | Consolidate all changes to the same paragraph into a single `redline` or `replace` operation. |
|
|
417
439
|
| `EXISTING_REVISIONS` | Target paragraph contains tracked changes from another author. | Fails closed to protect third-party review marks. If editing inside that reviewer's pending insertion is intended, pass `--existing-revisions slice-cross-author` (or `existingRevisions: 'slice-cross-author'`). Do not pass `accept-all-first` without explicit user authorization. |
|
|
418
440
|
| `PATCH_ROUNDTRIP_MISMATCH` | A cross-author surgical edit did not reconstruct the requested modified text exactly. | Treat the operation as unapplied. Re-extract the exact paragraph text and split the edit into a narrower operation that does not cross the reported structural boundary. |
|
|
419
441
|
| `FOREIGN_PARAGRAPH_MARK_DELETION` | A normal edit attempted to write into a paragraph wholly deleted by another reviewer. | Use an explicit `restore` operation if the user intends to counterpropose that paragraph; otherwise leave the deletion unresolved. |
|
|
420
442
|
| `RESTORATION_STATE_REQUIRED` / `RESTORATION_COUNT_MISMATCH` | A `restore` target is not a wholly foreign-deleted paragraph, or its replacement count does not match the paragraph range. | Re-inspect the document and target the deleted paragraph by stable descriptor; provide exactly one replacement string per source paragraph. |
|
|
443
|
+
| `REJECTED_INSERTION_STATE_REQUIRED` / `UNSAFE_REVISION_BOUNDARY` | An explicit rejected-view insertion did not resolve to supported plain run text inside a wholly foreign-deleted paragraph. | Do not fall back to a generic edit. Narrow the exact anchor/offset, or handle comments, bookmarks, fields, hyperlinks, moves, or other structural boundaries manually. |
|
|
444
|
+
| `GENERATED_OOXML_INVALID` | The operation introduced a new validation error relative to its baseline. | Treat the operation as unapplied and inspect `generatedIssues`; correct the generating operation or builder rather than repairing or accepting the source document's unrelated baseline defects. |
|
|
421
445
|
| `UNSAFE_DELETED_TABLE_ROW` / `UNSUPPORTED_MOVE_REVISION` / `SECTION_BREAK_PARAGRAPH` / `UNSAFE_PARAGRAPH_PLACEMENT` | Paragraph restoration cannot preserve the source structural boundary safely. | Do not retry as an ordinary redline. Resolve the row/move/section/placement condition manually or narrow the restoration to a safe paragraph. |
|
|
422
446
|
| `COMMENTED_CONTENT_MERGE` / `COMMENTED_CONTENT_DELETE` | Operation would overwrite, revert, or delete content with comments. | Fails closed to prevent orphaned comment threads. Report the comment author and text to the user; resolve the comment before re-editing. |
|
|
423
447
|
| `INVALID_OPERATION` | Operation object violates schema or has incompatible fields. | Validate the JSON structure against [`document-operations.schema.json`](file:///c:/Users/Phara/Desktop/Projects/Docx%20Redline%20JS/docs/schemas/document-operations.schema.json) before targeting is attempted. |
|
|
@@ -601,12 +625,22 @@ retain their string-only return type and return `''` for parse failures.
|
|
|
601
625
|
|
|
602
626
|
### Target text versus replacement text
|
|
603
627
|
|
|
604
|
-
Target resolution may normalize surrounding or repeated whitespace while
|
|
605
|
-
matching a paragraph. Replacement text is not normalized: tabs, line breaks,
|
|
606
|
-
non-breaking spaces, repeated spaces, and leading/trailing whitespace become
|
|
607
|
-
part of the requested edit. When editing extracted document text, copy the
|
|
608
|
-
exact paragraph text and modify it in place rather than round-tripping it
|
|
609
|
-
through a formatter that may change whitespace.
|
|
628
|
+
Target resolution may normalize surrounding or repeated whitespace while
|
|
629
|
+
matching a paragraph. Replacement text is not normalized: tabs, line breaks,
|
|
630
|
+
non-breaking spaces, repeated spaces, and leading/trailing whitespace become
|
|
631
|
+
part of the requested edit. When editing extracted document text, copy the
|
|
632
|
+
exact paragraph text and modify it in place rather than round-tripping it
|
|
633
|
+
through a formatter that may change whitespace.
|
|
634
|
+
|
|
635
|
+
The normalized caller target is never used for mutation offsets in a text-bearing
|
|
636
|
+
edit. After target selection, the engine uses the resolved paragraph's byte-exact
|
|
637
|
+
JavaScript string as the source coordinate system. The legacy format-only
|
|
638
|
+
fallback for field-code paragraphs with no extractable accepted-view spans is
|
|
639
|
+
not a text-replacement path. If an ASCII-space target selected an NBSP
|
|
640
|
+
source, `resolvedTarget.targetTextMatch` reports `space_equivalent`, escaped
|
|
641
|
+
source/request excerpts, and differing code points. An NBSP-to-space request is
|
|
642
|
+
tracked as a replacement; it must not retain the NBSP and append another space.
|
|
643
|
+
The CLI keeps these bounded diagnostics but removes resolved clause text.
|
|
610
644
|
|
|
611
645
|
For ordinary insertions and deletions, target the visible accepted view:
|
|
612
646
|
inserted `w:t` text is visible and deleted `w:delText` is not. Move revisions
|
package/ARCHITECTURE.md
CHANGED
|
@@ -94,6 +94,12 @@ No Word add-in entrypoints or host-specific integration layers are part of this
|
|
|
94
94
|
fingerprints, document order, and table context for deterministic reuse.
|
|
95
95
|
- `core/redline-validation.js`
|
|
96
96
|
- Runtime structural validation (`validateRedlineOoxml`) mirroring the test-suite invariants: no nested revisions, `w:delText` inside `w:del`, complete revision metadata, unique revision ids, preserved boundary whitespace.
|
|
97
|
+
- `core/validation-delta.js`
|
|
98
|
+
- Stable issue signatures and multiset subtraction for classifying baseline
|
|
99
|
+
versus generated validation issues without hiding added duplicate errors.
|
|
100
|
+
- `core/revision-cloning.js`
|
|
101
|
+
- Shared effective-property cloning strips historical revision descendants;
|
|
102
|
+
intentional revision-bearing splits refresh cloned property-change IDs.
|
|
97
103
|
- `engine/oxml-engine.js`
|
|
98
104
|
- Main reconciliation router, mode selection, existing-revision policy gate, and status/error result handling.
|
|
99
105
|
- `engine/route-selection.js`
|
|
@@ -102,7 +108,10 @@ No Word add-in entrypoints or host-specific integration layers are part of this
|
|
|
102
108
|
- `engine/run-builders.js`
|
|
103
109
|
- Shared builders for insertion/deletion wrappers, paragraph-mark revisions, visible run content, and run-property changes.
|
|
104
110
|
- `engine/surgical-*.js`
|
|
105
|
-
- Surgical run splitting, diff application, and span helpers for localized edits that preserve surrounding markup.
|
|
111
|
+
- 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.
|
|
112
|
+
The shared carrier splitter also handles explicit rejected-view deletion
|
|
113
|
+
splits, retaining `w:delText`, tabs, breaks, soft/non-breaking hyphens,
|
|
114
|
+
formatting, foreign metadata, and fresh trailing/property-change IDs.
|
|
106
115
|
- `engine/formatting-removal.js`
|
|
107
116
|
- Shared formatting removal and highlight helpers.
|
|
108
117
|
- `pipeline/list-markers.js`
|
|
@@ -130,10 +139,21 @@ No Word add-in entrypoints or host-specific integration layers are part of this
|
|
|
130
139
|
preflight, single-operation application, batch application, and scheduling.
|
|
131
140
|
- `services/document-operation-applier.js`
|
|
132
141
|
- Canonical single-operation validation, author resolution, dispatch, and
|
|
133
|
-
result metadata assembly.
|
|
142
|
+
result metadata assembly. Before commit it validates the entire live
|
|
143
|
+
document against the operation savepoint and refuses newly generated
|
|
144
|
+
structural errors, including duplicate revision IDs.
|
|
134
145
|
- `services/document-operation-mutations.js`
|
|
135
146
|
- Coupled OOXML mutation implementations for redline, highlight, and comment
|
|
136
|
-
operations.
|
|
147
|
+
operations. For text-bearing edits, the resolved paragraph's exact
|
|
148
|
+
accepted-view text is the source coordinate system even when target
|
|
149
|
+
selection used normalized whitespace;
|
|
150
|
+
bounded match-mode/code-point diagnostics are attached to resolved target
|
|
151
|
+
metadata. Explicit rejected-view insertion splits a supported direct
|
|
152
|
+
foreign `w:del` at an anchor-relative offset into sibling deletion,
|
|
153
|
+
insertion, and deletion carriers. Paragraph restoration emits its inserted
|
|
154
|
+
block after the untouched source range and verifies baseline-delta,
|
|
155
|
+
mutation-envelope, and lifecycle postconditions. These use leaf-module
|
|
156
|
+
imports and never import the root entry.
|
|
137
157
|
- `services/batch-operation-orchestrator.js`
|
|
138
158
|
- Comment-first stable scheduling, atomic policy, artifact aggregation,
|
|
139
159
|
per-operation results, one final document serialization, and deferred
|
|
@@ -157,11 +177,16 @@ No Word add-in entrypoints or host-specific integration layers are part of this
|
|
|
157
177
|
revision authors, table context, and advisory visible numbering.
|
|
158
178
|
- `node/docx-document.js`
|
|
159
179
|
- Transactional whole-DOCX editing, artifact wiring, validation, and rollback.
|
|
180
|
+
OOXML and package issues are classified as baseline/generated multisets, so
|
|
181
|
+
unchanged source defects remain diagnostics while new defects block writes.
|
|
160
182
|
This surface is excluded from the browser/root dependency graph.
|
|
161
183
|
- `node/cli.js` and `bin/docx-redline.js`
|
|
162
184
|
- Cross-platform, JSON-only agent command boundary. Read commands never
|
|
163
185
|
mutate; write commands require attribution, use package transactions, and
|
|
164
186
|
only overwrite source files under explicit `--in-place` authorization.
|
|
187
|
+
Mutation commands expose a compact contract: package/XML payloads and full
|
|
188
|
+
validation arrays remain internal, while stdout contains durability fields,
|
|
189
|
+
per-operation evidence, validation counts, and a derived completion flag.
|
|
165
190
|
- `orchestration/*`
|
|
166
191
|
- Route planning and list fallback orchestration utilities.
|
|
167
192
|
|
|
@@ -309,6 +334,16 @@ still be re-exported from `index.js`.
|
|
|
309
334
|
- Operation-level authors override the batch author. Runtime results expose
|
|
310
335
|
`authorUsed`, `authorsUsed`, `operationType`, `resolvedBy`, and resolved target
|
|
311
336
|
metadata so integrations can audit what the engine actually selected.
|
|
337
|
+
- A normalized target match does not become an edit coordinate system for a
|
|
338
|
+
text-bearing mutation. Mutation uses canonical accepted-view source text, and `resolvedTarget.targetTextMatch`
|
|
339
|
+
records `exact`, `space_equivalent`, or `normalized` selection plus bounded
|
|
340
|
+
invisible-character diagnostics.
|
|
341
|
+
- CLI contract version 3 is deliberately narrower than library result objects.
|
|
342
|
+
`apply`, `accept`, `reject`, and `delete-comments` omit `documentXml`, package
|
|
343
|
+
parts, inspection text, and full issue arrays. `completion` is true only when
|
|
344
|
+
`written === true`, top-level status is neither error nor partial, and every
|
|
345
|
+
operation result is non-error. The `validate` command remains the full issue
|
|
346
|
+
reporting surface.
|
|
312
347
|
- `preflightOperations` is the read-only safety boundary for agent-generated
|
|
313
348
|
batches. It uses strict targeting by default; mutation APIs retain permissive
|
|
314
349
|
legacy targeting unless `strictTargets: true` is requested. In v1.0.0,
|
package/CHANGELOG.md
CHANGED
|
@@ -5,10 +5,16 @@
|
|
|
5
5
|
### Safety Fixes
|
|
6
6
|
|
|
7
7
|
- **Foreign deleted-paragraph resurrection guard**: Refuses non-empty same-paragraph edits when another author owns the paragraph-mark deletion and all existing paragraph content is deleted. The operation now returns `FOREIGN_PARAGRAPH_MARK_DELETION` with the owning author instead of emitting lifecycle-unsafe OOXML; atomic document operations roll back byte-for-byte. `validateRedlineOoxml` reports already-authored instances as warnings.
|
|
8
|
+
- **Source-truth whitespace replacement alignment (WP09a)**: Strict target resolution may equate ordinary spaces with NBSPs, but mutation offsets now always come from the resolved paragraph's exact accepted view. Space-equivalent word replacement hunks are refined to character-local edits, and multiple plain-text edits are applied right-to-left against refreshed live spans. This tracks NBSP-to-space substitutions exactly while preserving unchanged hyperlinks and prevents earlier run splits from invalidating later anchors.
|
|
9
|
+
- **Exact mismatch diagnostics**: `PATCH_ROUNDTRIP_MISMATCH` now includes the expected and actual code points at the first mismatch. Document-operation results also report bounded `targetTextMatch` diagnostics when target selection used equivalent whitespace.
|
|
10
|
+
- **Baseline-delta validation (WP09c)**: Restoration, operation, and package validation now compare issue multisets against the source. Unchanged legacy defects remain visible without blocking safe work, while any added occurrence or mutation-envelope error fails closed with `GENERATED_OOXML_INVALID` before an operation is reported as applied.
|
|
11
|
+
- **Revision identity sanitation (WP09d)**: New paragraph/list builders inherit effective `pPr`/`rPr` formatting without cloning historical revision descendants. Operation-level whole-document validation catches duplicate revision IDs and rolls back the operation savepoint before committing its receipt.
|
|
8
12
|
|
|
9
13
|
### New Features
|
|
10
14
|
|
|
11
15
|
- **Explicit paragraph restoration (`type: 'restore'`)**: Restores or counterproposes another reviewer's pending whole-paragraph deletion as a separately tracked sibling paragraph. The source deletion remains untouched; the restored paragraph receives its own inserted paragraph mark, content insertion, sanitized paragraph properties, and fresh `w14:paraId`. Single paragraphs and contiguous ranges are supported, with full Accept/Reject lifecycle verification and structured refusals at unsafe table-row, move, section-break, and terminal-paragraph boundaries.
|
|
16
|
+
- **Compact mutation CLI contract (WP09b)**: CLI contract version 3 removes `documentXml`, OOXML/package artifacts, full inspection data, and full validation issue arrays from normal `apply`, `accept`, `reject`, and `delete-comments` stdout. Mutation responses retain actionable errors, per-operation receipts, output durability fields, code/count validation summaries, and a derived `completion` flag that cannot report success for failed, partial, or unwritten work.
|
|
17
|
+
- **Word-native deleted-section editing (WP09e)**: An explicit rejected-view `insert` operation can split a foreign deletion at an exact anchor-relative offset into sibling `del(A) / ins(B) / del(A)` carriers. Contiguous paragraph restorations now follow their untouched deleted source block, matching Microsoft Word's ordering. Ambiguous anchors and unsupported structural split boundaries remain fail-closed.
|
|
12
18
|
|
|
13
19
|
## 0.5.1
|
|
14
20
|
|
package/README.md
CHANGED
|
@@ -159,13 +159,14 @@ docx-redline apply contract.docx --target "Another author's clause" --modified "
|
|
|
159
159
|
docx-redline apply contract.docx --operations operations.json --atomic --output reviewed.docx
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
-
All commands emit JSON on stdout. `apply` defaults:
|
|
162
|
+
All commands emit JSON on stdout. `apply` defaults:
|
|
163
163
|
- **Author**: Defaults to `'AI Redliner'` (or `DOCX_REDLINE_AUTHOR` environment variable).
|
|
164
164
|
- **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.
|
|
165
165
|
- **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.
|
|
166
166
|
- **Transactionality**: Defaults to `atomic: false` (applies valid operations and reports any failures). Pass `--atomic` for all-or-nothing rollback on any operation error.
|
|
167
167
|
- **Tracked changes**: Defaults to `generateRedlines: true`. Pass `--no-redlines` when clean direct text edits are desired.
|
|
168
|
-
- **Inline edits**: Use `--target <text>` with `--modified <text>` or `--comment <text>` for quick one-liners without creating a JSON file.
|
|
168
|
+
- **Inline edits**: Use `--target <text>` with `--modified <text>` or `--comment <text>` for quick one-liners without creating a JSON file.
|
|
169
|
+
- **Compact mutation results**: `apply`, `accept`, `reject`, and `delete-comments` omit full OOXML/package payloads and inspection text from stdout. They report `written`, `outputPath`, per-operation results and receipts, compact validation counts, and a derived `completion` boolean. Use `validate` when full issue arrays are needed.
|
|
169
170
|
|
|
170
171
|
See [the agent workflow in AGENTS.md](./AGENTS.md#agent-document-workflow-cli) and the
|
|
171
172
|
[operation JSON Schema](docs/schemas/document-operations.schema.json).
|
|
@@ -203,8 +204,9 @@ Common result fields:
|
|
|
203
204
|
|-------|---------|
|
|
204
205
|
| `status` | Operation status: `'ok'`, `'partial'`, `'no-op'`, or `'error'`. |
|
|
205
206
|
| `error` | Present on failure; includes a stable `code` such as `PARSE_ERROR`, `TARGET_NOT_FOUND`, `PARTIAL_TARGET`, `EXISTING_REVISIONS`, `DIFF_TOKEN_LIMIT`, or `BATCH_OPERATION_FAILED`. |
|
|
206
|
-
| `written` | CLI/facade boolean indicating whether the output file was successfully written to disk. |
|
|
207
|
-
| `
|
|
207
|
+
| `written` | CLI/facade boolean indicating whether the output file was successfully written to disk. |
|
|
208
|
+
| `completion` | CLI-only boolean that is `true` only when a destination was written, top-level status is neither error nor partial, and no operation result failed. |
|
|
209
|
+
| `rolledBack` | Present and `true` when an atomic batch encountered an error and rolled back all changes. |
|
|
208
210
|
|
|
209
211
|
Word diffs are deterministic by default (no wall-clock timeout). Inputs above
|
|
210
212
|
the safe ceiling of 262,144 unique diff tokens return `DIFF_TOKEN_LIMIT` with
|
|
@@ -216,11 +218,11 @@ silent text loss.
|
|
|
216
218
|
During multi-round legal negotiations, a reviewer often needs to edit text that was previously inserted by another reviewer whose revision is still pending. Pass `existingRevisions: 'slice-cross-author'` (or `--existing-revisions slice-cross-author` via CLI) to edit inside another author's pending insertion without erasing their attribution or requiring prior acceptance:
|
|
217
219
|
|
|
218
220
|
```js
|
|
219
|
-
const result = await applyRedlineToOxml(paragraphOoxml, originalText, modifiedText, {
|
|
220
|
-
generateRedlines: true,
|
|
221
|
-
author: '
|
|
222
|
-
existingRevisions: 'slice-cross-author'
|
|
223
|
-
});
|
|
221
|
+
const result = await applyRedlineToOxml(paragraphOoxml, originalText, modifiedText, {
|
|
222
|
+
generateRedlines: true,
|
|
223
|
+
author: 'Reviewer B',
|
|
224
|
+
existingRevisions: 'slice-cross-author'
|
|
225
|
+
});
|
|
224
226
|
```
|
|
225
227
|
|
|
226
228
|
The engine applies Microsoft Word Desktop-native tracked change structures:
|
|
@@ -346,6 +348,12 @@ If a structural boundary prevents exact reconstruction, the transform returns
|
|
|
346
348
|
Pure insertion-only slicing uses an exact character-local diff so repeated words
|
|
347
349
|
cannot move an insertion to a different occurrence. Leading/trailing spaces,
|
|
348
350
|
tabs, and non-breaking spaces are treated as real changes rather than no-ops.
|
|
351
|
+
For text-bearing replacements, the exact accepted-view text of the resolved
|
|
352
|
+
paragraph—not a space-normalized caller target—defines mutation offsets. Word-level replacement
|
|
353
|
+
hunks that differ only by ordinary spaces and NBSPs are refined to character
|
|
354
|
+
edits so unchanged hyperlinks and their relationship attributes stay in place.
|
|
355
|
+
Runner results expose bounded `resolvedTarget.targetTextMatch` code-point
|
|
356
|
+
diagnostics when equivalent whitespace was used to identify the target.
|
|
349
357
|
|
|
350
358
|
### Deep Imports
|
|
351
359
|
|
|
@@ -533,11 +541,30 @@ const restoration = await applyOperationToDocumentXml(
|
|
|
533
541
|
documentXml,
|
|
534
542
|
{
|
|
535
543
|
type: 'restore',
|
|
536
|
-
target: { paragraphId: '1A2B3C4D' },
|
|
544
|
+
target: { paragraphId: '1A2B3C4D', revisionView: 'rejected' },
|
|
537
545
|
modified: 'Restored or adjusted paragraph text.'
|
|
538
546
|
},
|
|
539
547
|
'Editor'
|
|
540
548
|
);
|
|
549
|
+
|
|
550
|
+
// A single restoration follows its deleted source paragraph. A range
|
|
551
|
+
// restoration follows the complete deleted source block. Unchanged legacy
|
|
552
|
+
// validation defects are retained as baseline issues; newly generated errors
|
|
553
|
+
// fail closed before commit.
|
|
554
|
+
|
|
555
|
+
// To insert run-level text at a location visible only in the rejected view,
|
|
556
|
+
// provide explicit rejected-view intent and an exact anchor-relative offset.
|
|
557
|
+
const deletedTextInsertion = await applyOperationToDocumentXml(
|
|
558
|
+
documentXml,
|
|
559
|
+
{
|
|
560
|
+
type: 'insert',
|
|
561
|
+
target: { paragraphId: '1A2B3C4D', revisionView: 'rejected' },
|
|
562
|
+
anchor: { exactText: 'must pay', occurrence: 1, offset: 5 },
|
|
563
|
+
modified: '[clarification] ',
|
|
564
|
+
existingRevisions: 'slice-cross-author'
|
|
565
|
+
},
|
|
566
|
+
'Editor'
|
|
567
|
+
);
|
|
541
568
|
|
|
542
569
|
// applyOperationToDocumentXml(...) returns a full w:document payload.
|
|
543
570
|
zip.file('word/document.xml', opResult.documentXml);
|
|
@@ -128,7 +128,7 @@ export function inspectForeignDeletedParagraphTarget(paragraph, mutationAuthor)
|
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
export function getParagraphRestorationRefusal(paragraph) {
|
|
131
|
+
export function getParagraphRestorationRefusal(paragraph, options = {}) {
|
|
132
132
|
const pPr = directChild(paragraph, 'pPr');
|
|
133
133
|
if (directChild(pPr, 'sectPr')) {
|
|
134
134
|
return {
|
|
@@ -169,13 +169,15 @@ export function getParagraphRestorationRefusal(paragraph) {
|
|
|
169
169
|
};
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
172
|
+
if (options.requireFollowingParagraph !== false) {
|
|
173
|
+
let sibling = paragraph?.nextSibling || null;
|
|
174
|
+
while (sibling && (sibling.nodeType !== 1 || localNameOf(sibling) !== 'p')) sibling = sibling.nextSibling;
|
|
175
|
+
if (!sibling) {
|
|
176
|
+
return {
|
|
177
|
+
code: 'UNSAFE_PARAGRAPH_PLACEMENT',
|
|
178
|
+
message: 'Refusing to restore a deleted paragraph without a following paragraph in the same structural container.'
|
|
179
|
+
};
|
|
180
|
+
}
|
|
179
181
|
}
|
|
180
182
|
|
|
181
183
|
return null;
|
|
@@ -65,19 +65,22 @@ function parseOoxmlForValidation(oxml) {
|
|
|
65
65
|
* Issue severities: 'error' issues indicate output Word may repair or
|
|
66
66
|
* mis-resolve; 'warning' issues are suspicious but tolerated by Word.
|
|
67
67
|
*
|
|
68
|
-
* @param {string} oxml - OOXML string
|
|
68
|
+
* @param {string|Document|Element} oxml - OOXML string or an already-parsed DOM
|
|
69
69
|
* @returns {{ valid: boolean, issues: Array<{ code: string, severity: 'error'|'warning', message: string }> }}
|
|
70
70
|
*/
|
|
71
71
|
export function validateRedlineOoxml(oxml) {
|
|
72
72
|
const issues = [];
|
|
73
73
|
const addIssue = (code, severity, message) => issues.push({ code, severity, message });
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
const isDomNode = oxml && typeof oxml === 'object' && (oxml.nodeType === 9 || oxml.nodeType === 1);
|
|
76
|
+
if (!isDomNode && (typeof oxml !== 'string' || oxml.trim() === '')) {
|
|
77
|
+
addIssue('PARSE_ERROR', 'error', 'Input is not non-empty OOXML or a parsed XML DOM.');
|
|
77
78
|
return { valid: false, issues };
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
const { doc, error } =
|
|
81
|
+
const { doc, error } = isDomNode
|
|
82
|
+
? { doc: oxml.nodeType === 9 ? oxml : oxml.ownerDocument }
|
|
83
|
+
: parseOoxmlForValidation(oxml);
|
|
81
84
|
if (!doc) {
|
|
82
85
|
addIssue('PARSE_ERROR', 'error', `OOXML does not parse as XML: ${error}`);
|
|
83
86
|
return { valid: false, issues };
|
package/core/revision-cloning.js
CHANGED
|
@@ -6,6 +6,26 @@ import {
|
|
|
6
6
|
} from './types.js';
|
|
7
7
|
import { isWordElement } from './word-xml.js';
|
|
8
8
|
|
|
9
|
+
const REVISION_HISTORY_ELEMENTS = new Set([
|
|
10
|
+
'ins', 'del', 'moveFrom', 'moveTo', 'pPrChange', 'rPrChange',
|
|
11
|
+
'tblPrChange', 'trPrChange', 'tcPrChange', 'sectPrChange'
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Clones effective paragraph/run properties without copying tracked history
|
|
16
|
+
* into a newly created paragraph or run.
|
|
17
|
+
*/
|
|
18
|
+
export function clonePropertiesWithoutRevisionHistory(root) {
|
|
19
|
+
if (!root) return null;
|
|
20
|
+
const clone = root.cloneNode(true);
|
|
21
|
+
const candidates = [clone, ...Array.from(clone.getElementsByTagName?.('*') || [])];
|
|
22
|
+
for (const node of candidates.reverse()) {
|
|
23
|
+
if (!REVISION_HISTORY_ELEMENTS.has(String(node.localName || node.nodeName || '').replace(/^.*:/, ''))) continue;
|
|
24
|
+
node.parentNode?.removeChild(node);
|
|
25
|
+
}
|
|
26
|
+
return clone;
|
|
27
|
+
}
|
|
28
|
+
|
|
9
29
|
/**
|
|
10
30
|
* Assigns fresh document-scoped IDs to w:rPrChange elements in a cloned
|
|
11
31
|
* run-properties subtree. This preserves formatting-revision metadata while
|
|
@@ -32,6 +52,7 @@ export function refreshRunPropertyChangeIds(root, allocator = null) {
|
|
|
32
52
|
} else {
|
|
33
53
|
node.setAttribute('w:id', nextId);
|
|
34
54
|
}
|
|
55
|
+
resolvedAllocator._receiptCollector?.recordRevision(Number(nextId), 'rPrChange');
|
|
35
56
|
}
|
|
36
57
|
|
|
37
58
|
return root;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function issueKey(issue) {
|
|
2
|
+
return `${issue?.source || ''}\u001f${issue?.severity || ''}\u001f${issue?.code || ''}\u001f${issue?.message || ''}`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/** Subtracts baseline validation issues as a multiset, preserving duplicates. */
|
|
6
|
+
export function subtractValidationIssueMultiset(outputIssues = [], baselineIssues = []) {
|
|
7
|
+
const remaining = new Map();
|
|
8
|
+
for (const issue of baselineIssues || []) {
|
|
9
|
+
const key = issueKey(issue);
|
|
10
|
+
remaining.set(key, (remaining.get(key) || 0) + 1);
|
|
11
|
+
}
|
|
12
|
+
return (outputIssues || []).filter(issue => {
|
|
13
|
+
const key = issueKey(issue);
|
|
14
|
+
const count = remaining.get(key) || 0;
|
|
15
|
+
if (count === 0) return true;
|
|
16
|
+
remaining.set(key, count - 1);
|
|
17
|
+
return false;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validationErrors(issues = []) {
|
|
22
|
+
return (issues || []).filter(issue => issue?.severity === 'error');
|
|
23
|
+
}
|