You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
DeltaTrack works out part of a bill's structure by reading back the heading text it displays. Whether a heading is a division, a title or an account, and which node a passage belongs to, can depend on how that heading happens to be printed. So a change that should only affect how a heading looks can change the structure the report shows, and no test notices. This epic groups the issues that remove that coupling.
Some background, for anyone new to this:
The structure tree and its levels. Both pipelines (XML, read from GPO's tagged file, and PDF, read from the printed page) produce a flat list of nodes. src/deltatrack/structure_tree.py rebuilds them into a nested tree. Each tree node has a level: division (the "Division A" of an omnibus), title (TITLE I), major (a department), agency, account (the named pot of money), section, and a few others. docs/bill-structure.md lists them. The tree drives the Sections list in the Full bill view and the tree field of the canonical diff JSON (the published output contract).
Display label vs match path. Each node carries two heading chains. display_path is the breadcrumb a reader sees, e.g. Division A: Military Construction… › TITLE I › DEPARTMENT OF DEFENSE. match_path is a normalised twin the diff uses to pair a provision in one version with the same provision in the other. The display form is meant to be free to change. The match path is not.
#468 (closed 2026-08-04: changing how a division was displayed rewired which sections the diff compared) was the first case. It was fixed by giving each node a division_key built from the source alongside the label. That fix is the pattern for the rest.
What's still coupled (checked 2026-10-05 on develop, d3935ee)
Department and agency containers have no level from the source. The XML tree builds them from breadcrumb prefixes and calls them heading. When a headerless provision lands on one, the container takes that provision's level (structure_tree.py:167). Across the committed corpus, 418 containers (agencies, departments and titles) come out typed account this way (An agency or title that holds a headerless provision takes that provision's level, so it is reported as an account #470).
The level is published. It's in the canonical JSON and in the Sections list as data-level. Show each version's dollar amounts, typed, in three report views #736 (a draft PR adding financial views) files each dollar amount into division, department, agency and account columns by this level.
Fix in #739, a draft PR that keeps every XML heading in the breadcrumb. It's stacked on #736 and on #734 (the open PDF-headings PR). Its 2026-09-30 review asks that a heading not become the parent of sections it doesn't introduce (SEC. 421 of 118-hr-4366 v2 lands under SPENDING REDUCTION ACCOUNT)
Every sub-issue is closed or explicitly deferred with a reason.
The tree's division level comes from the source in both pipelines, not from the label text.
A render-invariance gate runs in CI and was shown red before it went green.
No container's level is overwritten by a provision it adopts.
Decide (ADR or a comment here) how containers get a level when the source can't type them. The PDF pipeline feeds the same tree builder without XML tags, so the contract has to cover it.
Scope
In scope: how the tree gets levels and addresses, and the gate that keeps display separate from structure.
Read:docs/bill-structure.md, the section "Emitted level vocabulary"; then src/deltatrack/structure_tree.py (_interior_level, _build_tree, _group_front_matter). It's under 300 lines.
The pattern to copy:Division in bill_tree.py (label and key built side by side from the source), plus its guard TestDivisionMatchKeyIndependence in tests/test_diff_bill.py.
Whether any code outside structure_tree.py still derives structure from display text. diff_bill.py and bill_tree.py were checked in August; the rest wasn't audited.
Filed 2026-08-06 after a review found #470, #471, #518 and #521 to be one mechanism. The tree was first built (9a35800, 2026-06-29) to make a diff navigable, and nesting by breadcrumb was the quickest way to get a hierarchy on screen. Levels were later read as structure without the model being redefined for that.
What's wrong
DeltaTrack works out part of a bill's structure by reading back the heading text it displays. Whether a heading is a division, a title or an account, and which node a passage belongs to, can depend on how that heading happens to be printed. So a change that should only affect how a heading looks can change the structure the report shows, and no test notices. This epic groups the issues that remove that coupling.
Some background, for anyone new to this:
src/deltatrack/structure_tree.pyrebuilds them into a nested tree. Each tree node has a level:division(the "Division A" of an omnibus),title(TITLE I),major(a department),agency,account(the named pot of money),section, and a few others.docs/bill-structure.mdlists them. The tree drives the Sections list in the Full bill view and thetreefield of the canonical diff JSON (the published output contract).display_pathis the breadcrumb a reader sees, e.g.Division A: Military Construction… › TITLE I › DEPARTMENT OF DEFENSE.match_pathis a normalised twin the diff uses to pair a provision in one version with the same provision in the other. The display form is meant to be free to change. The match path is not.Division A: Headerto GPO'sDIVISION A—Header. Any code that recovers structure by pattern-matching the label breaks silently when that happens.#468 (closed 2026-08-04: changing how a division was displayed rewired which sections the diff compared) was the first case. It was fixed by giving each node a
division_keybuilt from the source alongside the label. That fix is the pattern for the rest.What's still coupled (checked 2026-10-05 on
develop,d3935ee)_interior_level(structure_tree.py:88) calls a node adivisiononly if its label matches^Division [A-Z]. The GPO form doesn't match. In both pipelines, every division then becomes a genericheadingand the Front Matter group breaks up (A division's level is read back from its display label, so showing divisions GPO-style would strip it in both pipelines #471).heading. When a headerless provision lands on one, the container takes that provision's level (structure_tree.py:167). Across the committed corpus, 418 containers (agencies, departments and titles) come out typedaccountthis way (An agency or title that holds a headerless provision takes that provision's level, so it is reported as an account #470).Why it matters
level, so agencies typedaccountwould get the account's lowercase style.data-level. Show each version's dollar amounts, typed, in three report views #736 (a draft PR adding financial views) files each dollar amount into division, department, agency and account columns by this level.Sub-issues (all open, checked 2026-10-05)
SPENDING REDUCTION ACCOUNT)Order of work, and what it unblocks in #778
Done when
divisionlevel comes from the source in both pipelines, not from the label text.Scope
bill_tree.py.Where to start
docs/bill-structure.md, the section "Emittedlevelvocabulary"; thensrc/deltatrack/structure_tree.py(_interior_level,_build_tree,_group_front_matter). It's under 300 lines.Divisioninbill_tree.py(label andkeybuilt side by side from the source), plus its guardTestDivisionMatchKeyIndependenceintests/test_diff_bill.py.Unverified
structure_tree.pystill derives structure from display text.diff_bill.pyandbill_tree.pywere checked in August; the rest wasn't audited.History
Filed 2026-08-06 after a review found #470, #471, #518 and #521 to be one mechanism. The tree was first built (
9a35800, 2026-06-29) to make a diff navigable, and nesting by breadcrumb was the quickest way to get a hierarchy on screen. Levels were later read as structure without the model being redefined for that.