@ansonlai/docx-redline-js 0.5.0 → 0.5.2
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 +76 -15
- package/CHANGELOG.md +24 -1
- package/README.md +43 -11
- package/core/redline-validation.js +11 -5
- package/dist/docx-redline-js.esm.js +363 -55
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +77 -74
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/TESTING.md +18 -0
- package/docs/plans/2026-09-08-cross-author-revision-slicing.md +575 -0
- package/docs/schemas/document-operations.schema.json +1 -1
- package/engine/oxml-engine.js +39 -16
- package/engine/surgical-diff-application.js +203 -19
- package/engine/surgical-mode.js +55 -10
- package/engine/surgical-run-splitting.js +103 -0
- package/index.d.ts +1 -1
- package/node/cli.js +12 -7
- package/package.json +123 -123
- package/pipeline/diff-engine.js +22 -0
- package/scripts/generate-cross-author-slicing-fixtures.ps1 +256 -0
- package/services/batch-operation-orchestrator.js +24 -5
- package/services/document-operation-contract.js +22 -0
- package/services/document-operation-mutations.js +19 -3
- package/services/operation-preflight.js +32 -8
- package/services/revision-comment-management.js +40 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
# Cross-Author Revision Slicing: Nested Revisions via Carrier Splitting
|
|
2
|
+
|
|
3
|
+
**Status:** Completed — WP-01 through WP-07 implemented; final automated verification complete
|
|
4
|
+
**Date:** 2026-09-08
|
|
5
|
+
**Target releases:** v0.6.0–v1.0.0
|
|
6
|
+
**Priority:** Document fidelity, multi-author contract negotiation accuracy, and strict OOXML schema compliance over single-pass simplicity.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. Executive Summary & Findings
|
|
11
|
+
|
|
12
|
+
### The Problem
|
|
13
|
+
During contract review and multi-turn legal negotiations, a reviewer (Author B, e.g., "Lai, Anson") often needs to edit text that was previously inserted by another reviewer (Author A, e.g., "Lai, Barry") whose revision has not yet been accepted.
|
|
14
|
+
|
|
15
|
+
In previous discussions and documentation, it was assumed that deleting or inserting text inside another author's pending insertion was either impossible in WordprocessingML (OOXML) or logically paradoxical. However, empirical inspection of Microsoft Word Desktop proves otherwise:
|
|
16
|
+
|
|
17
|
+
1. **Word Desktop supports deletions inside pending insertions**: When Author B deletes text inside Author A's pending `<w:ins>`, Word Desktop displays Author B's deletion visibly (strikethrough formatting, deletion tooltip attributed to Author B with timestamp: `Lai, Anson deleted: <text>`), while the surrounding insertion text remains attributed to Author A.
|
|
18
|
+
2. **Word Desktop supports insertions inside pending insertions**: When Author B types text in the middle of Author A's `<w:ins>`, Word Desktop displays Author B's new text attributed to Author B, while preserving Author A's attribution on the preceding and succeeding words.
|
|
19
|
+
|
|
20
|
+
### The Underlying OOXML Mechanics
|
|
21
|
+
Empirical inspection of Microsoft Word Desktop 365 (via automated COM fixture generation in WP-01) reveals a vital asymmetry between how Word handles cross-author insertions versus deletions:
|
|
22
|
+
|
|
23
|
+
1. **Cross-Author Insertions (`insert-interior`) — Sibling Splitting**:
|
|
24
|
+
ECMA-376 Part 1 `CT_RunTrackChange` does NOT allow `<w:ins>` inside `<w:ins>`. Word Desktop splits the carrier `<w:ins>` into sibling fragments at the paragraph (`<w:p>`) level, splicing Author B's new `<w:ins>` between them:
|
|
25
|
+
```xml
|
|
26
|
+
<!-- Sibling 1: Author A's insertion (leading fragment) -->
|
|
27
|
+
<w:ins w:id="0" w:author="Barry Lai" w:date="2026-09-08T09:29:00Z">
|
|
28
|
+
<w:r><w:t xml:space="preserve">amended by this </w:t></w:r>
|
|
29
|
+
</w:ins>
|
|
30
|
+
<!-- Sibling 2: Author B's insertion spliced in between -->
|
|
31
|
+
<w:ins w:id="1" w:author="Anson Lai" w:date="2026-09-08T09:29:00Z">
|
|
32
|
+
<w:r><w:t xml:space="preserve">MASTER </w:t></w:r>
|
|
33
|
+
</w:ins>
|
|
34
|
+
<!-- Sibling 3: Author A's insertion (trailing fragment) -->
|
|
35
|
+
<w:ins w:id="2" w:author="Barry Lai" w:date="2026-09-08T09:29:00Z">
|
|
36
|
+
<w:r><w:t>Agreement.</w:t></w:r>
|
|
37
|
+
</w:ins>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
2. **Cross-Author Deletions (`delete-interior`) — Direct `<w:del>` Nesting inside `<w:ins>`**:
|
|
41
|
+
Under ECMA-376 Part 1 Section 17.13.5.21 (`CT_RunTrackChange`), `<w:del>` is an explicitly permitted child element of `<w:ins>`. Microsoft Word Desktop does **not** split `<w:ins>` for deletions; instead, it nests `<w:del>` directly inside `<w:ins>`:
|
|
42
|
+
```xml
|
|
43
|
+
<w:ins w:id="0" w:author="Barry Lai" w:date="2026-09-08T09:29:00Z">
|
|
44
|
+
<w:r><w:t xml:space="preserve">The Services will process the Input to </w:t></w:r>
|
|
45
|
+
<w:del w:id="1" w:author="Anson Lai" w:date="2026-09-08T09:29:00Z">
|
|
46
|
+
<w:r w:rsidDel="00F70C09"><w:delText xml:space="preserve">generate </w:delText></w:r>
|
|
47
|
+
</w:del>
|
|
48
|
+
<w:r><w:t>outputs for Customer.</w:t></w:r>
|
|
49
|
+
</w:ins>
|
|
50
|
+
```
|
|
51
|
+
This nested structure ensures that:
|
|
52
|
+
- If Author A's insertion is rejected, the entire container—including Author B's deletion of that unaccepted text—is discarded cleanly.
|
|
53
|
+
- If Author A's insertion is accepted, Author A's text is unwrapped into the baseline while Author B's `<w:del>` remains pending against the baseline.
|
|
54
|
+
|
|
55
|
+
### Why `docx-redline-js` Currently Blocks This
|
|
56
|
+
`docx-redline-js` currently guards against this with `EXISTING_REVISIONS` / `UNSAFE_REVISION_NESTING` because:
|
|
57
|
+
1. **Container Splitting Is Unimplemented for Insertions**: In `engine/surgical-diff-application.js`, `processInsert` inserts newly created revision elements into `span.runElement.parentNode`. When that parent is `<w:ins>`, inserting `<w:ins>` directly into it produces schema-invalid nested `<w:ins><w:ins>...</w:ins></w:ins>`.
|
|
58
|
+
2. **Overzealous Validation Rule**: In `core/redline-validation.js`, an existing check unconditionally flagged any `<w:del>` inside `<w:ins>` as `NESTED_REVISION`, contradicting ECMA-376 and Microsoft Word Desktop's native behavior.
|
|
59
|
+
3. **Same-Author vs. Cross-Author Asymmetry**: The engine already has `merge-same-author`, but that strategy works by *reverting* prior revisions back to the pre-revision baseline and re-diffing against the baseline into a single revision. Applying that to another author would erase Author A's attribution.
|
|
60
|
+
|
|
61
|
+
This plan defines the architecture, mutation mechanics, lifecycle rules, and test strategy to implement **Cross-Author Revision Slicing** natively.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 2. Hard Invariants
|
|
66
|
+
|
|
67
|
+
1. **Strict OOXML Schema Compliance (Word-Native Hierarchy)**:
|
|
68
|
+
- `<w:del>` INSIDE `<w:ins>` IS allowed (and expected by Word Desktop and ECMA-376 `CT_RunTrackChange`).
|
|
69
|
+
- `<w:ins>` must NEVER contain `<w:ins>`, `<w:moveFrom>`, or `<w:moveTo>`.
|
|
70
|
+
- `<w:del>` must NEVER contain `<w:ins>`, `<w:del>`, `<w:moveFrom>`, or `<w:moveTo>`.
|
|
71
|
+
- All revision wrappers must be immediate children of `<w:p>`, `<w:hyperlink>`, or (for `<w:del>`) `<w:ins>`.
|
|
72
|
+
|
|
73
|
+
2. **Durable ID Allocation & Metadata Preservation**:
|
|
74
|
+
- Split fragments of Author A's `<w:ins>` MUST preserve Author A's exact author string, date, and `dateUtc`.
|
|
75
|
+
- The first fragment may retain the original `w:id` if non-conflicting, but subsequent split fragments MUST be allocated globally unique `w:id` values via the active `RevisionIdAllocator`.
|
|
76
|
+
- Author B's new revision element (`<w:del>` or `<w:ins>`) MUST be allocated its own fresh `w:id` under Author B's name and current timestamp.
|
|
77
|
+
|
|
78
|
+
3. **Multi-Author Revision Lifecycle Consistency**:
|
|
79
|
+
- **Accept All**: Removing `<w:del>` and unwrapping all `<w:ins>` must produce the identical text string as Word Desktop.
|
|
80
|
+
- **Accept Author A Only**: Author A's split `<w:ins>` elements unwrap into baseline text. Author B's `<w:del>` and `<w:ins>` remain pending against that now-baseline text.
|
|
81
|
+
- **Reject Author A Only**: Author A's `<w:ins>` elements are removed. Any `<w:del>` by Author B situated inside Author A's insertion MUST be removed as well (you cannot delete text that was rejected from ever entering the document). Any `<w:ins>` by Author B remains as a pending insertion in the surrounding baseline.
|
|
82
|
+
- **Accept Author B Only**: Author B's `<w:del>` is removed; Author B's `<w:ins>` unwraps into text inside Author A's pending insertion. Author A's insertion fragments remain pending.
|
|
83
|
+
- **Reject Author B Only**: Author B's `<w:del>` is converted back into regular runs and coalesced back into Author A's `<w:ins>`. Author B's `<w:ins>` is removed.
|
|
84
|
+
|
|
85
|
+
4. **Zero Empty Containers**:
|
|
86
|
+
- If an edit touches the exact boundary of Author A's `<w:ins>`, no empty `<w:ins>` (container with 0 text characters) may be emitted.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 3. Architecture & Technical Design
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
Target Paragraph OOXML
|
|
94
|
+
|
|
|
95
|
+
v
|
|
96
|
+
Build Surgical Text Spans (Surgical Spans)
|
|
97
|
+
[Identifies text, offsets, and carrier revision element]
|
|
98
|
+
|
|
|
99
|
+
v
|
|
100
|
+
Target Range Overlaps Foreign Author <w:ins>
|
|
101
|
+
|
|
|
102
|
+
+------------------+------------------+
|
|
103
|
+
| |
|
|
104
|
+
v v
|
|
105
|
+
[Deletion Edit] [Insertion Edit]
|
|
106
|
+
1. Keep carrier <w:ins> intact 1. Split carrier <w:ins>
|
|
107
|
+
2. Extract deleted text at insertion offset
|
|
108
|
+
into <w:delText> 2. Create Author B's
|
|
109
|
+
3. Create Author B's <w:ins> with new ID
|
|
110
|
+
<w:del> with new ID 3. Splice at carrier level:
|
|
111
|
+
4. Nest <w:del> directly inside [Author A Left <w:ins>]
|
|
112
|
+
Author A's <w:ins> [Author B New <w:ins>]
|
|
113
|
+
[Author A Right <w:ins>]
|
|
114
|
+
|
|
|
115
|
+
v
|
|
116
|
+
Output Reconciliation Oracle
|
|
117
|
+
(Assert valid schema, unique IDs, only permitted nesting)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 3.1 Container Splitting Primitive: `splitTrackChangeCarrier`
|
|
121
|
+
A new utility in `engine/surgical-run-splitting.js`:
|
|
122
|
+
```js
|
|
123
|
+
export function splitTrackChangeCarrier(xmlDoc, carrierElement, splitOffset, allocator) {
|
|
124
|
+
// 1. Validate the <w:ins> carrier and preserve all carrier metadata.
|
|
125
|
+
// 2. Locate run and character offset corresponding to splitOffset.
|
|
126
|
+
// 3. Clone carrierElement into leftCarrier and rightCarrier.
|
|
127
|
+
// 4. Distribute child runs before splitOffset to leftCarrier, after to rightCarrier.
|
|
128
|
+
// 5. Allocate new unique revision ID for rightCarrier.
|
|
129
|
+
// 6. Return { leftCarrier, rightCarrier }.
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 3.2 Splicing in `processDelete`
|
|
134
|
+
When deleting a range `[startPos, endPos]`:
|
|
135
|
+
1. Check if the affected spans belong to a `<w:ins>` container.
|
|
136
|
+
2. If `span.runElement.parentNode` is `<w:ins>`:
|
|
137
|
+
- Identify whether the author of `<w:ins>` differs from the mutation `author`.
|
|
138
|
+
- Under cross-author slicing mode, keep the foreign `<w:ins>` intact.
|
|
139
|
+
- Create `<w:del w:author="AuthorB">` containing `<w:r><w:delText>deleted text</w:delText></w:r>`.
|
|
140
|
+
- Insert the `<w:del>` inside the carrier at the deleted run position, preserving unaffected runs on either side.
|
|
141
|
+
|
|
142
|
+
### 3.3 Splicing in `processInsert`
|
|
143
|
+
When inserting at position `pos`:
|
|
144
|
+
1. Check if `pos` falls inside an existing `<w:ins>`.
|
|
145
|
+
2. If `targetSpan.runElement.parentNode` is `<w:ins>`:
|
|
146
|
+
- Split carrier `<w:ins>` into `left_ins` and `right_ins` at `pos`.
|
|
147
|
+
- Create `new_ins` for Author B with Author B's text runs.
|
|
148
|
+
- Insert `[left_ins, new_ins, right_ins]` into the common `<w:p>`.
|
|
149
|
+
- Remove original carrier `<w:ins>`.
|
|
150
|
+
|
|
151
|
+
### 3.4 Straddle Deletions (Boundary Spanning)
|
|
152
|
+
When Author B's deletion starts in baseline text and ends inside Author A's `<w:ins>` (or vice-versa):
|
|
153
|
+
* The deletion must be partitioned:
|
|
154
|
+
1. The baseline portion emits a normal `<w:del>` child of `<w:p>`.
|
|
155
|
+
2. The insertion portion remains inside Author A's `<w:ins>` and emits a nested `<w:del>` there.
|
|
156
|
+
3. The baseline and insertion portions cannot be coalesced across the carrier boundary; preserve a top-level `<w:del>` plus a nested `<w:ins><w:del>...</w:del></w:ins>`, matching the Word Desktop fixture.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 4. Policy Configuration & Migration
|
|
161
|
+
|
|
162
|
+
We introduce a dedicated policy setting on `existingRevisions`:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
type ExistingRevisionsPolicy =
|
|
166
|
+
| 'merge-same-author' // Default in v0.5.x: merges same author, fails cross-author
|
|
167
|
+
| 'slice-cross-author' // NEW: merges same author, slices cross-author insertions
|
|
168
|
+
| 'accept-all-first' // Normalizes all prior revisions to baseline
|
|
169
|
+
| 'reject-input'; // Refuses any paragraph with revisions
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### CLI Flag
|
|
173
|
+
* `--existing-revisions slice-cross-author`
|
|
174
|
+
* Future migration in v1.0.0: make `slice-cross-author` the default behavior of `merge-same-author` or replace it once verified across stress tests.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 5. Work Packages
|
|
179
|
+
|
|
180
|
+
### WP-01: Baseline Word Desktop Fixture Generation [COMPLETED 2026-09-08]
|
|
181
|
+
* **Goal**: Produce reference DOCX files created directly in Microsoft Word Desktop 365 / 2021 covering:
|
|
182
|
+
1. Cross-author insertion in the middle of a pending insertion (`insert-interior`).
|
|
183
|
+
2. Cross-author deletion in the middle of a pending insertion (`delete-interior`).
|
|
184
|
+
3. Cross-author deletion at the start of a pending insertion (`delete-boundary-start`).
|
|
185
|
+
4. Cross-author deletion at the end of a pending insertion (`delete-boundary-end`).
|
|
186
|
+
5. Cross-author deletion straddling baseline text and an insertion (`delete-straddle-baseline-insertion`).
|
|
187
|
+
6. Multi-author stacked edits (`multi-author-stacked` - Author C editing Author B's edit inside Author A's edit).
|
|
188
|
+
* **Deliverables Produced**:
|
|
189
|
+
1. `scripts/generate-cross-author-slicing-fixtures.ps1`: Windows PowerShell script automating Microsoft Word Desktop 365 via COM (`Word.Application`) to author the 6 scenarios, export pending/accepted/rejected `.docx` files, and extract `word/document.xml`.
|
|
190
|
+
2. `tests/fixtures/cross-author-slicing/`: 36 golden fixture files (18 `.docx` and 18 UTF-8 BOM-clean `.xml` files) covering all triples:
|
|
191
|
+
- `insert-interior-{pending,accepted,rejected}.{docx,xml}`
|
|
192
|
+
- `delete-interior-{pending,accepted,rejected}.{docx,xml}`
|
|
193
|
+
- `delete-boundary-start-{pending,accepted,rejected}.{docx,xml}`
|
|
194
|
+
- `delete-boundary-end-{pending,accepted,rejected}.{docx,xml}`
|
|
195
|
+
- `delete-straddle-baseline-insertion-{pending,accepted,rejected}.{docx,xml}`
|
|
196
|
+
- `multi-author-stacked-{pending,accepted,rejected}.{docx,xml}`
|
|
197
|
+
3. `tests/cross_author_slicing_fixtures_tests.mjs`: Comprehensive automated test suite asserting:
|
|
198
|
+
- All 36 files exist and load cleanly without BOM corruption.
|
|
199
|
+
- Word Desktop native OOXML structural patterns match assertions (sibling `<w:ins>` splitting vs. nested `<w:del>` inside `<w:ins>`).
|
|
200
|
+
- Lifecycle oracle equivalence between `docx-redline-js` (`acceptTrackedChangesInOoxml`, `rejectTrackedChangesInOoxml`) and Word Desktop's native `AcceptAll` / `RejectAll`.
|
|
201
|
+
- Selective author accept/reject lifecycle assertions on nested revisions.
|
|
202
|
+
* **Files Touched**:
|
|
203
|
+
- `.gitignore` (MODIFIED to retain this plan in version control)
|
|
204
|
+
- `scripts/generate-cross-author-slicing-fixtures.ps1` (NEW)
|
|
205
|
+
- `tests/fixtures/cross-author-slicing/*` (36 files: 18 `.docx`, 18 `.xml`) (NEW)
|
|
206
|
+
- `tests/cross_author_slicing_fixtures_tests.mjs` (NEW)
|
|
207
|
+
- `docs/plans/2026-09-08-cross-author-revision-slicing.md` (MODIFIED)
|
|
208
|
+
* **Functions & Modules Touched / Created**:
|
|
209
|
+
- `scripts/generate-cross-author-slicing-fixtures.ps1`:
|
|
210
|
+
- `Extract-DocumentXml`: Extracts UTF-8 `word/document.xml` from DOCX ZIP archive.
|
|
211
|
+
- `Save-Triple`: Saves pending DOCX, accepts all and saves accepted DOCX, reopens and rejects all and saves rejected DOCX.
|
|
212
|
+
- Scenario blocks 1–6 utilizing COM Word automation (`$doc.TrackRevisions`, `$doc.Range.Text`, `$delRange.Delete()`, etc.).
|
|
213
|
+
- `tests/cross_author_slicing_fixtures_tests.mjs`:
|
|
214
|
+
- `loadFixtureXml`: Safe XML loader with leading BOM stripping.
|
|
215
|
+
- `findChildrenByLocalName`, `findDescendantsByLocalName`: Namespace-resilient OOXML tree inspection.
|
|
216
|
+
- Fixture inventory checks, Word Desktop structural checks, lifecycle parity checks (`acceptTrackedChangesInOoxml`, `rejectTrackedChangesInOoxml`), selective author accept/reject tests.
|
|
217
|
+
* **Key Findings & Next Phase Guidance (Crucial for WP-02 – WP-04)**:
|
|
218
|
+
- **Insertion Slicing**: Word Desktop splits `<w:ins>` into siblings at `<w:p>` level: `[ins(A), ins(B), ins(A)]`.
|
|
219
|
+
- **Deletion Slicing**: Word Desktop nests `<w:del>` directly inside `<w:ins>`: `<w:ins><w:r>...</w:r><w:del><w:r><w:delText>...</w:delText></w:r></w:del><w:r>...</w:r></w:ins>`. This is valid ECMA-376 `CT_RunTrackChange`.
|
|
220
|
+
- **Validator Update Required in WP-04**: `core/redline-validation.js` currently flags any `<w:del>` inside `<w:ins>` as `NESTED_REVISION`. That rule must be updated to allow `<w:del>` inside `<w:ins>`, while maintaining the prohibition against `<w:ins>` inside `<w:ins>` or `<w:ins>` inside `<w:del>`.
|
|
221
|
+
* **WP-01 Review (2026-09-08)**:
|
|
222
|
+
- Ran `node tests/cross_author_slicing_fixtures_tests.mjs`: all inventory, structural, lifecycle parity, and selective-author assertions passed.
|
|
223
|
+
- Corrected the generator to derive the repository root from `$PSScriptRoot` instead of a machine-specific absolute path.
|
|
224
|
+
- Added `Find-RequiredText` and changed all six scenario mutations to fail explicitly when Word cannot locate the intended text, preventing false golden fixtures.
|
|
225
|
+
- Corrected stale deletion diagrams and synthetic expectations in this plan to match the native nested `<w:del>` evidence.
|
|
226
|
+
|
|
227
|
+
### WP-02: Carrier Splitting & Revision Allocator Integration [COMPLETED 2026-09-08]
|
|
228
|
+
* **Goal**: Implement `splitTrackChangeCarrier` in `engine/surgical-run-splitting.js`.
|
|
229
|
+
* **Deliverable**: Unit tests proving clean splitting of `<w:ins>` runs at character offsets, proper allocation of unique revision IDs via `RevisionIdAllocator`, and metadata preservation (`w:author`, `w:date`, `w16du:dateUtc`).
|
|
230
|
+
* **Implementation Summary**:
|
|
231
|
+
- `splitTrackChangeCarrier` validates a `<w:ins>` carrier and accepted-view character offset, returns detached left/right fragments, and never mutates the source carrier.
|
|
232
|
+
- Interior splits retain the original revision ID on the left fragment and allocate a fresh document-scoped ID for the right fragment. Exact-start and exact-end splits return `null` for the empty side and do not consume an ID.
|
|
233
|
+
- Carrier attributes are cloned unchanged, including `w:author`, `w:date`, and `w16du:dateUtc`.
|
|
234
|
+
- A run split preserves `w:rPr` and text-like run children (`w:t`, `w:tab`, `w:br`, and related supported nodes). If cloning duplicates a nested `w:rPrChange`, the right-hand copy receives a fresh ID.
|
|
235
|
+
- Newly allocated carrier IDs are reported to an attached receipt collector when present.
|
|
236
|
+
* **Files Touched**:
|
|
237
|
+
- `engine/surgical-run-splitting.js` (MODIFIED)
|
|
238
|
+
- `tests/cross_author_carrier_splitting_tests.mjs` (NEW)
|
|
239
|
+
- `scripts/generate-cross-author-slicing-fixtures.ps1` (MODIFIED during WP-01 review)
|
|
240
|
+
- `docs/plans/2026-09-08-cross-author-revision-slicing.md` (MODIFIED)
|
|
241
|
+
* **Functions Touched / Created**:
|
|
242
|
+
- `engine/surgical-run-splitting.js`:
|
|
243
|
+
- `splitTrackChangeCarrier` (NEW): non-mutating `<w:ins>` split primitive.
|
|
244
|
+
- `resolveAllocator` (NEW): resolves the explicit or document-scoped `RevisionIdAllocator`.
|
|
245
|
+
- `setWordAttribute` (NEW): writes namespace-correct Word attributes.
|
|
246
|
+
- `getLocalName` (NEW): namespace-resilient carrier validation.
|
|
247
|
+
- Reuses `getRunContentPieces`, `getRunTextLength`, `sliceRunPieces`, and `createRunFromPieces`; these existing functions were not behaviorally changed.
|
|
248
|
+
- `scripts/generate-cross-author-slicing-fixtures.ps1`:
|
|
249
|
+
- `Find-RequiredText` (NEW): fail-fast lookup used by all six fixture scenarios.
|
|
250
|
+
- Fixture directory initialization (MODIFIED): now repository-relative.
|
|
251
|
+
- `tests/cross_author_carrier_splitting_tests.mjs`:
|
|
252
|
+
- `parseCarrier`, `wordAttribute`, `text`, and `revisionIds` (NEW test helpers).
|
|
253
|
+
- Tests cover interior/multi-run splitting, metadata and formatting preservation, unique carrier/`w:rPrChange` IDs, special run children, zero-empty-container boundaries, source immutability, and invalid inputs.
|
|
254
|
+
* **Verification**:
|
|
255
|
+
- `node tests/cross_author_carrier_splitting_tests.mjs` — PASS.
|
|
256
|
+
- `npx eslint engine/surgical-run-splitting.js tests/cross_author_carrier_splitting_tests.mjs` — PASS.
|
|
257
|
+
- PowerShell parser check for `scripts/generate-cross-author-slicing-fixtures.ps1` — PASS (the COM fixtures were reviewed, not regenerated).
|
|
258
|
+
- `npm test` — PASS, 90 test files passed and 0 failed.
|
|
259
|
+
- `npm run lint` — PASS.
|
|
260
|
+
- `npm run check:types` — PASS; all 123 runtime exports have declarations.
|
|
261
|
+
* **Handoff to WP-03**:
|
|
262
|
+
- Import `splitTrackChangeCarrier` into `engine/surgical-diff-application.js` and use it only when a generated insertion lands inside a foreign-author `<w:ins>`.
|
|
263
|
+
- The returned fragments are detached. WP-03 must splice non-null fragments plus Author B's new `<w:ins>` into the original carrier's parent, then remove the original carrier.
|
|
264
|
+
- Pass the active document-scoped allocator so split-fragment IDs participate in the same operation receipt and rollback lifecycle.
|
|
265
|
+
|
|
266
|
+
### WP-03: Surgical Engine Cross-Author Insert Splicing [COMPLETED 2026-09-08]
|
|
267
|
+
* **Goal**: Update `processInsert` in `engine/surgical-diff-application.js` to split carrier `<w:ins>` when author differs and hoist Author B's `<w:ins>` to paragraph sibling level (`[ins(A), ins(B), ins(A)]`).
|
|
268
|
+
* **Deliverable**: Tests verifying cross-author insertion produces 3 sibling `<w:ins>` tags in valid schema without `NESTED_REVISION` errors.
|
|
269
|
+
* **Implementation Summary**:
|
|
270
|
+
- `processInsert` now detects a foreign-author `<w:ins>` in ordinary, insertion-affinity, boundary, and replacement-anchor paths when `existingRevisions: 'slice-cross-author'` is active.
|
|
271
|
+
- `spliceInsertionAtCarrierOffset` uses the WP-02 primitive and replaces the original carrier with `[left foreign ins, current-author ins, right foreign ins]`, omitting null boundary fragments.
|
|
272
|
+
- Inserted text inherits effective run formatting but drops cloned historical `w:rPrChange` markup so revision IDs are not duplicated or misattributed.
|
|
273
|
+
- `applyRedlineToOxml` retains foreign revisions and routes `slice-cross-author` text edits through surgical mode. Same-author input continues to use the established reject-to-baseline/re-diff merge behavior.
|
|
274
|
+
- Pending move revisions still fail closed with `UNSAFE_REVISION_NESTING`.
|
|
275
|
+
* **Files Touched**:
|
|
276
|
+
- `engine/surgical-diff-application.js`
|
|
277
|
+
- `engine/surgical-mode.js`
|
|
278
|
+
- `engine/oxml-engine.js`
|
|
279
|
+
- `tests/cross_author_slicing_synthetic_tests.mjs` (NEW)
|
|
280
|
+
- `tests/cross_author_slicing_fixtures_tests.mjs`
|
|
281
|
+
- `docs/plans/2026-09-08-cross-author-revision-slicing.md`
|
|
282
|
+
* **Functions Touched / Created**:
|
|
283
|
+
- `processInsert` (MODIFIED): policy-aware foreign carrier detection and replacement-anchor support.
|
|
284
|
+
- `spliceInsertionAtCarrierOffset` (NEW): carrier split/splice orchestration.
|
|
285
|
+
- `withoutRunPropertyChanges` (NEW): preserves effective formatting without copying historical formatting revisions.
|
|
286
|
+
- `getCarrierSplitOffset`, `getCarrierGlobalStart`, `isForeignInsertion`, `isConnected` (NEW): carrier targeting helpers.
|
|
287
|
+
- `applySurgicalMode` (MODIFIED): forwards the policy and permits paired replacements inside an insertion carrier under slicing mode.
|
|
288
|
+
- `checkSafeAdjacencyForPairing` (MODIFIED): conditionally treats a foreign insertion carrier as a supported replacement context.
|
|
289
|
+
- `applyRedlineToOxml` (MODIFIED): retains cross-author revisions, merges same-author revisions, rejects moves, and selects surgical routing.
|
|
290
|
+
|
|
291
|
+
### WP-04: Surgical Engine Cross-Author Delete Splicing & Validation Update [COMPLETED 2026-09-08]
|
|
292
|
+
* **Goal**: Update `processDelete` in `engine/surgical-diff-application.js` to nest Author B's `<w:del>` directly inside Author A's carrier `<w:ins>` (matching Word Desktop). Update `core/redline-validation.js` to permit `<w:del>` inside `<w:ins>`.
|
|
293
|
+
* **Deliverable**: Tests verifying cross-author deletion produces `<w:ins>...<w:del>...</w:del>...</w:ins>` matching Word Desktop fixtures without validation errors.
|
|
294
|
+
* **Implementation Summary**:
|
|
295
|
+
- `processDelete` now groups adjacent affected runs sharing the same parent into one deletion wrapper, preserving each run's formatting and converting its text-like children to `w:delText`.
|
|
296
|
+
- When the shared parent is a foreign `<w:ins>`, the deletion wrapper remains directly nested in that carrier; the carrier and its metadata stay intact at start, end, interior, and complete-content deletions.
|
|
297
|
+
- Separate structural contexts receive separate deletion IDs but preserve shared replacement metadata/timestamps where supplied.
|
|
298
|
+
- `validateRedlineOoxml` now accepts only direct `<w:ins><w:del>...</w:del></w:ins>` nesting. It continues to reject `ins/ins`, `del/del`, `ins` inside `del`, and deeper non-direct revision nesting.
|
|
299
|
+
* **Files Touched**:
|
|
300
|
+
- `engine/surgical-diff-application.js`
|
|
301
|
+
- `core/redline-validation.js`
|
|
302
|
+
- `tests/redline_validation_tests.mjs`
|
|
303
|
+
- `tests/cross_author_slicing_synthetic_tests.mjs` (NEW)
|
|
304
|
+
- `tests/cross_author_slicing_fixtures_tests.mjs`
|
|
305
|
+
- `docs/plans/2026-09-08-cross-author-revision-slicing.md`
|
|
306
|
+
* **Functions Touched / Created**:
|
|
307
|
+
- `processDelete` (MODIFIED): plans run mutations, groups adjacent runs, emits one schema-correct deletion per parent context, and records replacement anchors.
|
|
308
|
+
- `nextElementSibling` (NEW): determines whether affected runs can share a deletion wrapper without crossing structural markers.
|
|
309
|
+
- `validateRedlineOoxml` (MODIFIED): implements the Word-native nested-deletion exception and retains all other nesting prohibitions.
|
|
310
|
+
|
|
311
|
+
### WP-05: Straddle & Boundary Deletion Normalization [COMPLETED 2026-09-08]
|
|
312
|
+
* **Goal**: Support deletions that cross between baseline text and pending insertions.
|
|
313
|
+
* **Deliverable**: Regression suite ensuring each deletion portion remains in its schema-correct context (top-level for baseline text, nested for pending insertion text) while sharing coherent author/event metadata.
|
|
314
|
+
* **Implementation Summary**:
|
|
315
|
+
- Cross-boundary deletion records are partitioned by their actual DOM parent: baseline portions emit top-level `<w:del>` and insertion portions emit nested `<w:del>` without attempting schema-invalid coalescing.
|
|
316
|
+
- Forward and reverse baseline/insertion straddles and deletion across two distinct foreign insertion carriers are covered.
|
|
317
|
+
- `revisionInsertionAnchors` preserves the post-deletion carrier/offset/formatting location for the insertion half of a replacement after original span nodes have been removed.
|
|
318
|
+
- Paired replacement metadata remains paired inside a foreign insertion: the deletion stays in the left carrier, the new insertion is hoisted, and the surviving right carrier receives its own unique ID.
|
|
319
|
+
* **Files Touched**:
|
|
320
|
+
- `engine/surgical-diff-application.js`
|
|
321
|
+
- `engine/surgical-mode.js`
|
|
322
|
+
- `tests/cross_author_slicing_synthetic_tests.mjs` (NEW)
|
|
323
|
+
- `tests/cross_author_slicing_fixtures_tests.mjs`
|
|
324
|
+
- `docs/plans/2026-09-08-cross-author-revision-slicing.md`
|
|
325
|
+
* **Functions Touched / Created**:
|
|
326
|
+
- `processDelete` (MODIFIED): records per-carrier replacement anchors and keeps parent contexts separate.
|
|
327
|
+
- `processInsert` (MODIFIED): consumes a connected replacement anchor before consulting stale original spans.
|
|
328
|
+
- `applySurgicalMode` and `checkSafeAdjacencyForPairing` (MODIFIED): allow paired events in the newly supported carrier context.
|
|
329
|
+
* **Verification to Date**:
|
|
330
|
+
- `node tests/cross_author_slicing_synthetic_tests.mjs` — PASS.
|
|
331
|
+
- `node tests/cross_author_slicing_fixtures_tests.mjs` — PASS, including engine reproduction of the five Word Desktop WP03-WP05 fixtures and AcceptAll/RejectAll parity.
|
|
332
|
+
- `npm test` — PASS, 91 test files passed and 0 failed.
|
|
333
|
+
- `npm run lint` — PASS.
|
|
334
|
+
- `npm run check:types` — PASS; all 123 runtime exports have declarations.
|
|
335
|
+
- `git diff --check` — PASS.
|
|
336
|
+
* **Handoff Boundary**:
|
|
337
|
+
- `slice-cross-author` is implemented in the low-level `applyRedlineToOxml` engine used by these work packages.
|
|
338
|
+
- CLI validation, operation preflight, document-operation/facade gating, schema enum changes, and public type exposure remain intentionally deferred to WP-07.
|
|
339
|
+
- WP-06 can build on the already passing native-fixture lifecycle tests; no lifecycle service code was changed in WP03-WP05.
|
|
340
|
+
|
|
341
|
+
### WP-06: Lifecycle Oracles & Selective Accept/Reject Support [COMPLETED 2026-09-08]
|
|
342
|
+
* **Goal**: Update `services/revision-comment-management.js` (`acceptTrackedChangesInOoxml` and `rejectTrackedChangesInOoxml`) to handle sliced revisions correctly:
|
|
343
|
+
- Selective accept/reject by author.
|
|
344
|
+
- Cascading rejection (rejecting Author A cleans up internal deletions by Author B).
|
|
345
|
+
* **Deliverable**: Round-trip lifecycle oracle tests comparing against Word Desktop results.
|
|
346
|
+
* **Implementation Summary**:
|
|
347
|
+
- Confirmed the existing outer-first accept/reject traversal already provides correct dependency behavior for nested deletions: accepting Author A unwraps the carrier and leaves Author B's deletion pending; rejecting Author A removes the carrier and its dependent deletion; rejecting Author B restores `w:delText` as normal text inside Author A's carrier.
|
|
348
|
+
- Added post-rejection normalization for sliced insertions. When rejecting Author B removes the middle insertion, adjacent Author A `<w:ins>` fragments with identical metadata other than `w:id` are coalesced back into one carrier.
|
|
349
|
+
- Coalescing is deliberately metadata-safe: different authors, dates, `dateUtc` values, or other carrier attributes are never merged.
|
|
350
|
+
* **Files Touched**:
|
|
351
|
+
- `services/revision-comment-management.js`
|
|
352
|
+
- `tests/cross_author_slicing_synthetic_tests.mjs`
|
|
353
|
+
- `docs/plans/2026-09-08-cross-author-revision-slicing.md`
|
|
354
|
+
* **Functions Touched / Created**:
|
|
355
|
+
- `rejectTrackedChangesInOoxml` (MODIFIED): runs compatible insertion coalescing after selective rejection.
|
|
356
|
+
- `nextElementSibling` (NEW): finds adjacent revision carriers while tolerating formatting whitespace nodes.
|
|
357
|
+
- `revisionMetadataWithoutId` (NEW): creates a stable metadata comparison key excluding only the split-specific revision ID.
|
|
358
|
+
- `coalesceAdjacentCompatibleInsertions` (NEW): merges compatible same-author insertion fragments without changing lifecycle counts.
|
|
359
|
+
* **Lifecycle Coverage**:
|
|
360
|
+
- Selective accept/reject of Author A and Author B on generated sibling insertion slices.
|
|
361
|
+
- Fragment coalescing after rejecting Author B.
|
|
362
|
+
- Selective rejection of nested Author B deletion and restoration inside Author A's insertion.
|
|
363
|
+
- Selective acceptance of Author A with Author B's deletion remaining pending.
|
|
364
|
+
- Cascading removal of Author B's nested deletion when Author A is rejected.
|
|
365
|
+
- Existing Word Desktop AcceptAll/RejectAll and selective-author fixture oracles remain green.
|
|
366
|
+
|
|
367
|
+
### WP-07: CLI, Preflight, and Facade Exposure [COMPLETED 2026-09-08]
|
|
368
|
+
* **Goal**: Expose `slice-cross-author` in:
|
|
369
|
+
- `docx-redline` CLI (`--existing-revisions slice-cross-author`)
|
|
370
|
+
- `services/operation-preflight.js` (recognizes cross-author slicing as valid rather than `EXISTING_REVISIONS`)
|
|
371
|
+
- `openDocx` / `applyOperationsToDocumentXml` options
|
|
372
|
+
* **Deliverable**: Updated schema definitions and CLI test cases.
|
|
373
|
+
* **Implementation Summary**:
|
|
374
|
+
- Added one canonical runtime policy list and validation helper. Operation-level and batch-level invalid policies now return `INVALID_OPERATION` instead of falling through to unrelated revision errors.
|
|
375
|
+
- Preflight reports foreign insertion/deletion edits as `ready` under `slice-cross-author`, preserves same-author comment safeguards, and continues to fail closed on pending moves.
|
|
376
|
+
- The document mutation gate now permits slicing operations to reach `applyRedlineToOxml`; operation-level policy overrides continue to flow through `document-operation-applier`.
|
|
377
|
+
- `applyOperationsToDocumentXml`, `openDocx(...).applyOperations`, and CLI `apply`/`preflight` accept the batch policy. CLI version capabilities now advertise `cross-author-revision-slicing`.
|
|
378
|
+
- Public TypeScript types, JSON Schema, README, and `AGENTS.md` document the new policy.
|
|
379
|
+
* **Files Touched**:
|
|
380
|
+
- `services/document-operation-contract.js`
|
|
381
|
+
- `services/operation-preflight.js`
|
|
382
|
+
- `services/document-operation-mutations.js`
|
|
383
|
+
- `services/batch-operation-orchestrator.js`
|
|
384
|
+
- `node/cli.js`
|
|
385
|
+
- `index.d.ts`
|
|
386
|
+
- `docs/schemas/document-operations.schema.json`
|
|
387
|
+
- `README.md`
|
|
388
|
+
- `AGENTS.md`
|
|
389
|
+
- `tests/existing_revisions_modes_matrix_tests.mjs`
|
|
390
|
+
- `tests/agent_operation_contract_tests.mjs`
|
|
391
|
+
- `tests/agent_cli_tests.mjs`
|
|
392
|
+
- `docs/plans/2026-09-08-cross-author-revision-slicing.md`
|
|
393
|
+
* **Functions / Types Touched or Created**:
|
|
394
|
+
- `EXISTING_REVISIONS_POLICIES` and `isExistingRevisionsPolicy` (NEW): shared policy contract and validator.
|
|
395
|
+
- `validateDocumentOperation` (MODIFIED): validates operation-level `existingRevisions` values.
|
|
396
|
+
- `preflightOperations` (MODIFIED): validates batch policy and models same-author merge, foreign-author slicing, and unsupported moves.
|
|
397
|
+
- `applyOperationsToDocumentXml` (MODIFIED): validates batch policy before creating or mutating a session.
|
|
398
|
+
- Document redline mutation policy gate in `applyToParagraphByExactText` (MODIFIED): permits slicing while preserving comment and move safeguards.
|
|
399
|
+
- `executeCli` (MODIFIED): validates `--existing-revisions`; apply/preflight pass-through remains shared with facade options.
|
|
400
|
+
- `ExistingRevisionsPolicy` (MODIFIED): includes `'slice-cross-author'`.
|
|
401
|
+
* **End-to-End Coverage**:
|
|
402
|
+
- CLI `apply --existing-revisions slice-cross-author` writes a package that inspects with both original and current authors.
|
|
403
|
+
- CLI `preflight` accepts the same operation and reports the selected policy.
|
|
404
|
+
- CLI rejects an unknown policy with `INVALID_OPERATION`.
|
|
405
|
+
- Runtime operation validation accepts `slice-cross-author` and rejects misspellings.
|
|
406
|
+
- JSON Schema is parsed and asserted to contain the new enum value.
|
|
407
|
+
- Facade/package validation and output reconciliation run through the existing CLI application path.
|
|
408
|
+
* **Final Verification**:
|
|
409
|
+
- `npm test` — PASS, 91 test files passed and 0 failed.
|
|
410
|
+
- `npm run lint` — PASS.
|
|
411
|
+
- `npm run check:types` — PASS; all 123 runtime exports have declarations.
|
|
412
|
+
- `git diff --check` — PASS.
|
|
413
|
+
|
|
414
|
+
### Final Automated Test Completion [COMPLETED 2026-09-08]
|
|
415
|
+
* **Coverage Added**:
|
|
416
|
+
- Completed the executable synthetic matrix for SYN-01 through SYN-12d. The final additions generate SYN-02 in the engine, add a second cross-author deletion to produce SYN-11, and exercise Accept All, Accept Author A, Reject Author A, Reject Author B, and selective rejection of the third author.
|
|
417
|
+
- Added strict package-facade differential tests for all five core Word Desktop scenarios. Each test reconstructs the pre-Anson package, applies `slice-cross-author` through `openDocx(...).applyOperations`, requires atomic package validation, and compares engine Accept-All/Reject-All text with the checked-in Word Desktop accepted/rejected DOCX files.
|
|
418
|
+
- Added a two-round package test that generates the three-author stacked-deletion fixture through Anson and Chris operations, asserts all three reviewers survive inspection, and compares both lifecycle endpoints with Word Desktop.
|
|
419
|
+
* **Files Touched**:
|
|
420
|
+
- `tests/cross_author_slicing_synthetic_tests.mjs` (MODIFIED)
|
|
421
|
+
- `tests/cross_author_slicing_real_tests.mjs` (NEW)
|
|
422
|
+
- `docs/plans/2026-09-08-cross-author-revision-slicing.md` (MODIFIED)
|
|
423
|
+
* **Functions / Test Helpers Touched or Created**:
|
|
424
|
+
- `tests/cross_author_slicing_synthetic_tests.mjs`: added the SYN-02/SYN-11/SYN-12 generated lifecycle block; no production functions changed during final test completion.
|
|
425
|
+
- `tests/cross_author_slicing_real_tests.mjs`:
|
|
426
|
+
- `fixtureBuffer` (NEW): loads a checked-in Word Desktop DOCX oracle.
|
|
427
|
+
- `visibleText` (NEW): obtains stable main-document text through package inspection.
|
|
428
|
+
- `resolvedText` (NEW): resolves all revisions through the package facade, requires package validation, and asserts no revision authors remain.
|
|
429
|
+
- PKG-01 through PKG-05 (NEW): strict package reproduction and differential lifecycle tests.
|
|
430
|
+
- PKG-06 (NEW): generated three-reviewer, two-round package negotiation test.
|
|
431
|
+
* **Final Verification**:
|
|
432
|
+
- `node tests/cross_author_slicing_synthetic_tests.mjs` — PASS.
|
|
433
|
+
- `node tests/cross_author_slicing_real_tests.mjs` — PASS.
|
|
434
|
+
- `npm test` — PASS, 92 test files passed and 0 failed.
|
|
435
|
+
- `npm run lint` — PASS.
|
|
436
|
+
- `npm run check:types` — PASS; all 123 runtime exports have declarations.
|
|
437
|
+
- `git diff --check` — PASS (line-ending conversion notices only; no whitespace errors).
|
|
438
|
+
* **Scope Note**:
|
|
439
|
+
- The repository does not contain the private `agreement.docx` referenced by REAL-01/REAL-02 or the exact `c5bb43ede5...` corpus package referenced by REAL-03. Those named cases remain external acceptance scenarios rather than silently skipped automated tests.
|
|
440
|
+
- REAL-04/REAL-05 require Microsoft Word Desktop COM and visual review. The checked-in fixtures were produced by Word COM, while the normal automated suite deliberately remains deterministic and non-interactive.
|
|
441
|
+
|
|
442
|
+
### Bug Follow-Up: Hyperlink Boundary Round-Trip Mismatch [FIXED 2026-09-08]
|
|
443
|
+
* **Report Reproduced**: A slicing edit with repeated text, hyperlink runs, and NBSP-to-space substitutions could return `status: "ok"` even though its accepted-view text differed from `modified`.
|
|
444
|
+
* **Root Cause**: `applySurgicalMode` discarded whitespace-only insertion diff segments by checking `textWithoutNewlines.trim().length`. The corresponding NBSP deletion still committed, changing `located at\u00a0example.com` to `located atexample.com`. Structural replacement paths could also proceed after `PAIRING_SKIPPED_STRUCTURAL_BOUNDARY` without a final exact-text oracle.
|
|
445
|
+
* **Fix**:
|
|
446
|
+
- Whitespace-only insertions are now applied rather than silently skipped.
|
|
447
|
+
- Every `slice-cross-author` surgical result reconstructs canonical accepted-view text and compares it exactly with the requested clean modified text.
|
|
448
|
+
- A mismatch returns `PATCH_ROUNDTRIP_MISMATCH`, `hasChanges: false`, diagnostic excerpts and offset, and the exact original OOXML. The document runner/facade therefore treats the operation as unapplied and preserves transactional rollback.
|
|
449
|
+
* **Files Touched**:
|
|
450
|
+
- `engine/surgical-mode.js`
|
|
451
|
+
- `engine/oxml-engine.js`
|
|
452
|
+
- `tests/cross_author_slicing_hyperlink_roundtrip_tests.mjs` (NEW)
|
|
453
|
+
- `CHANGELOG.md`
|
|
454
|
+
- `README.md`
|
|
455
|
+
- `AGENTS.md`
|
|
456
|
+
- `docs/TESTING.md`
|
|
457
|
+
- `docs/plans/2026-09-08-cross-author-revision-slicing.md`
|
|
458
|
+
* **Functions Touched / Created**:
|
|
459
|
+
- `applySurgicalMode` (MODIFIED): retains whitespace-only insertions and enforces the slicing accepted-view postcondition.
|
|
460
|
+
- `firstMismatchOffset` (NEW): locates the first exact-text divergence.
|
|
461
|
+
- `excerptAt` (NEW): provides bounded expected/actual diagnostics without returning entire contract paragraphs.
|
|
462
|
+
- `applyRedlineToOxml` surgical result handling (MODIFIED): restores the exact input OOXML on `PATCH_ROUNDTRIP_MISMATCH`.
|
|
463
|
+
- Test helpers `run`, `insertion`, `hyperlink`, and `acceptedParagraphText` (NEW).
|
|
464
|
+
* **Regression Coverage**:
|
|
465
|
+
- Low-level reproduction with two hyperlink relationship containers, repeated `Widget Policy`, and three NBSP-to-space edits.
|
|
466
|
+
- Full `applyOperationsToDocumentXml` atomic runner reproduction matching the CLI execution path.
|
|
467
|
+
- Exact Accept-All equality with the submitted modified string.
|
|
468
|
+
- Explicit fail-closed test proving mismatch status, error code, mismatch offset, and byte-exact original OOXML rollback.
|
|
469
|
+
* **Verification**:
|
|
470
|
+
- `node tests/cross_author_slicing_hyperlink_roundtrip_tests.mjs` — PASS.
|
|
471
|
+
- `npm test` — PASS, 93 test files passed and 0 failed.
|
|
472
|
+
- `npm run lint` — PASS.
|
|
473
|
+
- `npm run check:types` — PASS; all 123 runtime exports have declarations.
|
|
474
|
+
- `git diff --check` — PASS (line-ending conversion notices only; no whitespace errors).
|
|
475
|
+
|
|
476
|
+
### Insertion Stress Follow-Up [COMPLETED 2026-09-08]
|
|
477
|
+
* **Motivation**: Real usage reported failures across a wider variety of insertions after the first hyperlink/NBSP bug. A generated matrix was added to exercise location, payload, structure, lifecycle, and repeated-review dimensions rather than relying on a few fixed examples.
|
|
478
|
+
* **Defects Exposed and Fixed**:
|
|
479
|
+
1. Leading/trailing spaces, tabs, and NBSP-only additions were classified as no-ops because slicing inherited trim-based text-change detection. `applyRedlineToOxml` now uses exact comparison for `slice-cross-author`.
|
|
480
|
+
2. Word-token semantic diff cleanup could relocate a pure insertion between repeated phrases, especially inside a hyperlink. `computeInsertionOnlyDiffs` now selects a character-local, no-deletion diff whenever the original is an exact subsequence of the modified text; replacements retain the established word diff and exact round-trip guard.
|
|
481
|
+
3. In a paragraph containing both current-author and foreign insertion carriers, inserting into the current-author carrier produced illegal nested `w:ins`. `processInsert` now adds a normal run to that existing carrier while continuing to split foreign carriers into siblings.
|
|
482
|
+
* **Coverage Added**:
|
|
483
|
+
- 76 deterministic scenarios spanning carrier start/end/interior positions; single-, multi-, and formatted runs; repeated tokens; double spaces, tabs, NBSP, XML-sensitive characters, emoji, ZWJ emoji, combining characters, citations, and punctuation.
|
|
484
|
+
- Hyperlink interiors and both hyperlink boundaries; bookmarks; comment anchors; nested prior deletions; adjacent foreign authors; mixed current/foreign authors; three-container edits; and consecutive second-/third-reviewer rounds.
|
|
485
|
+
- Exact accepted-view, Accept-All, Reject-Current, validation, unique metadata, hyperlink preservation, and zero-empty-insertion assertions.
|
|
486
|
+
- Twenty scenarios also execute through `applyOperationsToDocumentXml` with atomic and strict-target settings, matching the CLI runner path.
|
|
487
|
+
- Nested hyperlink/field structures are required either to produce exact valid output or fail closed without throwing.
|
|
488
|
+
* **Files Touched**:
|
|
489
|
+
- `pipeline/diff-engine.js`
|
|
490
|
+
- `engine/oxml-engine.js`
|
|
491
|
+
- `engine/surgical-mode.js`
|
|
492
|
+
- `engine/surgical-diff-application.js`
|
|
493
|
+
- `tests/cross_author_slicing_insertion_stress_tests.mjs` (NEW)
|
|
494
|
+
- `CHANGELOG.md`
|
|
495
|
+
- `README.md`
|
|
496
|
+
- `docs/TESTING.md`
|
|
497
|
+
- `docs/plans/2026-09-08-cross-author-revision-slicing.md`
|
|
498
|
+
* **Functions Touched / Created**:
|
|
499
|
+
- `computeInsertionOnlyDiffs` (NEW): detects insertion-only transforms and returns a character-local diff only when it contains no deletion.
|
|
500
|
+
- `applyRedlineToOxml` (MODIFIED): uses exact slicing change detection, including boundary whitespace.
|
|
501
|
+
- `applySurgicalMode` (MODIFIED): selects insertion-only versus word diff without changing replacement semantics.
|
|
502
|
+
- `processInsert` (MODIFIED): inserts directly into an existing same-author carrier in mixed-author paragraphs.
|
|
503
|
+
- `isSameAuthorInsertion` (NEW): namespace-safe author comparison for carrier coalescing.
|
|
504
|
+
- Stress helpers `escapeXml`, `run`, `insertion`, `paragraph`, `parsed`, `acceptedText`, `authorOf`, `assertValid`, and `assertInsertionRoundTrip` (NEW).
|
|
505
|
+
* **Verification**:
|
|
506
|
+
- `node tests/cross_author_slicing_insertion_stress_tests.mjs` — PASS, 76 scenarios.
|
|
507
|
+
- `npm test` — PASS, 94 test files passed and 0 failed.
|
|
508
|
+
- `npm run lint` — PASS.
|
|
509
|
+
- `npm run check:types` — PASS; all 123 runtime exports have declarations.
|
|
510
|
+
- `git diff --check` — PASS (line-ending conversion notices only; no whitespace errors).
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
## 6. Comprehensive Verification Plan (Synthetic & Real Test Series)
|
|
515
|
+
|
|
516
|
+
To prove correctness across all layers of the stack, this plan defines two comprehensive test suites:
|
|
517
|
+
1. **Synthetic Unit & Boundary Suites** (`tests/cross_author_slicing_synthetic_tests.mjs`): Isolated OOXML fixtures testing edge cases, boundary alignments, and lifecycle mechanics.
|
|
518
|
+
2. **Checked-In Word Package Differential Suite** (`tests/cross_author_slicing_real_tests.mjs`): End-to-end strict-facade replay against actual DOCX packages created by Microsoft Word Desktop, including package validation and lifecycle comparison with Word-generated oracles.
|
|
519
|
+
|
|
520
|
+
The synthetic matrix below is fully automated. The checked-in package suite is also fully automated as PKG-01 through PKG-06. REAL-01 through REAL-05 remain an environment/input-dependent acceptance matrix for the named private/corpus documents and live Word COM/visual checks.
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
### 6.1 Synthetic Test Series (`tests/cross_author_slicing_synthetic_tests.mjs`)
|
|
525
|
+
|
|
526
|
+
| ID | Test Case Name | Input Structure | Operation (Author B) | Expected OOXML Structure | Invariant Assertions |
|
|
527
|
+
|:---|:---|:---|:---|:---|:---|
|
|
528
|
+
| **SYN-01** | Pure Interior Insertion | `<w:ins author="Barry">amended by this Agreement</w:ins>` | Insert `"MASTER "` before `"Agreement"` | `[ins(Barry): "amended by this "][ins(Anson): "MASTER "][ins(Barry): "Agreement"]` | 3 sibling `<w:ins>` nodes; no `NESTED_REVISION`; unique IDs allocated for `ins(Anson)` and trailing `ins(Barry)`. |
|
|
529
|
+
| **SYN-02** | Pure Interior Deletion | `<w:ins author="Barry">The Services will process the Input to generate outputs</w:ins>` | Delete `"generate "` | `<w:ins author="Barry">...<w:del author="Anson">generate </w:del>...</w:ins>` | One Barry carrier remains; nested `w:del` uses `<w:delText>` and is authored by Anson. |
|
|
530
|
+
| **SYN-03** | Boundary Deletion at Insertion Start | `<w:ins author="Barry">Notwithstanding the foregoing, the NDA remains</w:ins>` | Delete `"Notwithstanding the foregoing, "` | `<w:ins author="Barry"><w:del author="Anson">Notwithstanding...</w:del>the NDA remains</w:ins>` | Nested deletion is the carrier's first content node; Barry metadata remains intact. |
|
|
531
|
+
| **SYN-04** | Boundary Deletion at Insertion End | `<w:ins author="Barry">subject to Section 2.8 and applicable law</w:ins>` | Delete `" and applicable law"` | `<w:ins author="Barry">subject...<w:del author="Anson"> and applicable law</w:del></w:ins>` | Nested deletion is the carrier's final content node. |
|
|
532
|
+
| **SYN-05** | Complete Deletion of Pending Insertion Text | `<w:ins author="Barry">Obsolete clause insertion.</w:ins>` | Delete entire string `"Obsolete clause insertion."` | `<w:ins author="Barry"><w:del author="Anson">Obsolete clause insertion.</w:del></w:ins>` | Barry's carrier remains so rejecting Barry still cascades away Anson's dependent deletion. |
|
|
533
|
+
| **SYN-06** | Straddle Deletion (Baseline to Insertion) | `<w:r><w:t>Baseline start </w:t></w:r><w:ins author="Barry">inserted finish</w:ins>` | Delete `"start inserted"` | `[r: "Baseline "][del(Anson): "start "][ins(Barry): [del(Anson): "inserted"] " finish"]` | Top-level and nested deletion portions remain structurally separate, matching Word Desktop. |
|
|
534
|
+
| **SYN-07** | Straddle Deletion (Insertion to Baseline) | `<w:ins author="Barry">Inserted start</w:ins><w:r><w:t> baseline finish</w:t></w:r>` | Delete `"start baseline"` | `[ins(Barry): "Inserted " [del(Anson): "start"]][del(Anson): " baseline"][r: " finish"]` | Nested and top-level deletion portions preserve their respective carrier contexts. |
|
|
535
|
+
| **SYN-08** | Multi-Insertion Straddle (Author A to Author C) | `<w:ins author="Barry">Barry text </w:ins><w:ins author="Carl">Carl text</w:ins>` | Author B deletes `"text Carl"` | `[ins(Barry): "Barry " [del(Anson): "text "]][ins(Carl): [del(Anson): "Carl"] " text"]` | Each foreign carrier owns its nested deletion portion; neither carrier is sliced for deletion. |
|
|
536
|
+
| **SYN-09** | Multi-Run Formatting Preservation | `<w:ins author="Barry"><w:r><w:rPr><w:b/></w:rPr><w:t>Bold text </w:t></w:r><w:r><w:t>plain text</w:t></w:r></w:ins>` | Delete `"text plain"` | Barry's `<w:ins>` remains intact around a nested `<w:del>` containing a bold run for `"text "` and a plain run for `"plain"`. | Exact run-level formatting is preserved inside `<w:delText>` and unaffected insertion runs. |
|
|
537
|
+
| **SYN-10** | Paired Replacement Event inside Insertion | `<w:ins author="Barry">process the Input to generate outputs</w:ins>` | Replace `"generate"` with `"synthesize"` (`pairReplacements: true`) | `[ins(Barry): prefix + nested del(Anson)][ins(Anson): "synthesize"][ins(Barry): suffix]` | Deletion and insertion share timestamp; only the insertion requires carrier splitting/hoisting. |
|
|
538
|
+
| **SYN-11** | 3-Author Stacked Deletions | Output of **SYN-02** | Author C ("Davis, Chris") deletes `"process"` in Barry's carrier | `<w:ins author="Barry">...<w:del author="Davis">process</w:del>...<w:del author="Anson">generate</w:del>...</w:ins>` | Multiple distinct reviewer deletions coexist safely inside the same foreign insertion. |
|
|
539
|
+
| **SYN-12a** | Lifecycle Oracle: Accept All | Output of **SYN-02** | `acceptTrackedChanges({ allAuthors: true })` | Clean baseline string: `"The Services will process the Input to outputs"` | All `<w:del>` removed, all `<w:ins>` unwrapped; zero revision tags remaining. |
|
|
540
|
+
| **SYN-12b** | Lifecycle Oracle: Accept Author A Only | Output of **SYN-02** | `acceptTrackedChanges({ author: 'Barry' })` | Barry's text becomes baseline; Anson's `<w:del>` remains pending against the baseline. | Anson's `<w:del>` remains intact and reviewable. |
|
|
541
|
+
| **SYN-12c** | Lifecycle Oracle: Reject Author A Only | Output of **SYN-02** | `rejectTrackedChanges({ author: 'Barry' })` | Barry's insertion is deleted from the document. Anson's internal `<w:del>` is cascaded and pruned. | Prevents orphaned deletion of text that was rejected from ever existing. |
|
|
542
|
+
| **SYN-12d** | Lifecycle Oracle: Reject Author B Only | Output of **SYN-02** | `rejectTrackedChanges({ author: 'Anson' })` | Anson's nested `<w:del>` is unwrapped back into regular runs within Barry's `<w:ins>`. | Full restoration of Barry's original insertion. |
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
### 6.2 External Real-World Acceptance Series
|
|
547
|
+
|
|
548
|
+
| ID | Scenario & Source Document | Workflow & Operations | Expected Real-World Behavior | Verification Oracle |
|
|
549
|
+
|:---|:---|:---|:---|:---|
|
|
550
|
+
| **REAL-01** | **Salary.com Agreement: The Motivating AI Terms Deletion**<br>Source: `agreement.docx` (Section 2.1 Customer Data, `P40`) | 1. Document contains Barry Lai's pending insertion (`P40`, ID `45`).<br>2. Anson Lai runs operation to delete `"generate"` from `"to generate outputs"`.<br>3. `--existing-revisions slice-cross-author`. | Batch commits with `status: "ok"`, `written: true`.<br>Barry's insertion remains intact and contains Anson's visible, attributed nested deletion.<br>No `COMMENTED_CONTENT_DELETE` (since comment 144 is on Section 2.8, not 2.1). | Output file validated via `validateDocxPackage`. Revisions inspectable via `docx-redline inspect`. |
|
|
551
|
+
| **REAL-02** | **Salary.com Agreement: §14.1 NDA Carve-Out**<br>Source: `agreement.docx` (Section 14.1 Entire Agreement, `P131`) | 1. Barry has pending insertion of the amendment sentence.<br>2. Anson inserts August 25, 2026 NDA carve-out in the middle.<br>3. `--existing-revisions slice-cross-author`. | Barry's insertion remains visibly attributed to Barry (not baked into baseline).<br>Anson's carve-out sits as an adjacent/spliced insertion attributed to Anson. | `extract` and `inspect` show both `Lai, Barry` and `Lai, Anson` in `revisionAuthors`. |
|
|
552
|
+
| **REAL-03** | **SuperDoc Corpus: Interagency Multi-Counsel Negotiation**<br>Source: Corpus ID `c5bb43ede5...` (Joint Communications Protocol) | Round 1: BCHD Lead Agency Counsel applies insertions to Sections 3.2 and 4.1.<br>Round 2: MOHS Counterparty Counsel edits directly inside BCHD's insertions.<br>Round 3: Third Reviewer applies further modifications. | 3 distinct institutional authors with overlapping and sliced edits commit across rounds without merge corruption or loss of attribution. | Document hash checks; zero schema errors across all 3 rounds. |
|
|
553
|
+
| **REAL-04** | **Desktop Word 365 COM Automation Oracle**<br>Execution on Windows runner via native Word | Open the output DOCX files from **REAL-01**, **REAL-02**, and **REAL-03** via Windows COM automation (`word-client.mjs`). | 1. Word opens each file with **0 repair prompts** / corruption dialogs.<br>2. `Document.Revisions.Count` matches exact receipt counts.<br>3. `Document.Revisions.AcceptAll()` in Word matches engine `acceptAll()` bit-for-bit.<br>4. `Document.Revisions.RejectAll()` in Word matches engine `rejectAll()` bit-for-bit. | COM automation script asserts identical string contents after Word native Accept/Reject. |
|
|
554
|
+
| **REAL-05** | **Word Visual Rendering & PDF Export Proof**<br>Visual Evidence Pipeline | Export pages of **REAL-01** and **REAL-02** to PDF via Word COM `ExportAsFixedFormat`. Convert PDF pages to PNG. | 1. Deletions show strikethrough in Anson's reviewer color.<br>2. Insertions show underline in Barry's reviewer color.<br>3. Word Reviewing Pane displays balloons for both authors correctly without overlap or misaligned leader lines. | Visual evidence artifact generated in `tests/visual-evidence/` for human review sign-off. |
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
### 6.3 Test Execution Matrix
|
|
559
|
+
|
|
560
|
+
```bash
|
|
561
|
+
# 1. Run synthetic unit & boundary suite
|
|
562
|
+
node tests/cross_author_slicing_synthetic_tests.mjs
|
|
563
|
+
|
|
564
|
+
# 2. Run checked-in Word DOCX package differential suite (PKG-01..06)
|
|
565
|
+
node tests/cross_author_slicing_real_tests.mjs
|
|
566
|
+
|
|
567
|
+
# 3. Optional external acceptance: Word Desktop COM differential oracle (Windows desktop)
|
|
568
|
+
npm run test:word
|
|
569
|
+
|
|
570
|
+
# 4. Verify existing mode matrix remains 100% backward compatible
|
|
571
|
+
node tests/existing_revisions_modes_matrix_tests.mjs
|
|
572
|
+
|
|
573
|
+
# 5. Full regression check
|
|
574
|
+
npm test
|
|
575
|
+
```
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"captureKey": { "type": "string", "minLength": 1, "maxLength": 256 },
|
|
56
56
|
"target": { "$ref": "#/$defs/target" }, "targetRef": { "type": ["integer", "string", "null"] },
|
|
57
57
|
"author": { "type": "string", "minLength": 1 }, "generateRedlines": { "type": "boolean" },
|
|
58
|
-
"existingRevisions": { "enum": ["merge-same-author", "reject-input", "accept-all-first", "accept-all-first-keep-normalized"] },
|
|
58
|
+
"existingRevisions": { "enum": ["merge-same-author", "slice-cross-author", "reject-input", "accept-all-first", "accept-all-first-keep-normalized"] },
|
|
59
59
|
"pairReplacements": { "type": "boolean" },
|
|
60
60
|
"insertionAffinity": {
|
|
61
61
|
"type": "object",
|