feat(reports): merge runt continuation chunks and mark continuation rows (#1940) - #2032
Conversation
A continuation row blanks every leading column, so under a freshly repeated header it is visually indistinguishable from the orphaned-cell bug #1929 fixed. The degenerate form is worse: with no minimum trailing-chunk size, text whose tail lands just past a chunk boundary produced a row blank except for a single stray character, which reads as document corruption on a document whose whole purpose is to look credible. Both forms were reproduced by rasterized render. - Add packUsageCellRowsWithMinimum, which merges a would-be runt into the row before it. It wraps packUsageCellRows' row-level output rather than changing the chunker, because a runt arises from two paths -- the hard-split remainder and the "fits a fresh row" branch -- and a fix inside splitIntoPageSafeChunks would miss the second. Both of those functions are byte-identical to beta. - Resolve AC1's tension with AC2 by gating the reduced-budget repack behind an actual runt check. Packing once at full budget and returning it untouched when no runt exists preserves today's exact row counts for everything that fits one row and for multi-row content that already divides cleanly; an unconditional budget reduction would have split content that fits today, regressing the real-render-verified zero-degradation range for no reason. No ceiling is re-derived, and usageChunkCharsForWidth's one-sided clamp is untouched. - Prepend a literal ellipsis run to continuation rows. The ux-designer chose ink shape over colour or fill deliberately: this is a bank document that gets scanned, and colour is what degrades under greyscale printing. Usage is also the only cell that can carry the signal, since every other cell is blanked on a continuation row and #1973's subsets may leave no leading column at all. The marker is render-time only and never enters the reconstruction invariant. - Express the floor per-subset as max(MIN_CONTINUATION_ROW_FLOOR_CHARS, usageSafeTokenChars) so a merged runt fills at least one real line at whatever width the active subset gives Usage. Adds fontkit as an exact-pinned devDependency to verify the font has a real glyph for U+2026 rather than .notdef. It was already a production transitive dependency via pdfmake -> pdfkit, so this installs nothing new and leaves the runtime image unchanged. Fixes #1940 Co-Authored-By: Claude frontend-developer <noreply@anthropic.com> Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com> Co-Authored-By: Claude ux-designer <noreply@anthropic.com>
|
[security-engineer] Dependency-change review — verdict: clean, approve. Scope was the Supply-chain posture.
Lockfile correctness. Both packages hoisted to root
Classification. Correct as a devDependency. The only reference anywhere in On proportionality, which the orchestrator asked me to judge rather than assume: no concern. Since the package was already a transitive production dependency with zero new install footprint, this is not "pulling in a package for one test" — it is pinning one that was already there. The weaker fallback (measuring the marker's rendered width) is not needed on security grounds. |
|
[ux-designer] Verdict: APPROVED — implements the spec I posted on #1940 faithfully, and I re-verified the outcome it exists for by rendering. Point-by-point against the spec
The two things flagged for extra scrutiny
Not in scope, not reviewedChunking algorithm correctness beyond the runt-merge property tests, the No blocking findings. This closes the loop from #1929 round 4 cleanly. |
|
[product-architect] VERDICT: APPROVE — no blocking findings. The AC1/AC2 resolution is the right one, and the safety argument holds under an independent read. Four non-blocking follow-ups below, one of which folds into the ADR-034 pass already owed from #2007/#2028. 1. The AC1/AC2 resolution — right design, and the induction is soundIs this a two-pass design where a single-pass would do? No. I worked the alternatives and each one is strictly worse:
So the gate isn't a hedge — it is the only structure that satisfies AC1 and AC2 without paying AC8. Cost is one extra AC2 induction — independently verified, twice. I traced it by hand and then falsification-tested it. The hand proof, stated in the terms I'd want it defended in:
Falsification run (throwaway harness, verbatim ports of all three functions, discarded after): 400,000 randomized cases — Zero AC1 violations, zero rows over 2. Wrapping
|
…reasoning - Records why the runt-merge wraps packUsageCellRows' row-level output rather than changing the chunker: the AC's unit is the rendered row, while the chunker's unit is a chunk within one segment, and a row can hold a prose chunk plus the grey meta segment. The two-code-paths argument is true but the unit mismatch is the load-bearing reason. - Records the AC2 induction and that it was confirmed by fuzzing 400,000 cases rather than a third hand-trace: zero AC1/AC2/I1 violations, max overage 0. - Records that the marker is ink shape rather than colour deliberately, since colour is what degrades under the greyscale printing and photocopying a bank document undergoes, and that the visual outcome was confirmed by rasterizing real renders at the narrowest and widest legal subsets rather than inferred. - Records that the threshold-to-ceiling ratio does NOT stay constant across subsets -- the one-sided clamp pins the ceiling while the floor scales with width -- so that framing must not be reused as a justification. Refs #1940 Co-Authored-By: Claude product-architect <noreply@anthropic.com> Co-Authored-By: Claude ux-designer <noreply@anthropic.com>
|
🎉 This PR is included in version 2.14.0-beta.20 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
|
🎉 This PR is included in version 2.14.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
A continuation row blanks every leading column, so sitting under a freshly repeated header it is visually indistinguishable from the orphaned-cell bug #1929 fixed. The degenerate form is worse: with no minimum trailing-chunk size, text whose tail lands just past a chunk boundary produced a row blank except for a single stray character, which reads as document corruption on a document whose entire purpose is to look credible. Both forms were reproduced by rasterized render during the #1935 review rounds, not hypothesised.
packUsageCellRowsWithMinimummerges a would-be runt into the row before it. It wrapspackUsageCellRows's row-level output rather than modifying the chunker, because a runt arises from two paths — the hard-split remainder and the "doesn't fit this row's leftover but fits a fresh row" branch — and a fix insidesplitIntoPageSafeChunkswould have missed the second. Both of those functions are byte-identical tobeta, verified independently by the reviewer against the beta blob.…run prepended to continuation rows.max(MIN_CONTINUATION_ROW_FLOOR_CHARS, usageSafeTokenChars), so a merged runt fills at least one real line at whatever width the active subset gives Usage. The two operands are a deliberate non-redundant pair: the per-line figure does the main work, the constant is the absolute fallback for subsets where that figure is itself small.How AC1's tension with AC2 was resolved
AC1 wants a runt merged backwards; AC2 forbids that merge pushing the preceding row past the one-page budget the ceiling exists to guarantee. The naive fix — reduce every row's budget by the minimum — is provably safe but regresses AC8: it splits content that fits in one row today whenever that content falls in the
(maxChars - min, maxChars]band, even though no runt was involved.The implementation instead gates the reduced-budget repack behind an actual runt check. Pack once at full budget; if no row after the first is short, return it untouched. Only when a genuine runt exists does it repack at the reduced budget and merge backwards. So the zero-degradation range is byte-for-byte unchanged, and no ceiling is re-derived —
usageChunkCharsForWidth's deliberate one-sided clamp is untouched.The AC2 safety argument is carried in the doc comment rather than just its conclusion: every row
packUsageCellRowsreturns is<=the budget it was called with, and backward iteration means a merge receiver has never yet been grown by the pass, so every merge yields(<= maxChars - min) + (< min) < maxCharsat any cascade depth. The reviewer traced that induction by hand against the code and confirmed the comment matches the implementation.Two corrections to the issue text
MAX_SAFE_SMALL_CHUNK_CHARS(450). That constant no longer exists — it was Report PDF layout breaks: usage column overflows the page, rows split across page breaks, running header clipped #1929 round-4's 9pt suffix ceiling, removed when fix(reports): improve report PDF UX — paragraph breaks, inline meta, inline notes, column toggles #1959 moved the suffix inline. There is one ceiling today.Why the marker is a glyph and not text or colour
The ux-designer verified structurally that continuation rows are only reachable when Usage is visible, and that every other cell is unconditionally blanked — so Usage is the only cell that can carry a signal, and a repeated-identifier approach fails outright in subsets like
{allocatedAmount, usage}where no leading column exists. Colour and fill were rejected on two grounds: makingTABLE_LAYOUTrow-aware is disproportionate here, and colour is precisely what degrades under the greyscale printing and photocopying a bank document undergoes. Ink shape survives that; a fill does not.No i18n key is introduced, so AC6 is satisfied vacuously.
Test evidence
UsageCellSegment.text, so AC3's exact-reconstruction invariant (Report PDF layout breaks: usage column overflows the page, rows split across page breaks, running header clipped #1929 I1, highest precedence) is preserved.maxChars - mintomaxChars * 4asserts jointly that no row after the first is under the floor and no row exceeds the ceiling — the test most likely to catch a flaw in the induction that a hand-picked fixture would miss.fontkit.glyphForCodePoint(0x2026).id !== 0(glyph id 0 is.notdefby spec), with a positive control proving the check can fail in this exact font. A raw width measurement would not have been meaningful, since fonts routinely give.notdefa real advance.overviewPdf.ts.On the new devDependency
fontkitand@types/fontkit, exact-pinned. Thesecurity-engineerconfirmed it was already a production transitive dependency viapdfmake→pdfkit, so declaring it installs nothing new, adds no transitive packages, changes no integrity hash, and leaves the Docker runtime image byte-identical.@types/fontkitverified genuine DefinitelyTyped (publisher content hash present), not a typosquat.npm auditunchanged.Fixes #1940
🤖 Generated with Claude Code