Skip to content

fix(core): tracked span replace inside another author's insertion - #15

Closed
kklimuk wants to merge 5 commits into
mainfrom
claude/triage-issue-13-tracked-replace-in-ins
Closed

kklimuk wants to merge 5 commits into
mainfrom
claude/triage-issue-13-tracked-replace-in-ins

Conversation

@kklimuk

@kklimuk kklimuk commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

🤖 docx-cli triage

What was wrong

replace "disputed amount" "disputed sum" --track --author Editor on text inside Reviewer A's pending <w:ins> left "disputed sum" bare inside A's insertion, so it was credited to A. When A's insertion also held Reviewer B's nested <w:del>, offsets slid 24 characters: "The Client may " was deleted instead of the match. Both cases exited 0 with 1 occurrence replaced. edit --at pN:S-E --track shares the same code path (replaceSpanInParagraph) and had the same bug.

What changed

  • Under tracking, a match that sits entirely inside a tracked-change wrapper now splits that wrapper where it is. Editor's <w:del> stays nested in A's insertion, "disputed sum" goes in Editor's own <w:ins> between the halves, and the trailing half keeps A's author/date with a new unique w:id, which is how Word records it. Offsets count from the wrapper's start, and B's deletion is moved whole, untouched.
  • Code review on the rewrite also fixed these, each with tests:
    • Tracked replacements inside a hyperlink, smartTag or field used to land as bare text. They now get their own <w:ins>, and cut link text becomes a tracked <w:del>.
    • Insertion-point spans (pN:S-S) used to be appended untracked at the end of the paragraph. They now land at the point, tracked.
  • Files: mostly src/core/find/replace-span.tsx (a ~595-line rewrite), a new isTrackedChangeWrapper helper in src/core/parser/run-ops.ts, a filter option on wrapContiguousTrackable, and a notes section in src/core/track-changes/CLAUDE.md.
  • Fixture: multi-tracked.docx gets a cross-author p3 (Editor edits inside Reviewer A's still-pending insertion). Two tracked-change count assertions in track-changes.test.ts went up to match; tc0–tc5 are unchanged.

How it works

flowchart TD
  A[match span in paragraph] --> B{tracked and span inside one ins/del wrapper?}
  B -- no --> C[paragraph-level del + ins, as before]
  B -- yes --> D[split wrapper at span end; new w:id on tail half]
  D --> E[wrap matched plain runs in Editor w:del, still inside head half]
  E --> F[insert Editor w:ins between halves]
Loading

Follow-up commit (maintainer decisions)

  • Same author merges: when the editor is the author of the enclosing <w:ins>/<w:moveTo>, a span wholly inside it is edited in place: the cut text is removed, and the new text joins that insertion (same w:id). This matches Word editing your own pending insertion. Different authors keep the split shape.
  • --baseline = all changes rejected: runs now record their enclosing revision (within in the AST), and a run is hidden in a view if any revision around it is. Read, wc, find, the markdown renderer and iterateBlocks({view}) all share isRevisionVisible.
  • Code review in that pass also fixed:
    • Nested wrappers: a link, smart tag or insertion nested inside a split wrapper is now split recursively, so its matched text is cut.
    • revN pairing: a del and an ins now pair only when both are by the same author.
    • Untracked edge insertions no longer join the neighbouring author's insertion.
    • Fenced code blocks now respect --accepted/--baseline.

Verification

  • Same author: <w:ins 1 A>may withhold a disputed sum.</w:ins>, with no del and one revision.
  • Different author (case 1): the XML is as shown above. read --baseline → "The Client ", identical to track-changes reject --all; read --accepted → "The Client may withhold a disputed sum."
  • Nested-deletion doc (case 2): --baseline → "Fees are due. ", identical to reject --all; --accepted → "Fees are due. The Client may withhold disputed sum."
  • A new test asserts read --baseline ≡ reject --all and read --accepted ≡ accept --all for del-in-ins and ins-in-del docs, plus wc/find --baseline.
  • bun run check ✅ bun run test:unit ✅ (1733 tests).

Decisions (settled)

Not verified

  • The same-author merge on a <w:moveTo> (a tracked move) hasn't been checked against Word.

Fixes #13

🤖 Generated with Claude Code

https://claude.ai/code/session_012GoxD36rnNfg6uArkVxtJL


Generated by Claude Code

`replace --track` / `edit --at pN:S-E --track` whose match lay inside
another author's pending <w:ins> left the replacement bare inside that
insertion, attributing it to the other author. When the insertion also
held a nested <w:del> before the match, the span was rebuilt from the
first overlapping run's offset instead of the container's start, so the
wrong text was deleted while 1 replacement was still reported.

The span engine (replaceSpanInParagraph, shared by replace, replace
--batch and edit spans) now splits a revision wrapper that wholly holds
a tracked span within its own container, the way Word does: the cut's
<w:del> nests in the surrounding insertion, the replacement gets the
editor's own <w:ins>, and the trailing half of the insertion keeps its
author/date with a freshly allocated w:id. Container offsets come from
where the container starts. Nested revisions and markers that can't be
sliced move whole to the side they fall on, never into the new <w:del>.

Related span-engine fixes found in review:
- a tracked replacement inside a hyperlink/smartTag gets its own <w:ins>
- cut link/field text stays a <w:del> instead of being dropped untracked
- an empty span (pN:S-S) lands at the point, tracked, instead of being
  appended untracked at the paragraph end
- no empty phantom <w:del> for an insertion point inside a run

The multi-tracked fixture gains a cross-author paragraph (p3).

Triage-Routine: true
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GoxD36rnNfg6uArkVxtJL
@kklimuk

kklimuk commented Oct 8, 2026

Copy link
Copy Markdown
Owner Author

🤖 docx-cli triage

Opened this for #13. A tracked replace inside another author's pending insertion now splits that insertion and puts the new text in the editor's own <w:ins>, the way Word does. The nested-deletion offset shift is fixed, and both repro cases match the issue's expected output. Unit tests (1715) and bun run check pass. This is a sizable rewrite of replace-span.tsx, and it has one behavior change for same-author edits plus three known gaps, all listed in the description.

Action needed: approve/merge — decide the same-author split question and whether the three gaps become follow-up issues, then merge #15.


Generated by Claude Code

Same author merges: a tracked span replace/edit wholly inside the
tracked author's OWN pending <w:ins>/<w:moveTo> (exact w:author match,
nearest enclosing revision) no longer splits it into del + new ins. As
in Word, the cut is removed outright and the replacement joins the
existing insertion (same w:id/date); an insertion emptied by the edit
is dropped. Spans crossing out of it, and other authors, keep the split
shape unchanged.

Views follow the whole revision chain: the reader classified a run by
its innermost wrapper only, so a deletion nested in another author's
insertion showed in read --baseline although rejecting that insertion
removes it. TrackedChange gains `within` (the enclosing revision) and
one predicate, isRevisionVisible, decides visibility for text.ts/wc,
find, iterateBlocks({view}) and the markdown renderer, matching the XML
offset walkers. read --baseline/--accepted now equal reject/accept --all.

Found in review: nested links/smart tags/insertions inside a split
wrapper are split recursively (the match inside them was left behind);
an untracked insertion point at another author's revision edge lands
beside it; revN pairs only same-author del+ins; fenced code blocks
honor the accepted/baseline view; RunRevision schema covers break/tab/
text-box runs.

Triage-Routine: true
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GoxD36rnNfg6uArkVxtJL
claude added 3 commits October 8, 2026 19:49
Review fixes for the cross-author span replace:

- The span engine (now src/core/find/replace-span/: target, level,
  rebuild, split, replacement-runs) records each run's FULL wrapper
  path and rebuilds at the container above the OUTERMOST revision on
  the span's path under tracking, splitting every wrapper below. The
  editor's <w:ins> is never a descendant of another revision: a span
  in a link/smart tag inside another author's insertion, or in an
  insertion nested in a deletion, no longer lands inside it. A link the
  span sat in is re-created around the replacement.
- Cut text under ANY enclosing deletion stays as is (the flag now
  propagates through nested wrappers) instead of being re-deleted.
- Same-author merge keys on the nearest revision on the shared path, so
  a span crossing a link inside the own insertion still merges.
- An insertion point between two runs of a link lands inside the link
  with its style; at a link's edge it still lands outside.
- set/clear formatting, hyperlinks add and note references skip a
  wrapper by its accepted-view VISIBLE length (a deletion nested in an
  insertion counted, so edit --bold after a cross-author replace hit
  the wrong text); hyperlink/note placement now uses the accepted-view
  offsets find reports.
- One view rule: parser/revision-view.ts (isRevisionKindVisible,
  isWrapperVisibleInView, sumVisibleTextLength) — the AST's
  isRevisionVisible and every XML walker derive from it; the private
  copies in comments/markers.tsx are gone.
- read --baseline/--accepted filter breaks, tabs and images by the
  revision chain too (a break in a rejected insertion still rendered).
- track-changes list: a record's text includes nested revisions' text,
  so another author's insertion emptied by an edit lists as
  "withhold" (contains tc1) instead of "¶ paragraph mark" (now only a
  real paragraph-mark marker); --json drops the reader's within chain.
  revN pairs only a same-author, same-date del+ins neither nested in
  the other.

Triage-Routine: true
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GoxD36rnNfg6uArkVxtJL
…dation

Triage-Routine: true
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GoxD36rnNfg6uArkVxtJL
Triage-Routine: true
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GoxD36rnNfg6uArkVxtJL
@kklimuk

kklimuk commented Oct 8, 2026

Copy link
Copy Markdown
Owner Author

🤖 docx-cli triage

Closing at the maintainer's request. Three review rounds on this ~1,300-line rewrite each surfaced ~15 further issues, several of them regressions against main, so it is being replaced by a minimal, narrowly gated fix for #13 built from scratch on current main. The branch stays as a reference.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

replace --track inside another author's pending insertion credits the new text to that author, and misses the target next to a nested deletion

2 participants