Repository navigation
fix(replace): tracked span inside another author's insertion splits it Word-style - #20
Merged
Merged
Conversation
…t Word-style (#13) `replace --track` and `edit --at pN:S-E --track` share `replaceSpanInParagraph`. When the match lay wholly inside another author's pending `<w:ins>`, the replacement landed as a bare run inside that ins (credited to the other author), and when the match sat in a later run of the ins (e.g. after a nested `<w:del>`) the offset walk started at that run and cut the wrong words. Add ONE early branch in `replaceSpanInParagraph` that dispatches to the new `core/find/replace-in-revision.tsx` when: the replace is tracked, the span is non-empty, every overlapping run shares one immediate parent, and that parent is a `<w:ins>`/`<w:moveTo>` directly under the paragraph. Another author's wrapper is split the way Word does it (wrapper keeps its id + pre text + the editor's nested `<w:del>` of the cut; the editor's own top-level `<w:ins>` carries the replacement; the tail rides a fresh-id copy of the wrapper). The editor's own wrapper (exact `w:author` match) is rebuilt in place — same id, no `<w:del>`, no split. Nested non-run children move whole at their view-visible length and keep their document position (one `<w:del>` per contiguous cut group). Every other input takes the existing walkers unchanged: a golden comparison of 648 (fixture, command) pairs against main differs only on the 12 that hit the gate. No existing test expectation changed. Triage-Routine: true Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GoxD36rnNfg6uArkVxtJL
… per-author revN (#13) Four follow-ups on the in-revision span replace: - The editor's own `<w:ins>` rebuild no longer discards the cut runs outright: every non-text child (footnote/endnote/comment reference, drawing, text-box anchor, object) is kept in place, so what was anchored on the replaced words survives (`cutRemnants`). - The in-place merge is restricted to `<w:ins>`. A same-author `<w:moveTo>` takes the split path — moved text is relocated original text, not the author's new words. - A tail that holds only markers (comment range end + its reference run, bookmark end) stays in the wrapper after the cut instead of riding a fresh-id copy that would list as an empty revision (`isPureMarker`). - `revN` pairing (`cli/track-changes/groups.ts`) requires both halves to share an author, so the other author's head ins never pairs with the editor's nested del. `track-changes list` output is unchanged on every fixture. Triage-Routine: true Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GoxD36rnNfg6uArkVxtJL
…; page-break tail is a marker (#13) Final pass on the in-revision span replace: - `cutRemnants` keeps only ZERO-width children of the cut runs (note/comment references, drawings, text-box anchors — `inlineMarkerWidth === 0`, now exported from the parser barrel); a tab/break/sym the offsets counted as a character goes with the matched text instead of surviving the replacement. - Range-start markers, and anything ahead of the first cut run, are emitted BEFORE the replacement; range ends, reference runs and kept remnants AFTER it — a comment that opened inside the cut brackets the new words instead of collapsing to a point. - `isPureMarker` treats a run of nothing but `w:rPr` / `w:commentReference` / `w:lastRenderedPageBreak` as a marker, so such a tail no longer mints a phantom fresh-id `<w:ins>`. - Docs: drop the stale "pairs without checking authors" sentence; state the author-equality rule in the revN section of cli/track-changes/CLAUDE.md. Triage-Routine: true Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GoxD36rnNfg6uArkVxtJL
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 docx-cli triage
Replaces #15 (closed): a from-scratch, narrowly gated fix instead of the rewrite.
What was wrong
replace --track --author Editor(andedit --at pN:S-E --track) on text inside Reviewer A's pending<w:ins>left the replacement as a bare run inside A's insertion, so Word credited it to A. If A's insertion also held Reviewer B's nested<w:del>before the match, the cut slid left by its length and the wrong words were deleted:What changed
replaceSpanInParagraphgets a single early branch: under tracking, when a non-empty span lies wholly inside a<w:ins>/<w:moveTo>that is a direct child of the paragraph, it dispatches tosrc/core/find/replace-in-revision.tsx(new, ~250 lines). Every other input runs the existing code untouched.<w:del>nested inside A's insertion; the replacement is Editor's own<w:ins>as a sibling; A's tail continues in a copy with a freshw:id(same author/date). Nested children (B's deletion, bookmarks, comment markers) keep their position and are never re-wrapped; a tail holding only markers isn't minted as a revision.<w:del>; the text is edited in place inside the existing<w:ins>(same id/date). Zero-width anchors in the cut (footnote/comment references, images, text-box anchors) survive; counted characters (a matched tab) go with the text; a comment range that opened in the cut brackets the replacement. Exact string match onw:author. An author's own<w:moveTo>takes the split path (moved text is relocated original text, not new words).revNpairing (cli/track-changes/groups.ts, +1 condition): a del+ins pair must share an author, so A's head insertion no longer pairs with Editor's nested deletion into one acting handle.How it works
flowchart TD A[tracked span, all runs under one parent] --> B{parent is w:ins / w:moveTo directly in the paragraph?} B -- no --> C[existing code, byte-identical to main] B -- yes, other author --> D[nest Editor w:del in it · Editor w:ins beside it · tail copy with fresh id] B -- yes, same author, w:ins --> E[edit in place inside the insertion, keep anchors]Verification
read --accepted→ "Fees are due. The Client may withhold disputed sum."; B's deletion byte-identical; all outputs passdocx validate. Same-author case → one insertion,may withhold a disputed sum.main: 18 commands × 36 fixtures (untracked/tracked replace,--all, span edits, empty-span inserts,--current/--baseline, hyperlinks) = 648 runs; 636 byte-identical, and all 12 differing runs are gated cases (tracked matches inside an existing insertion/move intracked-changes,multi-tracked,tracked-moves).track-changes listover all fixtures: 72/72 identical.bun run check✅bun run test:unit✅ (1740 tests, +16 new, no existing expectation changed)./code-review xhighpasses on the full diff: no regression againstmainfound in any pass. Every finding inside the new path was fixed (anchors, moveTo, phantom tail, revN authors, counted characters, range ordering, page-break marker).Pre-existing issues the reviews surfaced (deliberately NOT touched here)
Each can be its own issue; say which you want filed:
rebuildContainerstarts its offset walk at the first overlapping run, so an untracked replace whose match is not in a wrapper's first run (multi-run<w:ins>, multi-run<w:hyperlink>) still cuts the wrong words. This is the real root cause of the offset half ofreplace --trackinside another author's pending insertion credits the new text to that author, and misses the target next to a nested deletion #13; the gated path computes the start correctly only for the tracked case.<w:del>.<w:ins>(lists as→ ¶).w:id(same family as Untracked replace across a hyperlink nested in another author's insertion corrupts text and duplicates w:id #16).track-changes listlabels any text-less run-level revision as¶ paragraph mark; accepting a nested deletion leaves an empty outer<w:ins>shell.multi-tracked.docxwould change itstoHaveLength(6)expectation.Decisions for you
Fixes #13
🤖 Generated with Claude Code
https://claude.ai/code/session_012GoxD36rnNfg6uArkVxtJL
Generated by Claude Code