Repository navigation
No test checks that a display-only label change leaves the structure tree unchanged #557
Description
What needs doing
Add a test that changes only how headings are displayed and fails if the structure DeltaTrack reports changes. Today, nothing checks that display and structure are independent. A change to a label's wording, case or punctuation can retype or regroup the structure tree, and the suite stays green. Work in #778 (the epic to show bills the way GPO prints them) is about to make exactly those changes.
Some background, for anyone new to this:
- The structure tree.
src/deltatrack/structure_tree.pyrebuilds each version's flat node list into a nested tree. Each node has alevel(division,title,majorfor a department,agency,accountfor the named pot of money,section, …). The tree drives the Sections list in the Full bill view and thetreefield of the canonical diff JSON (the published output contract). - Display label vs match path.
display_pathis the breadcrumb a reader sees.match_pathis the normalised key the diff pairs nodes on. Only the display form is meant to change with styling. - Why it's fragile. The tree nests nodes by their display breadcrumb and types some containers by pattern-matching it. Any label change therefore reaches the tree. A change to how a division heading is displayed silently rewires which sections the diff compares #468 (closed 2026-08-04: changing how a division was displayed rewired which sections the diff compared) is the one time this has shipped. Its guard,
TestDivisionMatchKeyIndependence(tests/test_diff_bill.py:1581), checks diff pairing only, not the tree.
The gate would be red today (checked 2026-10-05 on develop, d3935ee)
A prototype swapped build_division_label to GPO's DIVISION A—Header and compared trees by shape and level, ignoring label text:
- XML,
118-hr-4366enrolled. 13 node positions differ. The seven divisions becomeheading, and Front Matter losesSec. 1–Sec. 6to the top level. Across the committed corpus, all 201divisionnodes in 15 files lose their level. - PDF,
118-hr-4366and118-hr-2882engrossed amendments. The same happens.
That's #471 (a division's level is read from its display label). So #557 and #471 should ship in one PR: the gate shows red, the fix turns it green.
A second transform, upper-casing every breadcrumb segment, changes the tree in 16 XML files. In 6 of them two nodes merge, because the same account appears with different source casing under one agency. For example, 113-hr-3547 (engrossed amendment) has Salaries and expenses and salaries and expenses under Office of personnel management. So a case-only label change can merge nodes, because the address is the label (#552, the parent epic, point on addresses).
Why it matters
- Order of work. This gate and A division's level is read back from its display label, so showing divisions GPO-style would strip it in both pipelines #471 come before Division labels read "Division A: Header" instead of GPO's "DIVISION A—HEADER", in both pipelines #66 (show division labels GPO-style). This gate and An agency or title that holds a headerless provision takes that provision's level, so it is reported as an account #470 (agencies typed as accounts) come before XML reports show headings in the source file's inconsistent casing, not GPO's per-level casing #53 (GPO per-level heading casing).
- The failure is silent. A retyped or regrouped tree still renders and still passes the corpus tree gate, which accepts
headingas a valid level.
Done when
- A test builds the tree for real omnibus fixtures (XML and PDF), changes only the division display form, and asserts the trees match on everything except label text. That means the same nodes, in the same nesting, with the same levels and the same source node behind each.
- It was shown red on
develop, and the PR description says so. - It's green after A division's level is read back from its display label, so showing divisions GPO-style would strip it in both pipelines #471's fix, in the same PR.
- Both label seams can be switched by the test, ideally through one shared function. Today they're
build_division_label(bill_tree.py:562) for XML and an inline f-string in_assign_divisions(parsers/pdf_anchors.py:857) for PDF. - Fast and slow suites pass.
Scope
- In scope: the gate, the shared division-label seam, and A division's level is read back from its display label, so showing divisions GPO-style would strip it in both pipelines #471's fix.
- Out of scope:
- The GPO label itself, which is Division labels read "Division A: Header" instead of GPO's "DIVISION A—HEADER", in both pipelines #66.
- A casing transform. Add it once XML reports show headings in the source file's inconsistent casing, not GPO's per-level casing #53 settles how casing is applied. If XML reports show headings in the source file's inconsistent casing, not GPO's per-level casing #53 styles by CSS as planned, labels don't change and the gate has nothing to catch. If it rewrites strings, the merges above have to be resolved first.
- Fixing An agency or title that holds a headerless provision takes that provision's level, so it is reported as an account #470, which is a wrong level that doesn't depend on display.
Where to start
- Copy the pattern of
TestDivisionMatchKeyIndependence. It monkeypatchesbuild_division_labeland proves the label really changed before asserting. - Fixtures:
tests/corpus/118-hr-4366/has XML and PDF for each version; the engrossed amendment (5_…) has divisions in both. For PDF pages, usetests.pdf_corpus.cached_pages. - Compare trees with
build_xml_tree/build_pdf_treefromstructure_tree.py. Walk them as (depth, level, child count, source id) tuples. - Read
tests/division_labels.py. It explains why test baselines still parse labels and how it guards against a silent zero. - Related PR: Show every heading the XML tags in its breadcrumb, and measure the ledger on each kind of bill that appropriates #739 (a draft PR keeping every XML heading in the breadcrumb) lists this issue as related but doesn't add the gate.
Unverified
- Whether title labels (
TITLE I—…, read by_TITLE_RE) need a variant in the gate._TITLE_REis case-insensitive and today's label already uses GPO's form. - The PDF prototype rewrote the anchors'
divisionfield instead of patchingpdf_anchors.py.
History
Filed 2026-08-06 alongside #552. It originally offered either the #66 form or a synthetic transform as the alternate label. The measurements above favour starting with the #66 form.
Activity
docs(scratch): record final review round 4; findings converged
[-]A display-only change to a heading can alter the structure tree, and no test would catch it[/-][+]No test checks that a display-only label change leaves the structure tree unchanged[/+]