feat(96): hierarchy-aware table exactness metric + the calibration that kills the proposed accept rule - #104
Merged
Merged
Conversation
… kills the proposed accept rule GH-96 Unit A. Two deliverables, one of them a negative result that changes the design of Unit B before it is written. ## The metric `BenchmarkScorer.score_table_cells` compares numeric cells positionally and, on a shape mismatch, degrades to multiset recall. The GH-96 failure is a permutation — every digit present, right column, wrong row — so recall is perfect. On OBR EFO November 2022 page 13 it returns 100.0% both for output that is 38.4% correct by label and for a perfect transcription of the same page. It cannot separate them, so no past benchmark number is informative about this failure mode. `benchmark/table_exactness.py` keys cells to rows identified by label, matched in document order so a label reused under two parents ("Other measures" appears twice in that table) cannot be credited twice against the same source row — the collision that made the bake-off scratch scorer understate a perfect page by 6 cells. Page 13 escalated now measures exactly 100.0%, confirming its 95.2% was entirely artifact. Ground truth is model-free from the native text layer: scoped to located table regions (without which the parser reads prose containing numbers as table rows), rows grouped by vertical overlap rather than a y tolerance (parameter-free), and split at the last non-numeric word so a numeral inside a label ("Growth Plan after 17 October reversals") is not read as data. Hierarchy from x-indentation. `ceiling_note` marks a degenerate parse so an engine tie is never misread as a shared failure — but deliberately NOT when the prediction is empty, since an engine that emitted no table has genuinely failed. Conflating those would have hidden the three largest wins in the calibration below. ## The calibration — the gate on Unit B Cross-tabulating exactness against `defect_severity` over the 16 OBR table pages: severity improves on ZERO pages, and 9 of 9 genuine exactness gains would be rejected by the proposed accept rule. Two independent causes. Only `lane_gap` ever moves, and it moves the wrong way — on pages 46/48/55 socr emitted no table, so lane_gap starts at 0 and RISES once a real multi-column table is produced; the metric is anti-correlated with quality. And `label_binding_failure` is 0 on page 13, the canonical hierarchical-shift page whose entire defect is label binding, so it fails as a trigger as well. Unit B must therefore not use severity improvement as its accept test, nor label_binding_failure as its trigger. Recorded in docs/log/2026-07-30_gh96-severity-calibration.md. Aggregate with the better metric: socr 44.2%, escalated 85.0% over 2361 cells (scratch scorer said 38.6% / 74.0% over 2254 — both rise as its artifacts clear, the gap is essentially unchanged). Tests: 19 new, fixtures generated with fitz at test time so no third-party document content enters the repo. Includes a test pinning the justification — that the positional scorer scores the permutation 1.0 while this one scores it under 50%. Full suite 1285 passed, 1 xfailed. ruff clean. Refs #96
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
GH-96 Unit A of the agreed split (metric + fixture first, escalation lane second).
Two deliverables — and the second is a negative result that changes Unit B's design
before any of it is written.
Non-goals: no escalation lane, no engine calls, no config or CLI changes.
1. The existing metric cannot see this failure mode
BenchmarkScorer.score_table_cellscompares numeric cells positionally and, on ashape mismatch, degrades to multiset recall. The GH-96 failure is a
permutation — every digit present, right column, wrong row — so recall is perfect.
On OBR EFO November 2022 page 13:
score_table_cellsIt gives the identical 100.0% to a scrambled table and a perfect one. No past
benchmark number is informative about this failure mode.
benchmark/table_exactness.pykeys cells to rows identified by label, matched indocument order, so a label reused under two parents (
Other measuresappears twicein that table) cannot be credited twice against the same source row. That collision
is why the bake-off reported the escalated page 13 at 95.2%; it is exactly 100.0%.
Ground truth is model-free from the native text layer. Three things it must get
right, each learned from real data rather than assumed:
numbers ("rising to £61.7 billion (2.1 per cent of GDP)") as table rows.
label and its numbers as separate text objects, so block/line splits one visual
row in two. Parameter-free: words on a row have overlapping y-intervals.
reversals" has a numeral inside the label. An x-clustered column boundary is
chicken-and-egg (that stray 17 forms its own cluster and drags the boundary left);
this rule is parameter-free and correct for both cases.
ceiling_notemarks a degenerate parse so an engine tie is never misread as ashared failure — but deliberately not when the prediction is empty, since an
engine that emitted no table has genuinely failed. Conflating those hid the three
largest wins in the calibration below on my first pass.
2. The calibration — and it kills the proposed accept rule
The plan gated Unit B on one question: does
defect_severityimprove on the pageswhere exactness improves?
It improves on zero pages. 9 of 9 genuine gains would be rejected.
Two independent causes, both visible in the report:
lane_gapever moves, and it moves the wrong way. The other fourseverity components are
0on every page here. On pages 46/48/55 socr emittedno table at all, so
lane_gapstarts at 0 and rises to 8/4/5 once a realmulti-column table is produced. The metric is anti-correlated with quality:
emitting nothing scores better than emitting a correct table.
label_binding_failureis 0 on page 13 — the canonical hierarchical-shiftpage, whose entire defect is label binding. So it fails as the proposed
trigger as well as the accept test.
This also confirms the design-review finding that severity carries no safety
weight: a well-formed fabrication improves exactly these components.
Consequences for Unit B, recorded in the log note:
label_binding_failureas the trigger;plus a well-formedness check drawn from somewhere else;
cross-tabulated the same way before being trusted.
Aggregate, restated
The bake-off scratch scorer said 38.6% vs 74.0% over 2254 cells. Both rise as its
duplicate-label collision and parser ceiling clear; the gap is essentially
unchanged.
Test plan — observed results
pytest tests/test_gh96_table_exactness.py -q→ 19 passedpytest tests/ -q→ 1285 passed, 1 xfailedruff format --check+ruff checkon both Python files → clean; the log notealso passes
ruff@latest format --check(markdown code blocks, per fix(ci): unblock the format gate — ruff 0.16 formats Markdown code blocks #102)fitzat test time — no third-party document contententers the repo. Hierarchical / spanning-header tables: nested rows scramble while headline totals look right #96 accepts "OBR-style … or redacted minimal".
Includes
test_the_positional_scorer_cannot_see_what_this_one_measures, which pinsthe justification: the positional scorer scores the permutation
1.0while this onescores it under 50%.
Reviewer notes
each covered by a dedicated test.
Refs #96