Skip to content

A table keeps its headings, its sections and its caption - #744

Merged
WaylandYang merged 6 commits into
devfrom
feat/a-table-keeps-its-headings
Sep 17, 2026
Merged

WaylandYang merged 6 commits into
devfrom
feat/a-table-keeps-its-headings

Conversation

@WaylandYang

Copy link
Copy Markdown
Contributor

On the branch of #743, after it. Under #743 the NVDA earnings release got worse (misworded 10% → 19%, reads-alone 15% → 22%), and 38 of its 56 misworded statements were table rows of the shape "Basic —three months ended→ $1.08". The cause was not the contract: between the HTML converter and the chunker the tables lost their column headings, so every piece carried a fake header (| NVIDIA CORPORATION | | |) and none of the real ones. This change moves table handling to the reader, where the structure still exists.

What lands

A table is rendered from the DOM (utopia_ingest::table, before the Markdown converter sees it), by structure alone: hidden cells are skipped, spanning cells are laid on a grid, columns empty in every row go, a currency symbol or bracket in its own cell rejoins the number beside it (a dash that means "nil" stays where it is). Rows are classified by what they hold: a lone cell before any header or data is a caption line, a row of words with no label column is a header row, a lone label after the headers is a section, a label followed by figures is data. Multi-row headers stack into one heading per column ("Three Months Ended July 26, 2026"); sections fold into the labels of the rows under them ("Current assets › Accounts receivable, net", "Net income per share › Basic"), with indentation read from leading empty cells or the label's padding, and a section ends at a row of its own depth only once it has had a deeper child; a table split by a page break into a headerless second <table> with the same column count takes the headings of the one before; caption lines become a paragraph before the table. Nested tables and the tables of other parsers keep the converter's path.

The chunker keeps the caption run. Up to five consecutive short paragraphs before a table are its caption ("NVIDIA CORPORATION", "CONDENSED CONSOLIDATED STATEMENTS OF INCOME", "(In millions)", "(Unaudited)"), so every piece of a split table names the report, the unit and the periods.

The contract says what a period column is. In a table a cell is a statement about its row's thing whose phrase is the column heading; when the heading names one time it is when instead, the statement is about the thing the caption names and the phrase is the row label with its path; a heading that names a change between periods (quarter over quarter, year over year) stays the phrase; a unit the caption gives is a qualifier.

A table row takes its period from the header in the same chunk. The time-in-quote check kept 40 periods out of the release because the heading is not in the row's own quote; a quote that is a table row now finds its time words anywhere in the chunk, which by construction holds the table's header.

Two counted shapes. phrase_is_value and phrase_is_subject record a statement whose phrase repeats its value or its subject, the two shapes a model writes for a row without a heading or a caption. The statement is kept: the subject and the value are there, so nothing is lost, and the count makes the shape measurable.

Verified

Unit tests on the renderer (filing table with two header rows, spacer columns, dollar cells, sections and totals; a lone label as caption; a first row of words as header; hidden cells; dashes; a continued table; nested tables left alone), on the caption run in the chunker, and on time location for table rows; 84 ingest tests, clippy clean.

On the real release: 52 chunks become 32, none a table without its header; the balance sheet, income statement and cash-flow statement render with their real headings and section paths; the 8-K vote tables are unchanged. First statements from the highlights table under the final wording:

NVIDIA —GAAP › Revenue→ $96,221 {unit: $ in millions, except earnings per share} [Q2 FY27]
NVIDIA —GAAP › Revenue→ $81,615 {…} [Q1 FY27]
NVIDIA —GAAP › Net income→ $26,422 {…} [Q2 FY26]
revenue —Q/Q→ 18%
revenue —Y/Y→ 106%

The four NVDA documents are being re-extracted and judged under this build; the numbers against #743 follow in a comment.

Not in this change

Tables from the DOCX, PDF, spreadsheet and CSV parsers (they still promote the first row); the NVDA cross-domain paragraph units, which were cut from the old chunks and have no headings, and their reference facts, which carry no period; a bench check that a cell statement under a period column carries when; the "Total" row of a section without deeper children, which lands in the last sub-section.

🤖 Generated with Claude Code

@WaylandYang

Copy link
Copy Markdown
Contributor Author

The four NVDA documents, re-extracted under this branch and judged by the same second model as #743 (numbers in brackets are #743 on the same documents):

statements misworded not stated reads alone = false
earnings release (52 chunks → 32) 579 (295) 1% (19%) 0.3% (0%) 12% (22%)
8-K 2026-06-24, vote tables 83 (86) 4% (2%) 0% (2.3%) 2% (21%)
8-K 2026-08-17, exhibit 112 (93) 6% (3%) 0.9% (2.2%) 4% (15%)
8-K 2026-09-02 56 (102) 7% (12%) 1.8% (0%) 14% (10%)
all four 830 (576) 2.4% (13%) 0.5% (0.7%) 10% (18%)

Split by where the quote comes from: the 432 statements taken from table rows are misworded 1%, not stated 0%, reads alone = false 16%; the 398 from prose are misworded 4%, not stated 1%, reads alone = false 5%. Under #743 the 192 table-row statements were misworded 21% and unreadable 31%.

What the judge still calls unreadable on table rows is one shape, the cash-flow lines "NVIDIA —Changes in operating assets and liabilities, net of acquisitions › Accounts receivable→ (24,590) [Six Months Ended July 26, 2026]", where the section path carries the meaning (a change, not a balance) and the judge reads it as a fragment. The 8-K of 2026-09-02 counts fewer statements because its cover-page boilerplate is shared with the other two 8-Ks and dedup attributes those statements to the first document that stated them; per chunk it yields 102, as before. Drops: time_not_in_quote 2 (40 under #743), no phrase_is_value or phrase_is_subject.

🤖 Generated with Claude Code

@WaylandYang
WaylandYang force-pushed the feat/a-statement-reads-on-its-own branch from df861f5 to 977b619 Compare September 17, 2026 14:10
Base automatically changed from feat/a-statement-reads-on-its-own to dev September 17, 2026 14:18
WaylandYang and others added 6 commits September 17, 2026 22:18
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
@WaylandYang
WaylandYang force-pushed the feat/a-table-keeps-its-headings branch from 31d4112 to 4f5953a Compare September 17, 2026 14:18
@WaylandYang
WaylandYang merged commit fc45618 into dev Sep 17, 2026
4 checks passed
@WaylandYang
WaylandYang deleted the feat/a-table-keeps-its-headings branch September 17, 2026 14:25
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