Skip to content

fix(table): keep glyphs straddling a table outer edge (19% of negatives were losing their sign) - #14

Merged
hallelx2 merged 1 commit into
mainfrom
halleluyaholudele/hal-520-table-outer-edge-clipping
Aug 2, 2026
Merged

fix(table): keep glyphs straddling a table outer edge (19% of negatives were losing their sign)#14
hallelx2 merged 1 commit into
mainfrom
halleluyaholudele/hal-520-table-outer-edge-clipping

Conversation

@hallelx2

@hallelx2 hallelx2 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Fixes a silent sign-flip on financial data. Stacked on #9 — that branch is the base, so merge #9 first.

The bug

charsInCell assigns a glyph to whichever cell contains its centre. That is correct for an interior boundary: it decides which of two candidate cells owns a straddling glyph, and every glyph still lands somewhere.

At a table's outer edge there is no competing cell. The same rule stops disambiguating and starts deleting.

How it showed up

3M 2018 10-K, page 58, consolidated balance sheet. The value (16,048) ends with a ) spanning x=536.691..539.067, so its centre is 537.879. The last column ends at 537.871.

It missed by 0.008pt — about a nine-thousandth of an inch — and was dropped. (16,048) became 16,048: accounting notation for −16,048, read back as +16,048.

Across that filing's five financial statements:

                          before        after
page 56 (Income)          LOST  2       LOST 0
page 57 (Comprehensive)   LOST  0       LOST 0
page 58 (Balance Sheet)   LOST  3       LOST 0
page 59 (Equity)          LOST  0       LOST 0
page 60 (Cash Flows)      LOST 15       LOST 0
                          ─────────────────────
TOTAL 103 negatives       20 lost (19%)  0 lost

Every magnitude was already correct — only signs were wrong. That is what makes it dangerous: the output looks entirely plausible, so nothing downstream can flag it. A missing value is detectable; a flipped sign is not.

The fix

Outermost cells also keep glyphs that merely overlap the outer edge. The widening is bounded by the straddling glyph itself, so it cannot reach unrelated page content — only a glyph genuinely crossing the table's own boundary.

  • Interior boundaries: untouched.
  • charsInCell: keeps its exact previous behaviour for every other caller (delegates with both flags false, pinned by a test).

Verification

  • go build, go vet, go test ./... -count=1 -race — all green.
  • Existing table goldens pass unchanged, so this does not perturb the ruled-table path.
  • Re-measured on 3M pages 56–60: 103/103 negatives survive.
  • Ground truth was established independently — page 58 rendered to PNG and read directly, plus a poppler pdftotext -layout cross-check. Both confirm the parentheses exist in the document.

The regression test pins the real coordinates rather than round synthetic ones; the failure only reproduces at that 0.008pt margin.

Not fixed here

The opening ( still lands in the adjacent column (| ( | 16,135) |). That one is recoverable — the glyph exists, joining the row restores the value — so it is cosmetic rather than corrupting, and it needs column-boundary work rather than an edge rule. Same for | December 3 | 1, |. Tracked in HAL-520.

Closes HAL-520

Summary by Sourcery

Preserve glyphs that straddle a table’s outer edges so negative values retain their signs while keeping existing midpoint behaviour for interior boundaries unchanged.

Bug Fixes:

  • Prevent loss of closing parentheses at table outer edges that previously caused silent sign flips in financial figures.

Tests:

  • Add regression tests capturing real-world coordinates from a corrupted 10-K table and verifying that outer-edge widening does not absorb unrelated glyphs and that charsInCell retains its original behaviour.

charsInCell assigns a glyph to whichever cell contains its centre. That
is the right rule for an interior boundary -- it decides which of two
candidate cells owns a straddling glyph, and every glyph still lands
somewhere. At the table outer edge there is no competing cell, so the
same rule does not disambiguate, it deletes.

On 3M 2018 10-K page 58 the value "(16,048)" ends with a ")" centred at
x=537.879 while the last column ends at x=537.871. It missed by 0.008pt,
about a nine-thousandth of an inch, and was dropped -- turning the
accounting notation for -16,048 into a plain 16,048. Across that filing
five financial statements the same geometry flipped the sign of 20 of
103 negative numbers, 19%, with every magnitude still correct. A sign
error that leaves plausible-looking output is worse than a missing
value: a gap is detectable downstream, this is not.

Outermost cells now also keep glyphs that merely overlap the outer edge.
The widening is bounded by the straddling glyph itself, so it cannot
reach unrelated page content. Interior boundaries are untouched, and
charsInCell keeps its exact previous behaviour for every other caller.

Re-measured on the same five pages: 103 of 103 negatives now survive.
The regression test pins the real coordinates rather than round
synthetic ones, because the failure only reproduces at that margin.
@sourcery-ai

sourcery-ai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Reviewer's Guide

Modify table text assembly to keep glyphs that straddle a table’s outer horizontal edges, introduce a more general cell-edge-aware glyph selection helper, and add targeted regression tests to guard against both the original data-loss bug and unintended widening of table cells.

Sequence diagram for glyph assignment with outer table edges

sequenceDiagram
  participant assembleTableText
  participant charsInCellEdges

  assembleTableText->>assembleTableText: iterate tb.CellsGrid rows
  assembleTableText->>assembleTableText: find first,last nonzero columns
  assembleTableText->>charsInCellEdges: charsInCellEdges(chars, cell, ci==first, ci==last)
  activate charsInCellEdges
  charsInCellEdges->>charsInCellEdges: compute hMid,vMid
  alt [vMid outside cell vertical range]
    charsInCellEdges-->>charsInCellEdges: skip glyph
  else [vMid inside]
    alt [interior column]
      charsInCellEdges-->>charsInCellEdges: in = hMid within cell.X0..cell.X1
    else [outerRight && glyph straddles right edge]
      charsInCellEdges-->>charsInCellEdges: in = true
    else [outerLeft && glyph straddles left edge]
      charsInCellEdges-->>charsInCellEdges: in = true
    end
    charsInCellEdges-->>charsInCellEdges: append glyph if in
  end
  charsInCellEdges-->>assembleTableText: []Char cellChars
  deactivate charsInCellEdges
  assembleTableText->>assembleTableText: emit cell text from cellChars
Loading

File-Level Changes

Change Details Files
Adjust table row assembly so outermost cells treat their left/right edges as table boundaries that may retain straddling glyphs.
  • Compute first and last non-empty column indices per row before assembling cell text.
  • Pass flags to the glyph selection helper indicating whether a cell is at the outer left or right edge of the table.
  • Preserve behaviour for interior cells and empty cells while enabling special handling only on outer columns.
page.go
Refactor glyph selection logic into a new helper that can optionally include glyphs overlapping a table’s outer horizontal edges while keeping vertical containment strict.
  • Make charsInCell a thin wrapper delegating to the new charsInCellEdges helper with both outer-edge flags disabled.
  • Implement charsInCellEdges to select glyphs by midpoint for interior cells, plus glyphs whose bounding box overlaps the cell’s left or right edge when marked as outer.
  • Ensure glyphs fully beyond the outer edge are excluded and that vertical bounds remain unchanged.
page.go
Add regression tests that pin the real-world failing coordinates, verify that straddling glyphs are kept only when appropriate, and ensure existing callers continue to see the original behaviour.
  • Add a test reproducing the 3M 2018 10-K balance sheet geometry to assert the closing parenthesis survives when the cell’s right edge is marked as outer.
  • Add a test ensuring outer-edge widening does not absorb distant glyphs and does not cross row (vertical) boundaries.
  • Add a test confirming charsInCell retains its previous midpoint-only behaviour for existing callers.
cell_edges_test.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@hallelx2, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 57 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8d563823-0e1e-4ffb-a93a-5f2ecad19d2a

📥 Commits

Reviewing files that changed from the base of the PR and between 325e628 and 58fce9a.

📒 Files selected for processing (2)
  • cell_edges_test.go
  • page.go

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@sourcery-ai sourcery-ai 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.

Hey - I've left some high level feedback:

  • The charsInCellEdges signature with two booleans (outerLeft, outerRight) makes the call sites a bit opaque; consider using a small enum or separate helpers for left/right outer edges to make intent clearer and avoid misconfiguration.
  • The new comments embed a lot of filing-specific narrative; consider trimming them down to the minimal explanation of the edge-case geometry and linking to the detailed ticket instead to keep the codebase focused and easier to skim.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `charsInCellEdges` signature with two booleans (`outerLeft`, `outerRight`) makes the call sites a bit opaque; consider using a small enum or separate helpers for left/right outer edges to make intent clearer and avoid misconfiguration.
- The new comments embed a lot of filing-specific narrative; consider trimming them down to the minimal explanation of the edge-case geometry and linking to the detailed ticket instead to keep the codebase focused and easier to skim.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@hallelx2
hallelx2 merged commit c69feb3 into main Aug 2, 2026
5 checks passed
@hallelx2
hallelx2 deleted the halleluyaholudele/hal-520-table-outer-edge-clipping branch August 2, 2026 09:12
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.

1 participant