Skip to content

fix(replace): refuse a text span that runs across an equation - #29

Open
xsvm wants to merge 3 commits into
kklimuk:mainfrom
xsvm:fix/replace-span-equation
Open

xsvm wants to merge 3 commits into
kklimuk:mainfrom
xsvm:fix/replace-span-equation

Conversation

@xsvm

@xsvm xsvm commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Summary

An inline equation takes no characters in find/replace offsets, so a match could run across one without the matched text showing it. Repro: a paragraph Energy is $E=mc^2$ in physics.

Command Before
replace "Energy.*physics" "Gone" --regex Gone$E=mc^2$., exit 0, no hint: the words around the equation are cut and the equation is stranded in the new text
same with --track both text halves tracked as deleted, the equation stays live; accept/reject never touches it
edit --at p0:7-13 --text XX same stranding
edit --at p0 --text "New text" the equation silently jumps to the start of the paragraph, untracked
find "is in" matches, with nothing to say an equation sits between the two spaces

A footnote reference inside the span is deleted with it (it lives inside a <w:r>); a direct <m:oMath> child of the paragraph fell through the span walkers' keep-unknown-siblings branch.

The fix is a data check on what find already returns

  • find tags each match with the equations strictly inside it: equationsWithin (find/index.ts) sits next to trackedChangesOverlapping and walks the same AST runs on the same runViewText widths, so it can't disagree with the offsets find reports. TextMatch.equations / ParagraphSpanMatch.equations carry the EquationRuns, so find --json now shows them.
  • Single replace, replace --batch, cross-paragraph replace and edit --at pN:S-E refuse such a match with UNSUPPORTED before anything is applied, through one shared spanOverEquationsError. The message names the locator, e.g. p0: the text span covers eq0 ($E=mc^2$), and the hint points to equations edit --at eqN or a markdown rewrite. --dry-run refuses the same way; in a batch nothing is saved, like an invalid pattern. applyAcrossReplace reports the refused match as crossing and applies nothing.
  • A match that only touches an equation's edge (replace "Energy is " "Mass ") still works and keeps the equation.
  • Whole-paragraph edit --text on a paragraph holding an equation is refused: --text is the literal channel and has no way to say where the equation goes. A markdown rewrite with $…$ keeps working. The Edit lens gets the AST paragraph through a small Body.findBlockByNode, the reverse of resolveBlock.

No new XML offset walker, no new error class.

Note on #27: on main an EquationRun doesn't record the <w:del> around it, so an equation already tracked as deleted still counts as inside a span. Once #27 adds trackedChange to EquationRun, equationsWithin should skip runs isRevisionVisible hides; I can add that line to whichever PR merges second.

Test plan

  • bunx biome check ., bunx knip, bunx tsc --noEmit
  • 5 new tests in tests/cli/replace.test.ts (single/--track/--dry-run refusal, edge match, find --json, cross-paragraph, batch) and 3 in tests/cli/edit.test.ts (span, --text, markdown rewrite)
  • replace / edit / find / batch / equations / output-contract / info suites pass (533 tests)
  • Full suite: CI on this PR

An equation takes no characters in find/replace offsets, so a match could
run across one unseen. Replacing it cut the words around the equation and
left the equation stranded in the new text, exit 0, no hint; a whole-
paragraph `edit --text` moved it to the front of the paragraph.

- find tags each match with the equations strictly inside it
  (`equationsWithin`, on the same runViewText offsets as
  trackedChangesOverlapping), so `find --json` shows them.
- replace (single, --batch, cross-paragraph) and `edit --at pN:S-E` refuse
  such a match with UNSUPPORTED, naming the eqN, before anything is written;
  `--dry-run` refuses the same. A match touching only an equation's edge
  still replaces.
- `edit --text` on a paragraph holding an equation is refused; the hint
  points to a markdown rewrite with $…$.
@kklimuk

kklimuk commented Oct 29, 2026

Copy link
Copy Markdown
Owner

🤖 docx-cli triage

Thanks @xsvm, this is a real bug and the fix is well-shaped. Here's what I checked and what I'd still change.

What I checked (head b8132b6)

What the PR says it does: before this, a find/replace match or pN:S-E span could run across an inline equation without the text showing it, because an equation takes no characters. Cutting that span left the equation stranded. A whole-paragraph edit --text moved the equation to the front. The PR refuses all of these with UNSUPPORTED.

Repro doc: Energy is $E=mc^2$ in physics.

Command main this PR
replace "Energy.*physics" "Gone" --regex Gone$E=mc^2$., exit 0 UNSUPPORTED naming eq0, exit 1, file untouched
edit --at p0 --text "New words." $E=mc^2$New words., exit 0 UNSUPPORTED, exit 1
edit --at p0:7-13 --text XX Energy XX$E=mc^2$ physics., exit 0 UNSUPPORTED, exit 1

Also verified on the PR head:

  • Edge-touching matches still replace.
  • Table-cell paragraphs are covered.
  • edit --batch text and span entries refuse.
  • --text "", delete, --bold and --style on an equation paragraph still work.
  • bun run check and bun run test:unit pass.

Gaps the review found, and my fixes

I wrote these as commit 3276118 on top of yours, but I couldn't push it to your branch: the session's git proxy refused the push to xsvm/docx-cli (403, repo not in the session's authorized set). The commit is ready locally. Here's what it does:

  • Cross-paragraph replace still moved an equation sitting exactly at the match start. replace " in physics.\nMid" " X" gave Energy is X$E=mc^2$ … with exit 0. equationsWithin correctly treats that equation as an edge, but splitChildrenAt sent zero-width children at the start offset into the cut. The fix keeps edge children outside the cut on both sides. The result is now Energy is $E=mc^2$ X ….
  • Whole-paragraph edit --runs dropped the equation silently. It is now refused like --text.
  • edit --dry-run reported success for edits the real call refuses, both single and --batch. It now refuses the same way. edit --batch --dry-run now applies the entries in memory and skips only the save, the way replace --batch already does.
  • An equation already inside a pending <w:del> blocked every later --text rewrite. The whole-paragraph check now ignores deleted equations.
  • The refusal hint suggested pasting the canned sentence "Energy is $E=mc^2$ here." with the user's real locator. A weak agent could overwrite the real paragraph with that sentence. The hint now says to copy the line from docx read.
  • Refusals now name the full span (p0:7-13, or p0:14-p1:8 across paragraphs), not just the first block.
  • A shared describeEquations helper replaces the duplicated formatting, and the find --json test moved to find.test.ts.

With those fixes: bun run check ✅ and bun run test:unit ✅ (1950 pass). /simplify extracted the duplicated formatting into describeEquations. The /code-review xhigh findings are the items above.

Left as is:

Decisions for you

  • Is the change wanted at all? No issue asked for it. I think it is: it closes silent data loss with an exit 0.
  • Should whole-paragraph edit --text and --runs refuse when the paragraph has an equation? This changes behavior for existing users. The alternative is to drop the equation, which is what a range --text does.
  • Should a range edit --at pN-pM --text that covers equations refuse too, to match the new REFERENCE line "Text edits never strand an equation"?
  • Should a refused equation entry in replace --batch fail the whole batch with nothing saved? That's what an invalid pattern does. A missed entry, by contrast, still saves the entries that matched.

Action needed: routine blocked — the push of fix commit 3276118 to xsvm/docx-cli:fix/replace-span-equation was refused by the session git proxy. Add the fork to the session's sources and re-run, or ask @xsvm to apply the changes listed above. Once anything is pushed as kklimuk, your approval alone won't satisfy the last-push rule: you'll need a second approver or an admin-bypass merge.


Generated by Claude Code

- A cross-paragraph replace keeps an equation sitting exactly at the match
  start outside the cut (splitChildrenAt sends edge children outward on both
  sides), so it no longer moves behind the replacement.
- Whole-paragraph edit --runs refuses like --text instead of dropping the
  equation silently.
- edit --dry-run (single, span and --batch) applies in memory and skips only
  the save, so the preview refuses exactly what the real call would.
- An equation already inside a pending <w:del>/<w:moveFrom> no longer blocks
  a later --text rewrite.
- The refusal hint points at the paragraph's own read line instead of a sample
  sentence an agent could paste over the real paragraph.
- Refusals name the full span (p0:7-13, p0:14-p1:8); a shared
  describeEquations formats the equations.
- The find --json test moves to find.test.ts.

Follows the triage review on kklimuk#29.
@xsvm

xsvm commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I reproduced each gap on b8132b6 through the CLI first, and all of them were real. They're fixed in 9c68827, which I pushed myself, so the last-push rule shouldn't get in the way:

  • Equation at the start of a cross-paragraph match: splitChildrenAt now takes an edge side, so a zero-width child exactly at the start offset stays in the head and one at the end offset stays in the tail. replace " in physics.\nMid" " X" now gives Energy is $E=mc^2$ X ….
  • Whole-paragraph edit --runs is refused like --text.
  • edit --dry-run (single, span and --batch) now applies in memory and skips only the save, so it refuses whatever the real call refuses.
  • An equation inside a pending <w:del>/<w:moveFrom> no longer blocks a later whole-paragraph --text. This branch has no trackedChange on EquationRun yet, so the check walks the XML from the paragraph to the equation's node. Once fix(track-changes): show tracked equations, images and line breaks #27 lands it could read the run instead.
  • The hint no longer offers a sample sentence. It tells the agent to copy that paragraph's line from docx read FILE and edit it as markdown.
  • Refusals name the full span (p0:7-13, p0:14-p1:8), and a shared describeEquations formats the equation list.
  • The find --json test moved to find.test.ts. I added tests for each item above. REFERENCE.md now mentions --runs, deleted equations and --dry-run.

biome, knip, tsc and test:unit pass locally. The only failure is the Windows-only compiled-binary upgrade test, which also fails on main. Fork CI will run the rest.

As you noted, equationsWithin still counts tracked-deleted equations until #27 merges, and a range edit --at pN-pM --text still replaces whole paragraphs. I'm happy to follow up on either, depending on how you decide the open questions.

@kklimuk

kklimuk commented Oct 29, 2026

Copy link
Copy Markdown
Owner

🤖 docx-cli triage

Thanks @xsvm. I re-reviewed the new head 74ff900. Every gap from my last review is fixed, and you pushed the commits yourself, so the maintainer's own approval satisfies the last-push rule.

What I checked

Repro doc: Energy is $E=mc^2$ in physics. / Mid $x$ mid. / End two.

Command main (3c616a9) this PR (74ff900)
replace "Energy.*physics" "Gone" --regex Gone$E=mc^2$., exit 0 UNSUPPORTED p0:0-21 … eq0 ($E=mc^2$), exit 1, file untouched
replace " in physics.\nMid" " X" (equation at the match start) Energy is X$E=mc^2$ $x$ mid., exit 0 Energy is $E=mc^2$ X $x$ mid., exit 0 ✅
edit --at p0 --text "New words." $E=mc^2$New words., exit 0 UNSUPPORTED, exit 1
edit --at p0 --runs '[{"text":"Plain"}]' Plain (equation dropped), exit 0 UNSUPPORTED, exit 1
edit --at p0:7-13 --text XX Energy XX$E=mc^2$ physics., exit 0 UNSUPPORTED naming p0:7-13, exit 1
edit --at p0 --text … --dry-run / edit --batch … --dry-run exit 0, reports success UNSUPPORTED, exit 1, same as the real call
replace --batch (entry 0 clean, entry 1 over $x$) both applied, K$x$., exit 0 entry 1: p1:0-8 …, exit 1, nothing saved

Also verified:

  • The hint no longer offers a sample sentence. It now says "Copy p0's line from docx read FILE".
  • Table-cell paragraphs refuse for both edit --text and replace, with t0:r1c0:p0:7-15 named in the error.
  • --text "" on an equation paragraph still removes the line.
  • A markdown edit --dry-run leaves the file byte-identical.
  • An equation inside a pending <w:del> no longer blocks --text (covered by your unit test).
  • One side effect of the splitChildrenAt edge change that the description doesn't mention: a comment whose range starts exactly at a cross-paragraph match start now keeps its start. On main it collapsed to an empty anchor (7-7); now it covers the replacement (6-7). I think that's an improvement.
  • bun run check ✅. bun run test:unit: 1923 pass, 1 fail. The failure is the output-contract slow-pipe test, which timed out at 5 s under full-suite load. It passes when run alone (91/91), and none of this PR's files touch it.

I made no changes this round.

One small fix I'd still like (non-blocking, your call)

The new REFERENCE.md example and both refusal hints put markdown with $…$ inside double quotes, e.g. docx edit FILE --at pN "Energy is $E=mc^2$ here.". In bash, $E expands to an empty string, so the CLI receives Energy is =mc^2$ here.. The single $ doesn't parse as an equation, so the equation is silently lost and the edit exits 0. That's the same loss this PR prevents. A weak agent that copies the hint's quoting will hit it. Suggested change: use single quotes in the REFERENCE example, in the two hints (spanOverEquationsError and refuseLiteralOverEquations), and in the replace --help line, e.g. docx edit FILE --at pN '… $…$ …'.

Decisions for you (carried over)

  • Is the change wanted at all? No issue asked for it. I think it is: it closes silent equation loss that exits 0.
  • Should whole-paragraph edit --text/--runs refuse when the paragraph holds an equation? This changes behavior for existing users. The alternative is to drop the equation, as a range --text does.
  • Should a range edit --at pN-pM --text over equations refuse too, to match the new REFERENCE line "Text edits never strand an equation"? It still replaces whole paragraphs.
  • Should a refused equation entry in replace --batch fail the whole batch with nothing saved, as an invalid pattern does? A missed entry, by contrast, still saves the entries that matched.
  • Merge now, or wait for the single-quote fix above?
  • Follow-up for after fix(track-changes): show tracked equations, images and line breaks #27 merges: equationsWithin should skip tracked-deleted equations.

Action needed: approve/merge — PR #29 is ready once you've answered the questions above (optionally after @xsvm switches the examples to single quotes). The contributor made the last push, so your own approval satisfies the last-push rule.


Generated by Claude Code

@kklimuk

kklimuk commented Oct 30, 2026

Copy link
Copy Markdown
Owner

@xsvm hold off from making further changes till i've had a bit of time to read through and do my own reviews, it's gonna take me a few days.

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants