Skip to content

UC deductions: FRR family re-harvested from primary sources + DB ingest - #52

Merged
MaxGhenis merged 6 commits into
mainfrom
uk/uc-deductions-frr
Aug 19, 2026
Merged

UC deductions: FRR family re-harvested from primary sources + DB ingest#52
MaxGhenis merged 6 commits into
mainfrom
uk/uc-deductions-frr

Conversation

@vahid-ahmadi

@vahid-ahmadi vahid-ahmadi commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Part of #39 (staging handoff from #21). Stacked on #48 (base branch uk/db-ingest-uk-externals — it builds on that PR's Metric vocabulary); merge #48 first and this rebases to a clean diff.

Why a re-harvest

#21 staged the UC-deductions claims at ~/scorecard-harvest/uk_deductions/ — machine-local, not vendored. Rather than transcribe from the issue text, the Fair Repayment Rate family is re-harvested from primary sources, vendored in-repo, and page-verified (2026-08-14):

  • sources/harvest-uk-deductions/frr/autumn_budget_2024.pdf — FRR passages at paras 2.30 (p.47), 4.111 (p.110), 5.134 (p.142)
  • autumn_budget_2024_policy_costings.pdfverified absence: no FRR costing line in all 93 pages, consistent with UK UC deductions: external scores staged (FRR, JRF floor, historical cap costings) #21's finding that the measure is a financial transaction with no PSNB impact
  • DWP press release 2025-04-30 (effective date, 25%→15%)

claims_staged.jsonl carries 8 claims, each with a verbatim quote and page locator: PSNCR +£385m (2029-30), 1.2m gaining households, £420/yr average gain, 700k families with children — HMT and DWP statements staged as separate citable claims — plus the 2.8m households-with-deductions pre-FRR level. VERIFICATION.md records the full audit and what remains un-staged from #21 (JRF floor with its pre/post-FRR baseline_key split, historical cap costings, screening counts).

The ingest

scorecard_db/ingest_uk_deductions.py, under the harvest fail-loud contract:

  • The PSNCR line lands as a new cash_requirement_change metric with fiscal_measure=psncr — deliberately not revenue_change, so no PSNB-shaped comparison can ever silently join against it. This is UK UC deductions: external scores staged (FRR, JRF floor, historical cap costings) #21's "benchmark against PSNCR, never PSNB" rule made structural, and test_no_psnb_shaped_claims enforces it.
  • Reform rows ride policy_ref {"policy": "uc_fair_repayment_rate"} with the cap change and effective date as reform detail; the pre-FRR deductions level is a baseline claim.
  • Everything held_out; the DWP quarterly deductions outturn tables (the calibration source PE's parameters consume) are not staged at all — that boundary is the other UK UC deductions: external scores staged (FRR, JRF floor, historical cap costings) #21 rule.
  • models.py gains cash_requirement_change / gainer_count / average_annual_gain (the distributional-impact vocabulary of UK fiscal-event documents).

Verification

4 new tests (headline values, PSNCR-not-PSNB, quote+publication on every claim, DB round trip); full suite 137 passed, ruff clean.

The PE comparator from policyengine-uk#1815 (0.85m benunits, £421 mean gain vs DWP's £420) attaches as a pe_results row once its run artifacts are staged with engine/bundle provenance — that, plus the remaining #21 families, keeps #39 open.

🤖 Generated with Claude Code


Merge order: this PR hard-depends on #48 (UnitConcept.GBP and the #33 metric vocabulary come from that base) — merge #48 first; the branch is rebased onto it. #65 adds UnitConcept.GBP independently; whichever merges second takes the trivial enum conflict.

@DTrim99

DTrim99 commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Review — UC deductions: FRR family re-harvest

Provenance is honest and the structure is clean: 8 rows, no claim_id collisions, imports resolve, and the figures are round headline quotes expanded (m → raw), not derived. Reviewed as this-lane-only (stacked on #48).

Confirm before merge (stacking)

Should address

  • Row-3 quote may not be verbatim (claims_staged.jsonl:3, average_annual_gain): "households expected to be better off by £420 a year on average" reads as a reworded fragment of row-2's sentence ("…1.2 million households will be better off by £420 per year on average…") from the same paragraph. Two rows citing one sentence with differently-worded "verbatim" quotes — either confirm UK adapters (mode 1) #3 is a distinct string in AB2024 or point it at row-2's quote.

Suggestion

  • The "verified absence" of an FRR costing line (93 pages) is prose-only in VERIFICATION.md; test_no_psnb_shaped_claims only asserts no REVENUE_CHANGE metric, which is a proxy. A machine-checkable marker (a costing_absent: true note or a grep-assert fixture) would make the absence auditable rather than trust-me.

Reviewed with Claude Code assistance.

@vahid-ahmadi

Copy link
Copy Markdown
Contributor Author

All three items addressed in b32f8a0:

  1. Merge-order dependency now stated in the PR body: UK externals → DB ingest path (16,924 claims, five sources) #48 first (UnitConcept.GBP + the UK harvest → DB ingest path: attach the 48 staged campaign results #33 vocabulary are its base); branch rebased onto the updated UK externals → DB ingest path (16,924 claims, five sources) #48.
  2. Row-3 £420 quote confirmed distinct and verbatim: extracting the vendored AB2024 print shows two separate sentences carrying the figure — para 4.111's "This will mean 1.2 million households will be better off by £420 per year on average…" (row 2) and the para 2.30/5.134 wording "…with households expected to be better off by £420 a year on average" (row 3). Not a reworded fragment; a test now pins both strings verbatim (test_420_quotes_are_two_distinct_verbatim_sentences).
  3. Costing absence machine-checked (your suggestion): test_frr_costing_absence_is_machine_checked extracts the vendored 93-page costings PDF and asserts zero occurrences of the measure name; VERIFICATION.md points at the test. Both PDF tests importorskip(pypdf) so bare CI skips them and local runs enforce them.

Suite with pypdf: 182 passed / 4 skipped; ruff clean.

@DTrim99

DTrim99 commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Re-review — all addressed ✅

  • UnitConcept.GBP — correction to my earlier note: this is supplied by the base (UK harvest → DB ingest path: attach the 48 staged campaign results #33), not UK externals → DB ingest path (16,924 claims, five sources) #48, and the diff correctly extends only Metric. So it imports cleanly and isn't a GBP merge-order blocker. My prior attribution was wrong — sorry for the noise.
  • Row-3 "verbatim" quote — now a distinct passage (para 4.111 "£420 per year" vs 2.30/5.134 "£420 a year"), with test_420_quotes_are_two_distinct_verbatim_sentences asserting both appear verbatim in AB2024.
  • "Verified absence" — now machine-checked (test_frr_costing_absence_is_machine_checked extracts the costings PDF and asserts zero "Fair Repayment Rate" hits).

Clean from my side.

@DTrim99 DTrim99 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed — all findings addressed (GBP comes from #33 as noted, row-3 quote is now distinct + verbatim-tested, absence machine-checked). Approving.

@MaxGhenis
MaxGhenis force-pushed the uk/db-ingest-uk-externals branch from 0a8bc91 to 2eb5576 Compare August 19, 2026 20:19
MaxGhenis added a commit that referenced this pull request Aug 19, 2026
… 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>
@MaxGhenis
MaxGhenis deleted the branch main August 19, 2026 21:32
@MaxGhenis MaxGhenis closed this Aug 19, 2026
@MaxGhenis MaxGhenis reopened this Aug 19, 2026
@MaxGhenis
MaxGhenis changed the base branch from uk/db-ingest-uk-externals to main August 19, 2026 21:33
vahid-ahmadi and others added 3 commits August 19, 2026 17:34
Part of #39/#21. The original ~/scorecard-harvest/uk_deductions staging
is machine-local, so the Fair Repayment Rate family is re-harvested
directly from primary sources, vendored, and page-verified 2026-08-14:

- sources/harvest-uk-deductions/frr/ vendors the AB2024 print (FRR
  passages: paras 2.30/4.111/5.134) and the AB2024 policy costings
  document (verified ABSENCE of an FRR costing line across all 93
  pages — the measure is a financial transaction), plus 8 staged
  claims each carrying a verbatim quote and page locator:
  PSNCR +GBP385m 2029-30, 1.2m gaining households, GBP420/yr average
  gain, 700k families with children (HMT and DWP statements as
  separate citable claims), and the 2.8m households-with-deductions
  pre-FRR level.

- scorecard_db/ingest_uk_deductions.py maps them under the harvest
  fail-loud contract. The PSNCR line lands as the new
  cash_requirement_change metric with fiscal_measure=psncr —
  deliberately NOT revenue_change, so no PSNB-shaped comparison can
  silently join against it (the #21 rule; a test enforces it). Reform
  rows ride policy_ref uc_fair_repayment_rate (cap 25%->15%,
  effective 2025-04-30 as reform detail); the pre-FRR level is a
  baseline claim; everything held_out. DWP quarterly deductions
  outturns (PE's calibration source) are not staged at all.

- models.py gains cash_requirement_change / gainer_count /
  average_annual_gain (commented; stacked on the #33 vocabulary).

Remaining from #21, documented in VERIFICATION.md: JRF floor
(pre/post-FRR baseline_keys), historical cap costings, DWP/DfC
screening, Policy in Practice, Citizens Advice — each needs its own
page-verified pass. The policyengine-uk#1815 PE comparator attaches
once its run artifacts carry engine/bundle provenance.

Part of #39

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…checked

- Rows 2 and 3 verified as two DISTINCT verbatim AB2024 sentences (para
  4.111 vs the para 2.30/5.134 wording) by extracting the vendored PDF;
  a test now pins both strings so neither can drift into a reworded
  fragment of the other.
- The 93-page costing-absence claim is machine-checked: a test extracts
  the vendored costings PDF and asserts zero occurrences of the measure
  name (importorskip(pypdf) — skips on bare CI, runs locally).
- Rebased onto uk/db-ingest-uk-externals (UnitConcept.GBP dependency).

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>
@MaxGhenis
MaxGhenis force-pushed the uk/uc-deductions-frr branch from b32f8a0 to 2559bfa Compare August 19, 2026 21:47
MaxGhenis and others added 3 commits August 19, 2026 18:06
…ng + fy gates

Sol's round-1 probes on the rework, both blockers fixed:

1. Staged conditions merged AFTER the generated identity fields, so a
   poison row's geography="Mars" or a baseline_policy contradicting
   ReformRef.baseline was accepted. Reserved keys (country, geography,
   fy, baseline_policy) now raise, and the generated fields merge LAST
   (loud + inert). Poison tests for both vectors.

2. The 2.8m "households with deductions" row REMOVED from staging.
   Re-verified against the live press release: the staged quote was a
   paraphrase (the actual sentence is "With as many as 2.8 million
   households seeing deductions made..."), the release states no
   measurement vintage (the FY2025-26/period-2026 identity was
   unsupported — the reviewer traced the figure to DWP's Dec-2023 to
   Nov-2024 administrative deductions statistics, pre-FRR), and the
   quantity is exactly the DWP quarterly deductions OUTTURN family
   relationships.py routes away from external scores. Disposition
   recorded in VERIFICATION.md: future Ledger lane, staged from the
   statistics publication itself. The reform_policy=None branch now
   raises — a future level row is a deliberate decision, never a
   silent default.

Hardening per the round:
- Exact per-source accounting (hm_treasury 4 + dwp 3): an empty or
  truncated staging fails wholesale instead of delete-and-commit-zero.
- fy label validation: '2029-99' is malformed (suffix must be start
  year + 1), in this module AND in the foundation _fy (same class;
  '1999-00' still parses).

DB re-ingested: 58,468 claims (7 FRR), lane detail '7 claims'.
Suite: 227.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Python's \$ matches before a final newline, so the foundation _fy's
re.match(r"^...\$") accepted "2029-30\n", "2029/30\n", and "FYE 2030\n"
(sol round-2 probe). Both patterns now re.fullmatch, with trailing-
newline regressions; no other ^...\$ match patterns remain in the
package. Suite: 227.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two remaining \$-anchored call sites (round-3 probes: a trailing-
newline HBAI window period and UKMOD poverty metric were both
ingested): _SPAN and _UKMOD_POVERTY now fullmatch, with regressions.
_UKMOD_TAX_PROGRAMS stays .match deliberately — it is a prefix pattern
over canon-validated program values.

And the ROOT CAUSE of the recurring committed-DB header churn, fixed
rather than re-cleaned: ScorecardDB.__init__ unconditionally
DROP+CREATEd the comparisons view, so every process that merely READ
the committed DB (tests, exporters) wrote a schema change and bumped
the SQLite header counters — that is how round 3's stray binary delta
got swept into a test-only commit. _ensure_view now recreates the view
only when its stored definition differs from the code's; new tests pin
open/close byte-stability (x3) and stale-view rebuild. The committed
blob is restored to its round-2 state and verified byte-stable across
the full suite.

Suite: 230.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis merged commit df359f1 into main Aug 19, 2026
2 checks passed
@MaxGhenis
MaxGhenis deleted the uk/uc-deductions-frr branch August 19, 2026 22:35
MaxGhenis added a commit that referenced this pull request Aug 20, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants