OBR policy effects → DB ingest: 266 claims, four macro-effect metrics - #81
OBR policy effects → DB ingest: 266 claims, four macro-effect metrics#81vahid-ahmadi wants to merge 5 commits into
Conversation
…metrics The harvest (#75) landed 266 rows in data/externals/obr-policy-effects.json that nothing consumed — invisible to the DB and the app. This wires them in as their own source (obr_policy_effects), registered in build_db's chain after uk_deductions. - Four new Metric members. gdp_level_effect (package effect on the real-GDP path) and supply_side_impact (one measure's effect on POTENTIAL output) are deliberately distinct, as is decisions_effect_on_borrowing (PSNB) from revenue_change and cash_requirement_change (PSNCR). - Reform worlds are policy_ref slugs — the fiscal event's package, or the measure for the supply-side family — against the null current_law baseline, per baselines.py's documented convention (announcement vintage is a condition, not a baseline world). No new baseline registered. - conditions["decomposition"], keyed off (fiscal_event, sheet): the October 2024 workbook prints the AB2024 package twice (2.A by expenditure component, 2.B by measure/channel), so both publish a 'total' and a 'demand_multipliers'. Without the axis the two totals collide on one claim_id — finish() caught it. The sheet id alone is not the identity: C2.A is by-channel in the Nov 2023 and Mar 2024 workbooks. - Briefing paper No.10 states its horizon in words ("the fifth year of our forecast"), never a digit. The note rides verbatim in conditions, horizon names it symbolically, and the year maps in one place (_BP10_HORIZON_FY) for a reviewer to re-key. - Table B.1's aggregate_level/parent guard survives into conditions, so summing borrowing effects by FY cannot double-count. All 266 are held_out (relationships.uk_relationship, never inline): no pe-uk-data target or policyengine-uk parameter is fitted to a macro-effect path. Derived-file changes are the build's own output, committed as built: data/lanes.json gains the lane entry and its updated date, mirrored to the app copy. Suite: 277 passed / 2 skipped (24 new); build deterministic across two runs; no-drift gate clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nto uk/obr-policy-effects-ingest
Gate round 1 (reviewed as a pair with #75) — the transaction shape is right; five findings, four blockingClean and verified: adapter-to-DB accounting is exact (266 rows, 266 unique ids, 151/36/19/60, no ingest drops), the transaction correctly does delete + insert +
The skeleton is genuinely good — the fixes are concentrated in claim identity (baseline, period, units, dates), which is exactly where they're cheapest to fix before anything attaches. 🤖 Generated with Claude Code |
Reviewed as a pair with #75, which this branch now carries; the harvest side supplies the per-row provenance the fixes here consume. 1. Baselines are the rounds' own pre-measures worlds, not current_law. Every claim now carries a ReformRef.baseline descriptor — {policy: obr_pre_measures_<round>, counterfactual: <kind>} — mirrored into conditions["baseline_policy"], with the counterfactual kind split as Briefing paper No.10 chapter 2 splits it (legislated parameters for tax/welfare, the pre-existing activity baseline for DEL, the regulatory baseline for planning). March 2026 Table B.1 keys the November 2025 Budget forecast, its own stated counterfactual. All eleven (round, counterfactual) worlds are registered in baselines.py, a row whose baseline disagrees with its own round raises, and a baseline without a locator raises. 2. BP10 horizons are per scoring round. The paper re-states scorings from five earlier events, so "the fifth year of our forecast" is each measure's own round's fifth year: 2027-28 (Mar 2023) through 2029-30 (Mar 2025). All 19 were keyed to 2030-31, the November 2025 round's horizon. Period is claim identity, so those were 19 claims about years OBR never scored them for. _BP10_HORIZON resolves per event and an unregistered round raises rather than borrowing another's year. 3. Unit concepts are deliberate, and validated before mapping. Three new UnitConcept members — PERCENT_OF_REAL_GDP, PERCENTAGE_POINTS, PERCENT_OF_POTENTIAL_GDP — replace the collapse into bare PERCENT, and the staged label is now compared against the metric's expected unit and raises on drift instead of being canon-checked then discarded. Bare "percent" is no longer a registered label for this source, so the three cannot collapse back. 4. Publication provenance is per artifact. Each round's claims carry their own release date and dated URL rather than one generic obr.uk/publications/ stamp dated 2025-11-26, and the 60 March-2026 claims carry the publication date (2026-03-03) instead of the Wayback capture (2026-03-16). An unregistered artifact raises. 5. `basis` stops squatting: it is `forecast` on every row (its repo-wide meaning) and the scoring method moved to its own `scoring_method` condition, which also keys the relationship lookup. 266 claims and the exact accounting unchanged; the DB builds deterministically (two builds agree on content_hash), suite 299 passed, ruff format clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
1. Units are no longer conflated. GDP-level deviations, CPI effects and supply-side impacts were all emitting bare `percent`; they are three different quantities and now carry three unit concepts — `percent_of_real_gdp`, `percentage_points`, `percent_of_potential_gdp` — with `gbp_nominal` unchanged on Table B.1. Same rule that split GBP_PER_WEEK from bare GBP, and the adapter test now pins the mapping per metric instead of entrenching the conflation. 2. 266 is source-reconciled. Table B.1 prints 66 numeric cells across 11 lines; the parser selected 10 and the memo current-budget line fell through a silent `continue`. Every value-bearing line is now classified — emitted (TB1_ROWS) or a declared drop (TB1_DROPS, with a reason) — and an unclassified value line raises. The build reports and asserts `272 source cells = 266 claims + 6 deliberate drops`; the memo line is dropped because it scores the current budget, not PSNB. 3. Baseline provenance is per row, not one blanket sentence. Each row carries the pre-measures world of its own round (`obr_pre_measures_<event>`), the counterfactual KIND — Briefing paper No.10 scores tax/welfare measures against a legislated-parameter counterfactual and DEL/regulatory measures against the pre-existing activity baseline — and a locator. March 2026 Table B.1 keys `obr_november_2025_budget_forecast`, its own stated counterfactual, and the March 2025 WCA reversal's locator records that it scores against the WCA-adjusted world AS2023 created. 4. Identity vocabulary is closed, and `basis` stops squatting. Chart series labels, BP10 measures, types and channels are explicit registries; an unregistered label raises instead of `_slug()` minting one. `basis` is now `forecast` (its standard meaning everywhere in this repo) and the scoring method moved to its own `scoring_method` axis. Also: publication provenance is per artifact, so each round's claims carry their own release date and URL rather than one generic obr.uk/publications/ stamp — the input to #81's date fix. 266 claims unchanged; suite 261 passed, ruff format clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
|
All five addressed in
266 claims and the exact accounting unchanged; two builds agree on |
DTrim99
left a comment
There was a problem hiding this comment.
Reviewed on tip 50f1dea (includes #75) — pytest tests/test_obr_policy_effects_ingest.py = 38 passed. Clean ingest, same discipline as the rest of the lane.
Verified:
- Accounting — no drops here (every staged row ingests), so it degenerates to staged=ingested=266, asserted via per-metric
_EXPECTED(raises on drift) + thefinishcollision gate; 266 reconciles as 151+36+19+60. - The four macro metrics (
gdp_level_effect,cpi_inflation_effect,supply_side_impact,decisions_effect_on_borrowing) are genuine and correctly kept unconfusable — real-GDP path vs potential-output supply-side scoring, and PSNB vsrevenue_change/PSNCR. CPI carried aspercentage_points(a rate effect, not a level) is the right call. None should have been a drop. - Fail-loud on unknown field / every closed axis / unit-label drift / unregistered artifact / (event,sheet) / baseline — all tested.
- held_out — all 266 → HELD_OUT with a "scored, never consumed" basis (macro paths are what Macro members are scored against, not calibration targets); the
decompositionDISTINCT edge keyed on (event,sheet) correctly resolves the twice-printed AB2024 collision. - Values pass through verbatim (no re-derivation/re-signing), baselines.py registers the 11 (round, counterfactual) pre-measures worlds + the Mar-2026 post-measures world inside the ingest txn (asserted complete, no
current_lawdefault), and the lanes.json mirror is semantically identical.
Two tiny notes: the PR body still describes an outdated single-constant _BP10_HORIZON_FY, but the code correctly uses the per-event _BP10_HORIZON (body stale, code right); and the only lanes.json byte-diff is an unrelated em-dash escaping in a _schema note. Approving — merge after #75.
Stacked on #75 (base
uk/obr-policy-effects-harvest); retargets tomainwhen #75 merges. Both branches carry a merge of current main so the post-#74 build machinery is present — the diff here is this lane's nine files only.#75 landed 266 rows in
data/externals/obr-policy-effects.jsonthat nothing consumed: nobuild_dbstep read the file, so the claims never reached the DB or the app. This wires them in.What's here
scorecard_db/ingest_obr_policy_effects.py— new sourceobr_policy_effects, registered inbuild_db's chain afteruk_deductions(external claims before the campaign attaches). Same contract asingest_uk_externals:require_fieldson every row, closed identity registries, values passed through verbatim,calibration_relationshipfromrelationships.uk_relationshipand never inline. One transaction replaces the source wholesale with the baseline-registration gate inside it.Metricmembers.gdp_level_effect(a package's effect on the real-GDP path) andsupply_side_impact(one measure's effect on potential output) are deliberately distinct rather than one "GDP effect" metric — unifying them would merge a demand-inclusive path with a supply-side scoring.decisions_effect_on_borrowing(PSNB) is likewise kept apart fromrevenue_changeandcash_requirement_change(PSNCR), the same unconfusability rule UC deductions: FRR family re-harvested from primary sources + DB ingest #52 applied.policy_refreform naming what was scored —obr_<event>_packagefor the chart families,obr_<event>_<measure>for the supply-side family, so the same measure re-scored at a later event is a different world. Baseline stays the nullcurrent_law: OBR scores an announcement against the law in force at its own scoring date, which is exactly the conventionbaselines.pydocuments (announcement vintage is a condition, not a baseline world). No new baseline registered.scorecard_db/uk_aliases.py— the source's own closed vocabulary (29 programs, 26 subgroups, geography, units). Registered as its own namespace with nothing aliased intoobr's welfare programs: this source'semployer_nicsis a measure whose supply-side effect is scored, not a spending line.tests/test_obr_policy_effects_ingest.py— 24 tests; suite 277 passed / 2 skipped.Two identity decisions the data forced
1.
conditions["decomposition"]— caught byfinish(), not by inspection. The October 2024 workbook prints the AB2024 package twice: chart 2.A by expenditure component, 2.B by measure/channel. Both publish atotaland ademand_multipliers, so the two totals collided on oneclaim_id(they agree to ~1e-14, being the same series rendered twice). The decomposition is therefore identity-bearing, not provenance. It keys off(fiscal_event, sheet)because the sheet id alone is not the identity —C2.Ais by-channel in the Nov 2023 and Mar 2024 workbooks and by-expenditure-component in Oct 2024. Unregistered pairs raise.2. The supply-side horizon — the one interpretive call in this PR. Briefing paper No.10's Table 2.1 states its year in words, never as a digit: "Supply-side impact is the impact on potential output in the fifth year of our forecast."
periodis an int, so something had to be chosen. The note rides verbatim inconditions["horizon_note"],conditions["horizon"]names it symbolically, and the year maps in exactly one place —_BP10_HORIZON_FY = "2030-31"(the fifth year of the November 2025 forecast the paper accompanies). Flagging it explicitly: if you read the horizon differently, it is a one-constant change, and the test pins the mapping rather than the reasoning.Verification
uk_policy_effectsstep:{"claims": 266, "by_metric": {"gdp_level_effect": 151, "cpi_inflation_effect": 36, "supply_side_impact": 19, "decisions_effect_on_borrowing": 60}}— exact accounting, so a drifted harvest regeneration fails here rather than silently resizing the catalog.8fb2adf0…), and both no-drift gates clean — replicated exactly asci.ymlruns them (build ×2 →git diff --exit-code→ pytest →git diff --exit-code).aggregate_level/parentguard survives into conditions and is asserted at DB level (6 total / 24 subtotal / 30 component, zero orphaned non-total rows), so summing borrowing effects by FY cannot double-count.held_out: no pe-uk-data target or policyengine-uk parameter is fitted to a macro-effect path — they are what the Macro members get scored against.Derived-file changes are the build's own output, committed as built:
data/lanes.jsongains the lane entry, mirrored to the app copy. The feed's top-level stamp stays on the UK family's constant (FEED_UPDATED) because the build ends on this step while the suite ends on whichever UK ingest test runs last — all three writers have to agree or the no-drift gate fails; the comment says so.PE counterparts are step 3 of #55 and are not computed here.
Builds #55 (step 2: DB ingest).
🤖 Generated with Claude Code