Skip to content

fix(parser): nested text-box run-vanish leak (#641); refute object-table rule-row leak (#633) - #647

Merged
thewrz merged 11 commits into
mainfrom
fix/issue-641
Aug 4, 2026
Merged

fix(parser): nested text-box run-vanish leak (#641); refute object-table rule-row leak (#633)#647
thewrz merged 11 commits into
mainfrom
fix/issue-641

Conversation

@thewrz

@thewrz thewrz commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Why

Both issues are the same class of defect in body-objects.ts's captured-object text walk (collectText/extractBlobText), and both surface through hidden-text.integration.test.ts — investigated together, as directed.

What

Investigation notes (both premises proven empirically, per sprint policy)

#641 — reproduced on an unmodified checkout, then fixed:

before: interiorTexts = [{ text: "Outer visibleNESTED SECRET" }]
after:  interiorTexts = [{ text: "Outer visible" }]

#633 — reproduced on an unmodified checkout; the "fix" that broke an existing test:

before (unmodified): 4 leaked objectText nodes, each 40+ literal '*' characters

Implementing direction (a) from the issue (route object capture through isRuleRow) immediately turned note-region-corpus.integration.test.ts's "DOCX object-table verbatim rendering — hidden-text-test.docx (#300, ADR-072)" test red (expected 0 to be 4) — that test already pins this exact fixture's table rendering those exact 4 rule rows verbatim, by design. Reverted the code-level suppression; resolved via direction (b) instead (narrow the #294 test).

Related case named in #641 ("likely same fix"), confirmed and closed by the same mechanism: a single interior paragraph mixing one visible run and one w:vanish run (no nesting at all) leaked the vanish run's text the same way — pinned by its own regression test.

Mutation verification

Fixture corpus revalidation

pnpm fixture:snapshot / pnpm fixture:diff over the full 705-fixture corpus, comparing an unmodified checkout against this branch's final code: 0/705 fixtures changed. (No fixture in the corpus exercises the #641 nested-text-box shape, and #633 ended up as a test-only change — so this is the expected result, not a false negative; logged in the ADR.) Same single pre-existing parse-error fixture (11_53_00nle.docx) before and after — not a regression.

LOC note

This PR grows slightly past the file it touches most (body-objects.test.ts) by the sprint's own accounting rule (fixtures/tests count differently, but flagging per policy): the diff is concentrated in one parser module + its test file + one test-scoping fix + one ADR, all one coherent investigation per the sprint's "one PR, one question" directive.

Draft-phase review round (adversarial + independent verification)

Three further defects were found and fixed on this branch after the initial
implementation. All three are latent — no corpus fixture reaches any of
them — and full-corpus revalidation stayed at 0/705 changed throughout.

  1. w:vanish was read as element presence, not as the ST_OnOff toggle it
    is
    (82a05540). Per ECMA-376 §17.3.2.45 an explicit w:val of
    0/false/off switches the toggle OFF — a visible run. Because the
    new collectText branch DROPS text, reading presence as "hidden" would
    silently delete visible spec text. The ADR had dismissed this as
    "vanishingly rare"; it is not — two real CPI fixtures carry 15
    <w:vanish w:val="0"/> runs between them, several text-bearing
    (CPI_COMMUNICATIONS_RACK_MOUNTED_POWER_PROTECTION_CSIMFS.docx holds
    "Select voltage/phase; breaker number, " and "rating" in exactly that
    shape). Same fix applied to document.ts's runIsVanish /
    paragraphMarkVanish, which had the identical bug (8e083953,
    fix(cross):).

  2. The object EDIT walk was not updated to match the capture walk
    (039e1f77) — found by the Codex adversarial review. Dropping vanish runs
    changed what objectText means, but object-blob-edit.ts's
    rewriteFirstText still assumed "all text", despite a docstring promising
    it mirrors collectText. On a paragraph whose hidden run precedes its
    visible one, an edit landed in the hidden run while the visible run
    was blanked — the edit disappears from the document and real text is
    destroyed. Fixed by exporting hasRunVanish and reusing it, so the two
    walks cannot drift apart again.

Every gate added or changed here is mutation-verified in both directions,
so none of them can silently degrade into a no-op:

Gate Mutant Result
narrowed #294 assertion disable classifyOne's role === 'rule' branch red (expected true to be false) — still catches the paragraph-tier class
hasRunVanish (object walk) presence-only red — 2 survival tests
hasRunVanish (object walk) suppress nothing red — 5 tests incl. the ON-side
runIsVanish / paragraphMarkVanish presence-only red — 2 survival tests
runIsVanish / paragraphMarkVanish never vanish red — 8 tests incl. the ON-side
edit-walk vanish skip skip removed red — both ordering tests

Known divergence, deliberately NOT fixed here

merge/extract.ts's visibleText still reads hidden runs: for an
SDT-anchored cell paragraph mixing a hidden and a visible run it extracts
"HIDDEN SECRETvisible text" where the AST now stores "visible text", so an
untouched round-tripped DOCX can report as modified. The obvious remedy —
make visibleText skip vanish runs — is wrong as stated: it is merge's
general paragraph walk, and the paragraph tier deliberately KEEPS hidden-run
text in a mixed paragraph (document.ts's extractParagraphText, pinned by
a KNOWN AMBIGUITY test). A blanket skip would create the mirror divergence
for every ordinary paragraph. A correct fix must scope visibility to object
interiors only — a merge-engine semantics change affecting every table-cell
paragraph, owed its own ADR and PR. Recorded in ADR-092 rather than left to
be rediscovered; latent today (0/705).

Testing

  • pnpm lint (eslint + tsc --noEmit + prettier --check)
  • pnpm build
  • pnpm test — 252 files / 3563 tests passed
  • pnpm test:integration — 169 files passed, 1 skipped / 2053 tests passed, 2 skipped (full suite, DB-backed)
  • Target tests specifically: hidden-text.integration.test.ts and note-region-corpus.integration.test.ts both green
  • pnpm fixture:snapshot / pnpm fixture:diff — 0/705 changed
  • Mutation verification on both new/changed gates (see above)
  • Manual verification: N/A (parser-only change, no UI surface)

🤖 Co-authored by Claude Sonnet 5. Closes #641. Closes #633.

Summary by CodeRabbit

  • New Features

    • Improved handling of hidden document text during text extraction and editing.
    • Hidden runs are now consistently excluded, while explicitly visible content remains available.
    • Preserved verbatim content in text boxes and tables, including rule-row text.
  • Bug Fixes

    • Corrected visibility handling for hidden-text toggle values.
    • Prevented edits from modifying hidden text.
  • Documentation

    • Added an architectural decision record documenting visibility behavior and limitations.

thewrz and others added 3 commits August 3, 2026 23:20
…or paragraph (#641)

body-objects.ts's captured-object text walk (collectText/extractBlobText)
had no depth limit and no visibility check, so a w:vanish run's text leaked
into an ancestor interior paragraph's captured objectText regardless of how
deep it sat — including inside a text box nested within another text box's
interior paragraph, and in the simpler same-paragraph case of two sibling
runs (one visible, one vanish). Neither existing hidden/visible mechanism
caught this: body-text-box-visibility.ts's box-level correlation treats a
text box's interior as opaque and never recurses into nested boundaries, and
header-footer-region.ts's run classification stops at each w:r.

collectText now skips any w:r flagged by a new run-vanish presence check
before descending into it, at whatever depth it's encountered — closing both
shapes with one mechanism, entirely inside the tree already being walked
(ADR-092), rather than extending the box-level hiddenFlags/hiddenSubtrees
correlation deeper (which would reintroduce the #636 near-miss's
correlation-desync risk one level down).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… are deliberately verbatim

hidden-text.integration.test.ts's "no bare-asterisk node anywhere" assertion
was failing against hidden-text-test.docx: 4 leaked objectText nodes, traced
to body-objects.ts's interior-paragraph capture never routing through
isRuleRow/classifyOne's rule-row suppression (ADR-086).

Routing object capture through that suppression was tried first and broke
note-region-corpus.integration.test.ts's own pre-existing, deliberately
scoped regression test for this SAME fixture, which pins its body table
rendering those SAME 4 asterisk-rule cells verbatim — ADR-072 decision 8
(#300) already establishes that a captured table/text-box's interior text is
a faithful, out-of-band, VERBATIM mirror of the source document, never
re-run through paragraph-tier suppression. The two tests encoded
contradictory expectations for the same nodes; #294's assertion simply
predates #300 and was never reconciled with it.

Resolution: narrow hidden-text.integration.test.ts's assertion to exclude
objectText nodes, with a comment citing ADR-072 decision 8 and the sibling
test that pins the verbatim behavior. body-objects.ts gets an explanatory
comment only, no logic change. body-objects.test.ts gains positive
regression tests pinning verbatim capture (a text-box and a table interior
paragraph whose text is a rule row both surface unchanged), replacing the
suppression tests from the initial, incorrect approach. Mutation-verified:
temporarily disabling ADR-086's paragraph-tier role === 'rule' check still
fails the narrowed #294 assertion, confirming it still catches genuine
paragraph-tier regressions rather than vacuously excluding too much.

Closes #633.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tation record

Records the combined design decision for #641 (fixed: run-vanish awareness
in the captured-object text walk, at whatever depth) and #633 (refuted: the
reported leak is ADR-072 decision 8's deliberate verbatim object-capture
behavior, not a defect — resolved by narrowing a test assertion instead).
Cross-references ADR-087 decision 7 (why hiddenSubtrees/hiddenFlags
correlation is deliberately not extended to nested boundaries), ADR-072
decision 8 (#300, the already-shipped verbatim-capture rule this ADR defers
to), and ADR-086 (paragraph-tier rule-row suppression, untouched).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds toggle-aware w:vanish handling, skips hidden runs during object-text extraction and rewriting, preserves verbatim object content, and adds regression coverage for nested text boxes, tables, paragraph visibility, and text editing.

Changes

DOCX visibility and object text

Layer / File(s) Summary
Toggle-aware vanish semantics
src/parser/docx/body-objects.ts, src/parser/docx/document.ts, src/parser/docx/*test.ts
w:vanish treats explicit 0, false, and off values as visible for runs and paragraph marks.
Nested object text capture
src/parser/docx/body-objects.ts, src/parser/docx/body-objects.test.ts, src/parser/docx/hidden-text.integration.test.ts, src/parser/docx/note-region-corpus.integration.test.ts, src/parser/docx/body-order.ts, docs/adr/092-object-text-walk-run-and-paragraph-suppression.md
Object text traversal skips vanished runs at nested depths. Visible text, nested OOXML, and decorative rule rows remain preserved as specified.
Visible object text rewriting
src/parser/docx/object-blob-edit.ts, src/parser/docx/object-blob-edit.test.ts, docs/adr/092-object-text-walk-run-and-paragraph-suppression.md
rewriteFirstText skips vanished runs and edits the first visible text leaf without changing hidden content.

Sequence Diagram(s)

sequenceDiagram
  participant DOCX as DOCX object content
  participant collectText
  participant hasRunVanish
  participant objectText
  DOCX->>collectText: Traverse nested text-box or table XML
  collectText->>hasRunVanish: Check each run's w:vanish value
  hasRunVanish-->>collectText: Return visible or hidden state
  collectText->>objectText: Add visible w:t content
Loading

Possibly related issues

Possibly related PRs

  • wrzonance/SpecR#513 — Directly relates to the existing object-text extraction and editing paths extended here.
  • wrzonance/SpecR#521 — Directly relates to object-blob-edit.ts handling of object text.
  • wrzonance/SpecR#636 — Directly relates to text-box visibility handling and nested hidden-run extraction.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the nested text-box hidden-run fix and the object-table rule-row finding addressed by the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

thewrz and others added 7 commits August 3, 2026 23:43
… path (#641)

Review found that #641's regression coverage only exercised the shared
run-vanish text walk (collectText) via buildTextBoxObject; buildTableObject
calls the identical anchorInteriorParagraphs/collectText walk but had no
independent test proving a table cell's hidden run stays excluded. The
existing all-vanish table fixture never reaches collectText at all — it is
classified fully-hidden by the pre-existing ADR-038 path before extraction.

Add two table-analogue tests mirroring the existing text-box cases: a
visible cell paragraph mixing one visible run and one w:rPr>w:vanish run,
and a visible cell paragraph with a nested hidden text box. Verified both
fail (leak the hidden text) with the vanish skip disabled and pass with it
restored — confirming the coverage gap is closed, not vacuous.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… text

The object-tier verbatim rule — a captured table/text-box's cell text is an
out-of-band mirror, never re-run through paragraph-tier note suppression — is
ADR-072 decision 14, which names this exact case ("an asterisk-rule decoration
row a spec author used purely as in-cell visual separation"). Decision 8 is
floating-vs-inline placement and says nothing about text capture.

This citation predates the branch (origin/main a9e6a69); #641 propagated it
into new comments, corrected separately. Comment-only, no behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ndary

computeBodyOrder's comment describes a KNOWN AMBIGUITY about nested
table/text-box artifacts never being independently visited. That is ADR-072
decision 20 ("a nested table/text-box inside a captured object's blob is
double-anchored but never independently addressable"), which carries the same
KNOWN AMBIGUITY label. Decision 8 is floating-vs-inline placement.

Pre-existing on origin/main (a9e6a69); found while auditing #641's citations.
Comment-only, no behaviour change — drop this commit if you prefer it separate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The object-tier verbatim rule these comments rely on is ADR-072 decision 14,
not decision 8. Decision 14 names this exact case — "an asterisk-rule
decoration row a spec author used purely as in-cell visual separation" —
and states that suppressing it would mean selectively editing locked content.
Decision 8 governs floating-vs-inline placement and is unrelated.

The wrong number was inherited from note-region-corpus.integration.test.ts
and propagated into the new comments and ADR-092. Comment/doc-only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… walk

#641's new `hasRunVanish` suppression used element PRESENCE to decide a run
was hidden. `w:vanish` is an ST_OnOff toggle (ECMA-376 §17.3.2.45): an
explicit `w:val` of 0/false/off means the toggle is switched OFF — a VISIBLE
run, usually one overriding an inherited vanish from its style. Because
`collectText`'s new branch DROPS text, reading presence as "hidden" silently
deletes visible spec text from a captured table or text box.

ADR-092 dismissed this as "vanishingly rare". It is not: two real CPI corpus
fixtures carry 15 `<w:vanish w:val="0"/>` runs between them, several of them
text-bearing — CPI_COMMUNICATIONS_RACK_MOUNTED_POWER_PROTECTION_CSIMFS.docx
holds "Select voltage/phase; breaker number, " and "rating" in exactly that
shape. No fixture puts one inside a captured object today, so nothing was
lost yet; the presence-only rule was a latent silent-data-loss landmine of
the same class as the #636 near-miss.

Regression tests pin the SURVIVAL direction on both capture paths (text box
and table) — the direction an over-suppression bug hides in, since it raises
no error, only missing text — plus the ON direction (`w:val="1"` and a bare
`<w:vanish/>` must still suppress) so the guard cannot regress to a no-op.
Both mutants verified red: presence-only fails the 2 survival tests;
suppress-nothing fails 5 including the ON-side test.

ADR-092's scope-limit section is corrected accordingly. The one remaining
limit (no `w:rStyle` character-style vanish resolution) is now stated with
its measured basis — 0 of the 39 corpus DOCX files use that shape — and with
the reason it is the acceptable side to err on: it under-suppresses, so its
failure mode is a detectable leak, never invisible data loss.

Corpus revalidation (`pnpm fixture:snapshot`/`fixture:diff`, 705 fixtures):
0/705 changed for main -> branch-before-this-fix, 0/705 for
branch-before-this-fix -> final, and 0/705 for main -> final. Three-part
structure and note-leak counts unchanged; same single pre-existing
parse-error fixture (11_53_00nle.docx) throughout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sibling of the object-text-walk fix in the preceding commit, found while
verifying it. `runIsVanish` and `paragraphMarkVanish` both decided hiddenness
from the mere PRESENCE of `w:vanish`. Per ECMA-376 §17.3.2.45 it is an
ST_OnOff toggle: an explicit `w:val` of 0/false/off switches it OFF, marking
a VISIBLE run or paragraph mark — usually one overriding an inherited vanish
from its style. Both call sites feed hidden-content suppression, so the
presence-only read could classify visible spec text as hidden and drop it.

Measured before changing anything: no fixture's output moves. The 15
`<w:vanish w:val="0"/>` runs in the two CPI corpus fixtures sit in
paragraphs that already resolve hidden through their `CMT` paragraph style,
so the run-level path never decides them. `pnpm fixture:snapshot` /
`fixture:diff` over all 705 fixtures: 0/705 changed, three-part structure and
note-leak counts unchanged, same single pre-existing parse-error fixture.
This removes a latent silent-data-loss path; it does not fix a live
regression, and it is deliberately kept to its own commit for that reason.

Three regression tests pin both directions: `w:val="0"` on a run and on a
paragraph mark must stay VISIBLE, and an explicit `w:val="1"` must still
hide. Mutants verified red — presence-only fails the 2 survival tests;
never-vanish fails 8 including the ON-side test, so the guard cannot regress
to a no-op in either direction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…k too

Adversarial-review P1. Dropping vanish runs from `collectText` changed what
an anchored interior paragraph's `objectText` MEANS — it is now the
paragraph's VISIBLE text, not all of its text — but `object-blob-edit.ts`'s
`rewriteFirstText` still assumed "all text". Its own docstring already
promised it mirrors `collectText` ("an edit reaches the text wherever
capture read it from"); that promise silently became false.

The damage is ordering-dependent and severe. On an interior paragraph whose
HIDDEN run precedes its visible one, an edit landed in the hidden run — so
the user's new text was invisible in Word — while the following visible run
was blanked, destroying real spec text. Verified by mutation: removing the
new skip fails both new ordering tests.

Fixed by reusing the SAME predicate rather than writing a second copy free to
drift: `hasRunVanish` is now exported from body-objects.ts and imported here,
so capture and rewrite cannot disagree again. A skipped vanish run is left
untouched rather than blanked — its text is locked, out-of-band content this
edit path has no mandate to rewrite (ADR-072's no-silent-loss posture) — and
that also keeps post-edit visible text exactly equal to the new text, which
is what capture reads back.

Three regression tests: hidden-run-first (the dangerous ordering),
hidden-run-last (which a naive "skip the first run" fix would break), and a
`w:vanish w:val="0"` run, which is VISIBLE and must remain a normal edit
target.

ADR-092 additionally records the ONE reader deliberately left unchanged:
merge/extract.ts's `visibleText`. Verified empirically to extract
"HIDDEN SECRETvisible text" where the AST now stores "visible text", so an
untouched round-trip can report as modified. The obvious remedy is wrong —
`visibleText` is merge's general paragraph walk, and the paragraph tier
deliberately KEEPS hidden-run text in a mixed paragraph (pinned by
document.test.ts's KNOWN AMBIGUITY test), so a blanket skip would create the
mirror divergence for every ordinary paragraph. A correct fix must scope
visibility to object interiors, changing merge's notion of text for every
table-cell paragraph — a merge-engine semantics change owed its own ADR and
PR. Latent today: 0/705 fixtures reach the shape.

Corpus revalidation main -> final: 0/705 changed. Unit 3563 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thewrz

thewrz commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

This was written agentically; verify its assertions and edit accordingly:

Adversarial review — Codex gpt-5.6-sol, effort xhigh (run once, on the final draft diff)

1 finding: 1 P1. Confirmed and fixed, with its suggested remedy partially declined on evidence.

[P1] Object-text readers were left out of sync with the new hidden-run filtering — src/parser/docx/body-objects.ts:217

When a captured table/text-box paragraph contains both visible and vanished runs, this skip stores only visible text in CapturedObjectText while leaving every hidden w:t under the same SDT anchor. merge/extract.ts::visibleText still reads those hidden leaves, so an unchanged generated DOCX is reported as modified; object-blob-edit.ts::rewriteFirstText likewise rewrites every leaf, so if the hidden run comes first an edit is placed into hidden content and the visible run is blanked. Apply the same visibility rule to the merge and edit walks, including unanchored vanished table paragraphs.

Both halves verified empirically before acting — not accepted on the reviewer's word.

Half A — object-blob-edit.ts::rewriteFirstText: CONFIRMED, FIXED in 039e1f77.
Correct and serious. rewriteFirstText's own docstring already promised it mirrors collectText ("an edit reaches the text wherever capture read it from"); this PR silently made that false. On an interior paragraph whose hidden run precedes its visible one, the edit landed in the hidden run — invisible in Word — while the visible run was blanked, destroying real text. Fixed by exporting hasRunVanish from body-objects.ts and reusing it, rather than writing a second copy free to drift. Skipped vanish runs are left untouched rather than blanked (ADR-072's no-silent-loss posture), which also keeps post-edit visible text exactly equal to the new text. Three regression tests cover hidden-first, hidden-last, and the w:vanish w:val="0" visible case; mutation-verified — removing the skip fails both ordering tests.

Half B — merge/extract.ts::visibleText: CONFIRMED as a divergence, but the suggested remedy DECLINED as written.
The divergence is real and I reproduced it: for an SDT-anchored cell paragraph mixing a hidden and a visible run, the merge extractor returns "HIDDEN SECRETvisible text" while the AST now stores "visible text", so an untouched round-trip can report as modified.

The proposed fix — "apply the same visibility rule to the merge walk" — would introduce a worse bug than it removes. visibleText is merge's general paragraph walk, not an object-specific one, and the paragraph tier deliberately KEEPS hidden-run text in a mixed paragraph (document.ts's extractParagraphText; pinned by document.test.ts's "does NOT mark vanish when only some runs are hidden" KNOWN AMBIGUITY test). A blanket vanish skip in visibleText would therefore create the exact mirror divergence for every ordinary body paragraph in every document.

A correct fix must scope visibility to object interiors only, which means threading a new mode through walkBlocks / visitParagraph / visibleText and changing merge's notion of text for every table-cell paragraph — a merge-engine semantics change that belongs in its own ADR and PR rather than inside a parser leak fix. It is latent today: no corpus fixture puts a mixed visible/hidden run pair inside a captured object (0/705 on pnpm fixture:diff). Recorded in ADR-092's Decision section so it cannot be silently rediscovered, and escalated for a human call.

Independently found and fixed in the same round (not from Codex)

  • 82a05540w:vanish was read as element presence rather than as the ST_OnOff toggle ECMA-376 §17.3.2.45 defines. An explicit w:val="0" means the toggle is OFF (a visible run), and since collectText's new branch drops text, the presence-only read would silently delete visible spec text. ADR-092 had called this "vanishingly rare" — it is not: two real CPI fixtures carry 15 such runs between them, several text-bearing.
  • 8e083953 (fix(cross):) — the identical presence-vs-toggle bug in document.ts's runIsVanish / paragraphMarkVanish. Measured before changing: 0/705 fixtures move, because those CPI paragraphs already resolve hidden via their CMT paragraph style. Latent landmine removed, not a live regression fixed.

Verification

pnpm lint clean · pnpm test 252 files / 3563 passed · target integration suites (hidden-text + note-region-corpus) 40 passed · pnpm fixture:snapshot/fixture:diff 0/705 changed for main → final, three-part structure and note-leak counts unchanged, same single pre-existing parse-error fixture. Every gate added or changed is mutation-verified in both directions (see the PR body's table), including the narrowed #294 assertion, which still goes red when classifyOne's role === 'rule' branch is disabled — so narrowing it to exclude objectText did not make it vacuous.

🤖 Co-authored by Claude Opus 5 (1M context).

@thewrz
thewrz marked this pull request as ready for review August 4, 2026 16:56
@thewrz

thewrz commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/parser/docx/document.ts (1)

133-145: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve explicit OFF when resolving inherited vanish.

A direct w:vanish w:val="0" does not stop the fallback style checks. runIsVanish continues to w:rStyle, and resolveParagraphVanish continues to vanishStyleIds. A direct OFF value can therefore still suppress text from an enabled character or paragraph style.

Return a tri-state value: absent, ON, or OFF. Consult inherited styles only when the local value is absent.

  • src/parser/docx/document.ts#L133-L145: make explicit OFF terminal before checking vanishCharStyleIds.
  • src/parser/docx/document.ts#L187-L202: make explicit paragraph-mark OFF terminal before checking vanishStyleIds.
  • src/parser/docx/document.test.ts#L211-L243: add named regressions for direct-OFF character-style and paragraph-style overrides.
  • docs/adr/092-object-text-walk-run-and-paragraph-suppression.md#L119-L137: revise the decision text to match the tested precedence.

As per coding guidelines, add a regression test for every bug fix named after the observed symptom.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/parser/docx/document.ts` around lines 133 - 145, Update runIsVanish and
resolveParagraphVanish in src/parser/docx/document.ts (lines 133-145 and
187-202) to resolve local vanish values as absent, ON, or OFF, stopping
inheritance when an explicit OFF is present before checking character or
paragraph style IDs. Add named regressions in src/parser/docx/document.test.ts
(lines 211-243) for direct-OFF character-style and paragraph-style overrides,
and revise docs/adr/092-object-text-walk-run-and-paragraph-suppression.md (lines
119-137) to document the tested precedence.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/parser/docx/body-objects.ts`:
- Around line 187-192: Resolve character-style vanish through
StyleMap.vanishCharStyleIds before object capture and rewrite: update
hasRunVanish and the collectText path in src/parser/docx/body-objects.ts (lines
187-192 and 216-225), and the rewriteFirstText flow in
src/parser/docx/object-blob-edit.ts (lines 196-202), to recognize runs whose
w:rStyle applies vanish. Add capture regressions in
src/parser/docx/body-objects.test.ts (lines 1038-1076) and rewrite regressions
in src/parser/docx/object-blob-edit.test.ts (lines 277-339). Document the
behavior and updated handling in
docs/adr/092-object-text-walk-run-and-paragraph-suppression.md (lines 139-153).

---

Outside diff comments:
In `@src/parser/docx/document.ts`:
- Around line 133-145: Update runIsVanish and resolveParagraphVanish in
src/parser/docx/document.ts (lines 133-145 and 187-202) to resolve local vanish
values as absent, ON, or OFF, stopping inheritance when an explicit OFF is
present before checking character or paragraph style IDs. Add named regressions
in src/parser/docx/document.test.ts (lines 211-243) for direct-OFF
character-style and paragraph-style overrides, and revise
docs/adr/092-object-text-walk-run-and-paragraph-suppression.md (lines 119-137)
to document the tested precedence.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 08432a20-0523-4cea-94ef-05bb796c729c

📥 Commits

Reviewing files that changed from the base of the PR and between 35935f1 and 006f80f.

📒 Files selected for processing (10)
  • docs/adr/092-object-text-walk-run-and-paragraph-suppression.md
  • src/parser/docx/body-objects.test.ts
  • src/parser/docx/body-objects.ts
  • src/parser/docx/body-order.ts
  • src/parser/docx/document.test.ts
  • src/parser/docx/document.ts
  • src/parser/docx/hidden-text.integration.test.ts
  • src/parser/docx/note-region-corpus.integration.test.ts
  • src/parser/docx/object-blob-edit.test.ts
  • src/parser/docx/object-blob-edit.ts

Comment thread src/parser/docx/body-objects.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant