Skip to content

Revive the UK leg as an internal audit of policyengine-uk - #212

Merged
MaxGhenis merged 10 commits into
mainfrom
uk-revival-2026-10
Oct 11, 2026
Merged

MaxGhenis merged 10 commits into
mainfrom
uk-revival-2026-10

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This revives PolicyBench's UK leg as an internal audit tool for policyengine-uk. Nothing is published: the public release stays US-only, and no snapshot, payload or dashboard file changes.

Bit-rot fixed (each one observed, with a test)

  1. Engine pin. policyengine-uk 2.89.0 → 2.125.1 (latest on PyPI, 2026-10-10). The 5 October branch bumped policyengine-core from 3.32.8 to 3.32.17 and changed no US reference: a fresh 100-household US sample gave identical references under both, 2,292 of 2,292 rows. Main's lock now carries 3.32.29.
  2. PIP silently zero. The pinned transfer artifact (uk-data 9514dfb) stored pip_*_reported, which policyengine-uk stopped reading in #1656, so PIP was 0 for every household. It is re-pinned to uk-data 6b1f80e (sha256 c663daea…), which stores PIP categories.
  3. Prompt and reference disagreed on amounts. The prompt showed the stored 2025 amounts; the reference used PE-UK's uprated 2026-27 values (employment income ×1.034, rent ×1.021). Given the prompted facts, the engine's income tax matched the old reference in 24 of 100 households. The loader now reads every stored variable at the reference period.
  4. References read facts the prompt omitted. UK references came from the full microdata record: UC deductions, a seeded draw of new LCWRA claimants, sex, and a seeded date of birth. They now come from a PE-UK simulation of exactly the prompted facts, one per household, as on the US path. calculate_uk_transfer_microsimulation_values keeps the old path as a cross-check: 691 of 700 cells agree. The 9 that don't are explained: 8 by UC deductions, 1 by the LCWRA draw.
  5. Silent skips on renamed variables. is_child_or_QYP was removed in policyengine-uk#1655 and the loader skipped it without warning. The loader now fails loudly on any stored variable the engine doesn't define, on a renamed derived variable, and on benefit-unit inputs the prompt can't show.
  6. Derived facts the engine relies on. The prompt now states them:
    • sex and date of birth (State Pension age, the savings credit cutoff);
    • education status at 16-19 (qualifying young person rules; PE-UK otherwise imputes it from age);
    • LCWRA held since before 6 April 2026, in place of "is disabled for benefits", with a child-specific line for children;
    • annual hours worked.
  7. Supervisor was US-only. It never passed --country, so every UK worker failed. It now passes each scenario's country and output set. US workload fingerprints are unchanged (tested).

One canonical scenario (review rounds 2-6)

The prompt renderer and the reference builder both start from canonical_uk_scenario / canonical_uk_person, so a scenario loaded from a manifest gives each the same facts. It applies these conventions, each stated in the prompt:

  • Facts as shown. Amounts are stored as the whole number the prompt shows and rates to the four significant figures it shows. Explicit zeros are kept as stated facts.
  • Relationships. Two listed adults are "a couple, not married or in a civil partnership"; the reference sets claimant and partner flags and no marriage, so PE-UK's presumed-child rule and married-couple presumption don't apply.
  • Locality. A private renter without a Broad Rental Market Area gets a fixed BRMA in their region, stated in the prompt (PE-UK otherwise reads Maidstone's LHA everywhere, policyengine-uk#2256).
  • Statuses PE-UK would fill in. Someone aged 16 to 19 with no education status is not in education. Non-advanced education is "full-time ... below higher-education level", with the age it began (16 where none is given). Someone with self-employment income and no determination is not in gainful self-employment.
  • Pension contributions. PE-UK deducts both kinds from taxable income and neither from adjusted net income (policyengine-uk#2243). The labels state that convention: employee contributions under a net pay arrangement, personal contributions as a gross amount; neither is described as relief at source.
  • Rejected inputs. Tax-unit and benefit-unit inputs, and person or household fields the prompt filters out, raise instead of reaching the engine unseen.

Reference adversary for the UK

The reference adversary from #200 now audits the UK leg:

  • consensus-flags and adversary-prepare take --country; load_payload reads one country's payload.
  • UK cases get UK prompts: fiscal year 2026-27, legislation.gov.uk and GOV.UK sources, pounds, region, and a 2026-10-10 freeze with the schema to match.
  • All 62 committed US cases re-render byte for byte (checked against main's module), and the US schemas are unchanged.

Invariants (tested)

  • Prompted facts only. A UK reference situation holds exactly the prompted facts (Hypothesis property test over random inputs).
  • No batch dependence. A household's reference does not depend on the other households in the batch.
  • Reference-period inputs. Prompted amounts are the reference-period values, not the stored ones.
  • No silent drops. Every stored artifact variable is one the installed engine defines.
  • Protected LCWRA claim. A lone LCWRA household gets more than the new-claimant rate, which backs the prompt's "held since before 6 April 2026". This is a slow test against the real engine.
  • US unchanged. US supervisor fingerprints and US adversary prompts are unchanged.

Verification

Run locally on Python 3.13 at the final head:

Six rounds of GPT-6.1 Sol review; round 6 approved 869e9bb2. The reviews and prompts are in ~/reviews/policybench-uk-revival-2026-10/reviews/.

What this found

The internal UK run on 100 transfer-path households cost $11.08 across six models. A blind reference audit of 79 cells (59 consensus, 20 single-model misses) found 21 cells on policyengine-uk defects, 34 on unstated facts (now stated, above), 9 on rounding (fixed here) and 13 where the reference held. Engine fixes from this work:

The full record is ~/reviews/policybench-uk-revival-2026-10/README.md. Whether to publish a UK board is Max's call (d1254).

🤖 Generated with Claude Code

MaxGhenis and others added 3 commits October 10, 2026 07:46
- Pin policyengine-uk 2.121.0 (core 3.32.17); a fresh 100-household US
  sample gives identical references under both cores (2,292 rows).
- Re-pin the public transfer artifact to policyengine-uk-data 6b1f80e, which
  stores PIP component categories (the old pin's pip_*_reported columns were
  silently ignored after policyengine-uk#1656, zeroing PIP).
- Prompt reference-period (uprated 2026-27) inputs instead of the stored 2025
  values; income tax references matched the prompted facts in only 24 of 100
  households before.
- Compute UK references from the prompted facts, one PE-UK simulation per
  household (the 2026 UC health element is a seeded per-benefit-unit draw);
  keep the full-microdata calculation as a cross-check.
- State derived facts the engine uses: sex and date of birth, education
  status at ages 16-19, LCWRA held before 6 April 2026.
- Fail loudly when the artifact stores a variable the engine does not define
  or a prompted derived variable is renamed (is_child_or_QYP was removed).
- Make the supervisor pass --country and use the country's output set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
consensus-flags and adversary-prepare take --country; load_payload reads a
country's payload (load_us_payload keeps its behaviour). UK cases get UK
prompts: fiscal year 2026-27, legislation.gov.uk and GOV.UK sources,
pounds, the region, and a 2026-10-10 freeze (policyengine-uk 2.125.1), with
the schema's freeze date to match. All 62 committed US cases re-render
byte for byte and the US schemas are unchanged. Also pin policyengine-uk
2.125.1, the latest release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
policybench-site Ready Ready Preview Oct 10, 2026 10:51pm UTC

Request Review

The prompt formats every UK amount with no decimals, but the situation
used the unrounded reference-period values (£15,095.16 shown as £15,095),
so 150 of the internal run's 700 references sat up to £0.99 from the
stated facts. Round each UK numeric input as the formatter does (half to
even) when building scenarios; an amount shown as £0 becomes unlisted.

For the frozen internal run, a rounded copy of the manifest renders all
100 prompts byte for byte as the models saw them, and its references
leave the 59 consensus flags unchanged.

Property test: rounding never changes what the prompt displays for an
amount that stays in the prompt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ference

Address the review of #212:
- Relationships: the listed adults go in as claimant and partner and every
  child as neither (is_claimant_or_partner), and a couple as not married,
  matching the prompt, which now says so. PE-UK otherwise presumed a 37/18
  pair to be parent and child and presumed every couple married, giving
  Marriage Allowance the prompt never states. On the frozen run this moves
  ten income tax references by £252 and resolves five consensus cells.
- Locality: a private renter gets a Broad Rental Market Area, the area of
  the region's largest city, stored and stated in the prompt. PE-UK reads
  the LHA from the BRMA alone and defaults everyone to Maidstone.
- Education: someone in non-advanced education is stated (and supplied)
  to be in full-time education begun before 19; PE-UK's default entry age
  (1000) failed the qualifying-young-person entry condition.
- LCWRA wording: the assessment, with protected status conditional on UC
  entitlement, so pension-age adults are not told they hold an award.
- Boundary: to_pe_uk_situation rejects tax-unit or benefit inputs and
  person fields the prompt filters out.
- Tests: the situation property test uses valid typed values and checks
  each input against the rendered prompt; new tests for the boundary, the
  renter BRMA, and (slow, real engine) an unmarried 37/18 couple.
- docs/audit.md names each country's freeze; the benchmark card describes
  the new conventions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Address round 2 of the #212 review. The conventions (whole-number amounts,
a private renter's Broad Rental Market Area, a non-advanced-education entry
age) applied only when scenarios were extracted from the transfer data, so
a scenario loaded from a manifest could still give the prompt and the
reference different facts.

- canonical_uk_scenario applies them, idempotently; describe_household and
  to_pe_uk_situation both start from it.
- The education entry age is a stated fact: an explicit age is kept and
  shown, 16 only where the scenario gives none (it overwrote explicit ages).
- to_pe_uk_situation also rejects household inputs the prompt filters out.
- The BRMA mapping is described as a fixed area within each region
  (Maidstone is not the South East's largest city).
- Tests: a loaded unrounded manifest gives prompt and reference the same
  rounded facts and BRMA; explicit entry ages 18 and 19; hidden household
  inputs; idempotence (Hypothesis).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lief method

Address round 3 of the #212 review and two conventions the reference audit
surfaced.

- Explicit zeros are stated facts and are kept (a zero
  months_since_last_birthday moved the date of birth when dropped); rate
  fields keep their precision; months render without a currency sign.
- canonical_uk_person, shared by describe_person and the reference: someone
  aged 16 to 19 with no education status is "not in education" (PE-UK
  imputed enrolment from age), and someone with self-employment income and
  no gainful self-employment determination has none (PE-UK presumed one and
  applied the Universal Credit minimum income floor). Both follow the
  prompt's rule that an unlisted status is false, and both are stated.
- Pension contribution labels state the relief method: employee
  contributions under a net pay arrangement, personal contributions as
  relief at source.

On the frozen run this moves three Universal Credit references; two land on
the model consensus and the audit judge's own answer (scenario_044 £8,442.06,
scenario_047 £15,873.48).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 2 commits October 10, 2026 16:55
… shown

Round-4 review of the UK revival.

- The prompt called personal pension contributions "relief at source",
  but PolicyEngine UK deducts them from taxable income, so the label
  described a method the reference does not follow. It now states the
  convention: a gross amount deducted from taxable income.
- A UK rate reached the engine at full precision while the prompt showed
  four significant figures. The canonical scenario now stores the rate
  the prompt shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round-5 review of the UK revival. PolicyEngine UK deducts both kinds of
pension contribution from taxable income but neither from adjusted net
income, so they do not lower the High Income Child Benefit Charge or the
personal allowance taper (policyengine-uk #2243). Both prompt labels and
the benchmark card now say so. The rate round-trip property also covers
negative rates and values around the thousands separator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Merging on the gates.

  • Head: 869e9bb2.

  • Checks: all green (lint, paper, test, app, Vercel).

  • Mergeable: MERGEABLE, CLEAN, not a draft, no change request.

  • Independent review: GPT-6.1 Sol, six rounds. Rounds 1 to 5 requested changes:

    • relationships and marriage;
    • BRMA, rounding on manifest load, entry age, hidden household inputs;
    • explicit zeros, missing education, rates;
    • the personal pension label and rate precision;
    • pension contributions and adjusted net income.

    All were fixed. Round 6 approved this head.

  • Scope: no snapshot, payload or dashboard file changes, and no score changes; the public release stays US-only.

  • Merge method: squash, as for the recent PRs on main.

🤖 Generated with Claude Code

@MaxGhenis
MaxGhenis merged commit efaf5b3 into main Oct 11, 2026
6 checks passed
@MaxGhenis
MaxGhenis deleted the uk-revival-2026-10 branch October 11, 2026 13:16

This branch was successfully deployed

1 active deployment
Preview — 869e9bb2 Deployed Oct 10, 2026 by vercel[bot]
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.

1 participant