Skip to content

fix(replace): cut untracked spans in place across nested wrappers (#16) - #21

Merged
kklimuk merged 3 commits into
mainfrom
claude/triage-issue-16-replace-nested-ins-link
Oct 11, 2026
Merged

kklimuk merged 3 commits into
mainfrom
claude/triage-issue-16-replace-nested-ins-link

Conversation

@kklimuk

@kklimuk kklimuk commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

🤖 docx-cli triage

What was wrong

docx replace inslink.docx "withhold a" "keep a" (untracked; the match starts in Reviewer A's <w:ins> and ends inside a <w:hyperlink> nested in it) printed replace 1 occurrence replaced and exited 0. The result read The Client may [a disputed](#x)keep aamount., and both halves of the split ins had w:id="1". The same offset miscount also hit any untracked match in a later run of a multi-run ins or hyperlink: replace "disputed" "contested" gave macontestedld a disputed amount..

What changed

  • Untracked, a revision wrapper is no longer split. An additive <w:ins>/<w:moveTo> is cut in place by running the same walker over its content, so nested hyperlinks, fields and revisions are cut at the right offsets. The replacement lands where the match starts, so it stays in A's insertion, and no id is duplicated. A wrapper left with no children is dropped. A subtractive <w:del>/<w:moveFrom> stays whole.
  • Tracked: when a wrapper is split, its post-half now gets a fresh w:id from the allocator.
  • rebuildContainer now starts counting at the container's own offset (RunSlot.parentStart) and counts nested wrappers in any container.
  • Files: src/core/find/replace-span.tsx, src/core/track-changes/CLAUDE.md (updated the known-gap notes), and tests/cli/replace.test.ts (8 tests; 7 of them fail on main).

Verification

Decisions for you

  • Untracked: a replacement whose match starts inside another author's pending insertion now stays inside that insertion (credited to them), the same as a match wholly inside it. Before, it landed between two duplicate-id halves as plain text. OK?
  • Still a known gap: a tracked (--track) match crossing a hyperlink nested inside another author's ins still cuts at the wrong offsets. Also, a tracked replace across a top-level hyperlink deletes the link's cut text without a <w:del> (read --baseline loses it). Should these be follow-up issues?

Action needed: approve/merge — review the two decisions above, then merge.

Fixes #16
Fixes #22

🤖 Generated with Claude Code

https://claude.ai/code/session_019XWuv9YGyEx2uZMPbg5Egd


Generated by Claude Code


Generated by Claude Code

An untracked replace whose match crossed a revision wrapper split it into
two halves that both kept the wrapper's w:id, put the replacement between
them as plain original text, and never descended a hyperlink nested inside,
so the link's part of the match survived and the text scrambled.

Untracked, the cross-boundary walker now recurses into an additive
<w:ins>/<w:moveTo> and cuts it in place (nested hyperlinks/fields/revisions
sliced at the right offsets, replacement where the match starts, no id
duplicated); a subtractive wrapper stays whole. Tracked, the post-half of a
split wrapper gets a fresh id from the allocator. rebuildContainer now
starts at the container's own offset and counts nested wrappers, so a match
in a later run of a multi-run ins or hyperlink cuts the right words.

Triage-Routine: true
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XWuv9YGyEx2uZMPbg5Egd
…sertion (#22)

The #16 in-place cut already maps offsets past another author's nested
<w:del>; this regression test fails on main (it cut "The Client may "
instead of "disputed amount") and passes on this branch.

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

kklimuk commented Oct 11, 2026

Copy link
Copy Markdown
Owner Author

🤖 docx-cli triage

This PR also fixes #22, reported by @Dmitry-Kov. In that issue, an untracked replace matches text that comes after another author's <w:del> nested in a pending <w:ins>.

Repro: run docx replace nested-deletion.docx "disputed amount" "disputed sum" on Fees are due. + <w:ins A>The Client may withhold <w:del B>any </w:del>disputed amount.</w:ins>.

  • On main: the command printed replace 1 occurrence replaced and exited 0, but it cut the wrong span. The accepted text came out as Fees are due. disputed sumwithhold disputed amount.
  • On this branch (before my new commit): the accepted text is Fees are due. The Client may withhold disputed sum. Reviewer B's <w:del> and Reviewer A's <w:ins w:id="1"> are otherwise unchanged, and docx validate passes.

What I changed: I pushed one test-only commit, 130838a (a match after a deletion nested in the ins cuts the right words (#22)). It fails against main's replace-span.tsx and passes here. I added Fixes #22 to the PR description. bun run check ✅ bun run test:unit ✅ (1761 tests). The commit only adds a test, so I reviewed it by hand instead of running /simplify and /code-review.

Action needed: approve/merge — review this PR's open decisions, then merge it to close both #16 and #22.


Generated by Claude Code

Triage-Routine: true
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UKadSq4CY1JScFMncHeY4W
@kklimuk
kklimuk merged commit 89adae7 into main Oct 11, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants