UK externals → DB ingest path (16,924 claims, five sources) - #48
Conversation
Three review defects fixed1. Two different reforms shared one reform world. HMRC prints "Change standard rate by 1 percentage point" under both the VAT and the Insurance Premium Tax sections, and the policy slug was built from the description alone — so six claims shared one 2. Calibration decisions bypassed 3. A derivative of a consumed target was labelled held-out. Entitled-non-recipient counts and unclaimed amounts were Plus a doctrine gap this PR would have opened. Re-verified end to endAgainst the fixed adapter outputs from #43–#47: 16,924 claims (dwp_takeup 1,638 / dwp_hbai 13,056 — +168 from the rows #44 recovered / uk_hmrc 1,213 / obr 259 / ukmod 758), zero Note for merge order: this now depends on #44's and #47's fixes for its claim counts, and #47's corrected values (32 of the wrong Table 4.7 cells previously reached the DB through this path). Re-run the ingest after those merge. Still open from the review, deliberately not done here: |
|
Position on the feed-architecture question this PR is half of (now tracked as #57): I propose #32 lands first — it is the superset (eight sources vs five, Ledger routing, and the baselines registry #13 that the mode-2 lanes #54/#56 need) — and this PR then reduces to a rebase of its three review-hardening deltas that #32 does not carry:
I'll do that rebase as a follow-up PR once #32 merges, and this one can close in its favour. If maintainers prefer the opposite order, those three deltas are the part that must survive either way. Either resolution un-HOLDs #50 (its reviewer parked it on exactly this adjudication). |
Review — UK externals → DB ingest pathStrong foundation PR. I ran the ingest + the full test suite against the real Should fix
Minor
Heads-up on merge order: #52 depends on this PR's Reviewed with Claude Code assistance. |
|
All four should-fixes addressed in 0a8bc91 (plus a merge of main so the #43–#47 adapters live under this branch and the integration tests run against regenerated outputs):
On the minor: HBAI subgroup counts keep |
Re-review — main findings addressed, two loose ends ✅
|
|
Holding a full approval only on the two loose ends from my re-review: reconcile the PR body to 16,924 (code and title already agree), and coordinate the |
|
Both squared away: the PR body was reconciled to 16,924 earlier today (the comment likely crossed with the edit — it reads 16,924 now, matching title and code). On the GBP line: proposed sequence is this PR merges first (it is the base of the #52 stack), and #65 rebases immediately after, dropping its duplicate |
relationships.py rebuilt from the literal consumption surfaces read at
the certified pins (pe-uk-data@dd68c73 targets/sources/{obr,dwp,
hmrc_spi}.py; policyengine-uk takeup.yaml parameters): OBR consumed =
the 12 named Table 4.9 forecast lines (outturn cells must route to
Ledger and RAISE here); PC take-up = seed_source (parameter cites the
FYE-2020 edition); HB take-up = held_out (engine's stated
by-definition-1.0 design; uk#1813 comparator); HMRC liabilities =
seed_source (shared SPI 3.6/3.7 base), reckoner = held_out; unknown
combinations raise. models.py = main's baseline machinery + pr-48's UK
vocabulary (3-way applied). Remaining: alias registry, Ledger router,
atomic ingest, exact accounting, tests, regen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Evidence-based rebuild on the #71 schema foundation. Every relationship now traces to a consumption surface read at the certified pins (2026-08-19): pe-uk-data@dd68c73 targets/sources/{obr,dwp,hmrc_spi}.py and policyengine-uk's takeup.yaml parameters. - Ledger routing (boundary rule 2026-08-02): 749 admin outturn cells — DWP recipient counts/amounts claimed (546), HMRC 2023-24 SPI-outturn liabilities (166), OBR FY2024-25 column (37) — route to data/ledger/uk_admin_outturns.jsonl with deterministic fact ids and the consuming pin named where pe-uk-data literally reads the cell. The relationship registry RAISES if an outturn ever reaches it. - Relationships keyed exactly: OBR consumed = the 12 named Table 4.9 forecast lines (adapter vocabulary, pe-uk-data labels quoted); PC take-up family = seed_source (the engine parameter cites the FYE-2020 edition); HB = held_out (engine's stated by-definition-1.0 design; uk#1813 comparator); HMRC levels = seed_source (shared SPI 3.6/3.7 base), reckoner = held_out; UKMOD = peer-held. Distribution: 78 consumed / 1,836 seed / 14,261 held. - Closed identity registry (scorecard_db/uk_aliases.py): (source, axis, value) -> canonical, raising on unknowns; HBAI/UKMOD poverty lines canonicalized to (basis, percent, median-vintage) so HBAI-relative and UKMOD-60 share a world deliberately; winter_fuel alias; explicit DISTINCT pairs (HB-pensioners vs HB; benefit_units vs families). - Units repaired: gbp value_kind (never 'usd'), GBP_PER_WEEK / GBP_PER_MONTH / INDEX_0_1 unit concepts (Gini is an index). - Atomic: stage + validate everything, then ONE transaction replaces the five sources wholesale; rollback verified by injection. - Exact accounting: 16,175 claims + 749 ledger + 1,829 drops = every adapter row (the adjudication's corrected 16,924); drift raises. Registry: 52 worlds (+hmrc_indexed_baseline_spring_2025, gate-caught). DB 54.3 MB. Suite: 213 tests (21 UK: triangles, ledger routing, rollback injection, accounting drift, pinned integration). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0a8bc91 to
2eb5576
Compare
|
Repaired against the adjudication's eight blockers and re-pushed (dual-gate flow, same as #16/#24): every relationship now traces to a consumption surface read at the certified pins — pe-uk-data@dd68c73's targets/sources modules and policyengine-uk's take-up parameters. Headlines: 749 admin outturn cells route to |
…d-to-end All ten findings: 1. Period convention: the branch's stale start-year comment contradicted the repo contract — main keys FY claims by END year (RV parser: 'period is the ending year'; live claims: CT notes 2027, CA AB2591 FY2027-28 -> 2028). Comment corrected; _fy() was already right. Engine start-year periods are result-side provenance, never claim identity. 2. HMRC: the workbook's own description sheet marks every year through 2023-24 as outturn — the full 1990-91..2023-24 span (490 facts) now routes to Ledger; claims 723, seed 498. Accounting: 15,851 claims + 1,073 ledger + 1,829 drops = 16,924, drift-gated. 3. Zero GBP-as-usd remains (reckoner + OBR value kinds fixed; verified in the DB). 4. HBAI absolute line: anchor-aware per the Notes sheet — poverty_line 'absolute_60_fixed_median' + poverty_line_anchor fye_2011 (5,632) / fye_2025 (480) / mixed_fye2011_fye2025 (416 straddling windows, explicit never averaged). 5. Reckoner rows keep their machine-readable orientation (sign_convention/direction/change_direction in conditions). 6. Ledger facts carry aggregate_level + parent (the anti-double-counting hierarchy). 7. The identity gate is end-to-end: units validate through canon() in every stager, reckoner geography/program canonicalized, Ledger identities canonicalized; obr NI aliases to Northern Ireland. 8. ingest() runs the deliberate-registration baseline gate. 9. Equivalisation conditions per the vendored methodology quotes: HBAI modified_oecd / modified_oecd_companion_ahc; UKMOD distribution, gini and poverty rows bhc + modified_oecd (the three-way triangle is well-posed by construction). 10. Lanes unified: DB and feed share the adapter lane ids at 'ingested'. Suite: 213. HBAI adapter's rounded-text precision (finding 9b) is an upstream #44 adapter defect — follow-up, not this ingest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… persistence Sol's round-2 probes found two mechanism defects, both fixed: 1. HMRC reckoner rows bypassed the closed unit registry (the revenue_effect branch returns before the canon call). Unit validation now runs at the top of the loop, before ANY branch; regression test injects an unknown-unit reckoner row. 2. register_baselines ran AFTER the claims transaction committed — it validated the DB but did not guard it (an unregistered-baseline probe raised with all 15,851 replacement claims already committed). Split into register_baselines_txn (bare executes, caller owns the transaction — nested `with conn:` would commit early, which is why the txn form may never open one) + the standalone wrapper. ingest() now runs delete + insert + registration gate + lane rows in ONE transaction: an unregistered world rolls back everything. Rollback test: poison claim -> raise -> DB byte-identical, poison world never registered. Also per the round-2 pass: - equivalisation + poverty_line_anchor promoted into STANDARD_CONDITIONS; semantic pins added to the integration test (anchor distribution 5,632/480/416, every mixed anchor is a window, equivalisation tracks housing costs on all 13,056 HBAI rows, UKMOD gini/income all BHC + modified-OECD, none vacuous) — count-only accounting cannot detect semantic condition loss. - Stale accounting comment corrected: 15,851 + 1,073 = 16,924 admitted; + 1,829 drops = 18,753 adapter rows. - Period comment narrowed: END-year is the LIVE claim convention; archived harvest NOTES staged some sources by start year, so archive-built ingests (#52) must translate at staging time. - Two pre-existing unused imports removed (ruff check; CI only gates format). Committed DB unchanged: this round changes validation and transaction mechanics, not staging semantics. Suite: 215. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sol round 3 confirmed both production blockers resolved but caught the UKMOD semantic pin being unsound: json_extract of a MISSING key is SQL NULL, and NULL <> 'x' is not true, so a bare <> counted zero mismatches even with both load-bearing conditions stripped from all 64 rows. Mutation-probed: the old guard caught 1/3 defect classes (wrong value only); the NULL-safe form catches 3/3 (stripped either/both + wrong). Also per the round: - mixed-anchor guard asserts non-null period_start/period_end, not just the window_kind label (1/1 on a labeled-but-columnless probe). - rollback test now compares every claim row (claim_id, source, value, conditions, baseline_key), not source counts, and its docstring says "logically identical" — which is what is actually asserted. Test-only delta. Suite: 215. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…es, one transaction Rebased from the pre-repair #48 branch onto main (the two original harvest commits cherry-picked intact) and adapted to the foundation contract, fixing four defects: 1. value_kind was "usd" on GBP rows -> "gbp" (sol's #48-round-1 class). 2. The FRR reform rows rode the DEFAULT baseline (= current law), but the costing's own counterfactual is the 25%-cap law the FRR replaced — today's current law INCLUDES the FRR, so a PE result computed against it would be a cross-world comparison. The rows now carry the registered pre_frr_uc_deductions world (baselines.py entry with AB2024/DWP-PR provenance), mirrored in conditions["baseline_policy"]. 3. The £420 average annual gain was bare GBP — a per-household average a query could sum. New GBP_PER_HOUSEHOLD unit concept (mirror of USD_PER_HOUSEHOLD, same value_kind rule). 4. ingest() was a bare upsert with no gates. Now the foundation shape: ONE transaction (delete both sources + insert + the deliberate- registration gate + lane row — commit or nothing), lane-feed mirror after commit. Also: period asserted = fy END year per row (the live claim convention; staged rows verified clean, all 8); identity values (program/subgroup/geography/unit) route through the closed registry (hm_treasury + dwp registered in uk_aliases); measure + fiscal_measure promoted into STANDARD_CONDITIONS; staged unit labels validated against the expected concept per metric. Committed DB ingested: 58,461 -> 58,469 claims; registry 52 -> 53 worlds; uk-deductions-frr lane 'ingested' in DB + feed. Suite: 224. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rebase completion (the branch forked before the whole UK arc — #43-#48, #52, #71, #72 all landed under it): - uk-deductions-frr (merged today, unknown to this branch) was the one committed lane without a country tag -> UK. - The UK ingests' appended lane metas now carry country explicitly (ingest_uk_externals' five lanes + ingest_uk_deductions' one), so a fresh-feed append passes the new sync_lane_feed guard and never files a UK lane under the app's missing-key US default. - app/public/data copies refreshed from data/ (they had drifted to a 270-row populations.json vs 284) and pinned: new test asserts the committed copies byte-match data/, and a second asserts every committed lane carries US|UK — the two drift classes this rebase surfaced. Suite: 241 python + 3 bun; oxlint + vite build clean; committed DB byte-stable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uard (#73) The #44 follow-up from the #48 gate: cells now carry the workbook's unrounded office:value (the 16% headline is 0.15911572...), with the display text kept only as a rounded-rendering consistency guard that aborts mispaired cells. Headline checks become rounds-to and are mutation-tested non-vacuous. Regenerated externals + DB: 12,342 dwp_hbai values changed, nothing else — accounting, anchors, conditions, claim ids all identical; zero rate rows left at exact 2dp. Dual gate: fable + sol, one round (sol reparsed the vendored ODS and matched all 13,056 rows exactly). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…e the country config with the executed-baseline provenance The promised one-line GBP resolution (this branch was second to land, so its duplicate UnitConcept.GBP goes; #48's commented one stays). The rebase also reconciles #71's executed-baseline machinery with the per-country config: ENGINE_VERSIONS lives inside COUNTRIES["US"] with the back-compat alias, and _obbba_results carries both #71's position argument (chain_pos construction) and the country/run_prefix parameters. Suite: 248 passed, 6 skipped; ruff format clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Part of #33 — the UK ingest path itself. (Not "Closes": the campaign-results attachment half stays open, for the reason documented below.)
What's here
scorecard_db/ingest_uk_externals.py— the UK analogue of the harvest adapters: maps the five UK adapter outputs (data/externals/, from DWP take-up adapter (mode 1): FYE 2024 external side #43–UKMOD statistics adapter (mode 1): Country Report validation tables #47) ontoExternalScoreunder the same contract (fail loudly on unmapped vocabulary, tally deliberate drops, values never re-derived). Per-source stagers are individually testable, mirroring thestage_scores()pattern.scorecard_db/models.py— deliberate vocabulary extensions: 7 new Metric members (taxpayer_count,average_tax_rate,average_tax_amount,unclaimed_benefit_amount,gini,income_statistic,income_share), 3 UnitConcepts (gbp,benefit_units,children), and 8 documented UK STANDARD_CONDITIONS keys (country,fy,housing_costs,poverty_line,basis,welfare_cap,quantile,per).tests/test_uk_externals_ingest.py— mapping tests on synthetic rows (run on any branch) + a full round-trip integration test that self-activates once the adapter outputs merge.Verified end-to-end
Run against the real adapter outputs (pulled from the #43–#47 branches): 16,924 claims — dwp_takeup 1,638 / dwp_hbai 12,888 / uk_hmrc 1,213 / obr 259 / ukmod 758 — with zero
claim_idcollisions through thefinish()gate, and deliberate drops tallied (1,456 DWP CI-range variants; 373 UKMOD non-primary variants — score against primaries, never transcriptions).Honesty decisions
consumed_as_target(pe-uk take-up parameters cite the same publication); HBAI isheld_outunder the census-spm doctrine; OBR 2024-25 outturn isconsumed_as_target, forecast yearsheld_out.policy_refworlds slugged from the verbatim change description againsthmrc_indexed_baseline_spring_2025, with the description kept asconditions["option"]— so each of the 75 changes × 3 years is a uniquely-keyed world.fylabel + integer end-year period; HBAI 3-year regional averages rideperiod_start/period_endwithwindow_kind=annual_average.Finding: the campaign descriptors need re-emission
The staged
hmrc_reckoner_t2family (14 PE results insources/campaign-20260802/uk/) cannot attach yet: its descriptors under-specify. Two personal-allowance rows carry byte-identical conditions (tax_section+fy+direction_semantics) — the actual change (£100 vs 1% vs 10%) lives only in free-textpe_construction. The exactly-one-match contract iningest_campaign.pycorrectly refuses this, and the repo's never-guess doctrine says the fix is re-emission with the change label, not inference. The DB claims here are keyed(option, fy)ready for that join — the integration test demonstrates the exactly-one query. The other UK families (free_joins → OBR receipts lines, obr_measures → policy costings, two_child/uprating → Resolution Foundation) additionally need their harvest claims, which are not vendored in this repo.Sequencing
Merge after #43–#47 (the module's inputs). CI is green on this branch standalone — the integration test skips itself until the externals exist.
🤖 Generated with Claude Code