Skip to content

Lock policyengine-uk 2.162.3 and take the actionable #1095 items - #1400

Merged
juaristi22 merged 33 commits into
mainfrom
uk-pe-uk-bump-next
Oct 26, 2026
Merged

juaristi22 merged 33 commits into
mainfrom
uk-pe-uk-bump-next

Conversation

@juaristi22

@juaristi22 juaristi22 commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The next policyengine-uk bump (plan of 2026-10-25, part of #1095). It locks policyengine-uk 2.162.3, writes or records the inputs the releases from 2.152.0 add, and takes the #1095 items that need no new data source, no A/B of their own and no open ruling. It is rebased on main after #1381, so it reads the feed at 272c987, and every derived file is regenerated on that pin. Draft: see "Before ready".

The #1095 items

  • Three FRS household codes refuse a value outside the data dictionary, naming the variable: GVTREGNO, PTENTYP2 and TYPEACC (Map FRS household codes from explicit data-dictionary tables policyengine-uk-data#636). The council tax band (CTBAND) keeps its mapping: a code outside bands A to I still takes band D.
  • Seven more inputs left at the engine default are recorded with their errors.
  • obr.domestic_rates compiles, pinned to the UK, and is held as a measured diagnostic: OBR's line looks like the district rate only, and domestic_rates is the whole bill.
  • The held ORR rail-support row is measured on table 7271's support from all sources.
  • England's single adult council tax discount gains its billing-authority cells (MHCLG Council Taxbase line 8), controlled by the national row, with their signed local hold.
  • HMRC's Child Benefit families in payment by number of children bind as five national rows (one to four, five or more; August 2025), on a new count of each benefit unit's Child Benefit children. Their notes record that the counts imply about 1.8 percent more spending than obr.child_benefit allows; both rows bind, so the solve splits the gap.
  • The self-employment loss-relief measures are declared once as named terms of the expression grammar, which gains max(<sum>, 0).
  • Report-only tools: engine council tax against the stored FRS bill (CT-C), the local age rows on a private-household basis, and single years of age against ONS mid-2025 (Measure each build's HBAI distribution and single ages against its base policyengine-uk-data#600's age check). The age report withholds a whole sex when any of its single years rests on fewer than 10 source households.
  • A test guards the nested mortgage debts; paternity pay's 52 weeks get a note.
  • Write policyengine-uk's alcohol, tobacco and gambling duty inputs from the LCFS stage #1381's gambling participation gate lets the stake scale reach 2.5 instead of 2.0 (María's ruling of 2026-10-26). Full spine builds, on main after Write policyengine-uk's alcohol, tobacco and gambling duty inputs from the LCFS stage #1381 and here, put the National Lottery's at about 2.03, so every full UK spine stopped at that gate.

The lock and what it moves

  • policyengine-uk 2.151.0 to 2.162.3, the newest on PyPI; policyengine-core stays at 3.33.3. The uprating pins, the bonus sacrifice anchor, the CGT projection gate's pinned growth path (2.155.0's OBR population vintage), the concept coverage and APPROVED_UV_LOCK_SHA256 re-derive. Two engine tests pinned uprating factors from before that vintage and now pin the engine's.
  • The HMRC and OBR income tax rows read the tax before the Winter Fuel Payment charge (2.157.0).
  • obr.winter_fuel_allowance binds FY2025-26 alone: the payment falls in November and December, so calendar 2025 holds the 2025-26 winter, which the engine models from 2.157.0.
  • obr.vat measures VAT on private school fees too (2.157.1); it stays held.
  • England outside London's bus fares are priced as the engine prices them (2.158.3).
  • The DfE spring-term universal measure reads the engine's own eligibility (2.156.1). The measure for 2-year-olds keeps its DfE-age gate.
  • From 2.162.2 the engine's savings_interest_income is the interest outside tax-free accounts: it reads gross interest under a legacy_gross ISA income contract and takes tax_free_savings_income out when it loads the file. The frame still writes gross interest, and now declares that contract itself, in every H5 it writes and in the datasets it hands the engine. So the HMRC SPI interest amounts by income band now measure the interest outside ISAs, as SPI Table 3.7 counts it, and their counts now count the recipients of interest outside ISAs. The ONS interest diagnostic adds the ISA part back.
  • isa_cash_interest_flat_rate_charge (2.162.2), a tax on the cash interest in non-cash ISAs from 2027-28, joins the HBAI deductions that the survey-concept gross income adds back.
  • The 13 inputs that 2.161.1 and 2.162.2 add stay at their defaults, recorded with their errors: Northern Ireland's Energy Price Guarantee inputs, ISA dividends and non-cash ISA interest, the Lifetime ISA's flows and two purchase flags.
  • The national and local references, membership and compile-parity receipts, the local census, the coverage manifest, the H2 fixture and the gate digests are regenerated on 272c987.

Moved out on María's ruling: the household-property split among FRS householders goes to the A/B PR, since it moves the UC take-up gate and needs its own measurement.

Measurement

Local, licensed runs; their outputs stay with the spines.

Before ready

Disclosure

No figure in this PR, its commits or its changelog rests on fewer than ten source households or names a record. No row's model figure is given where the row is of the SPI channel. The surface-wide fit and weight statistics are given as in #1308. Publisher figures appear as published.

Signatures

  • María signed on 2026-10-25: the Child Benefit rows' in-year hold and the single adult discount cells' local hold (committed with the local regeneration).
  • Awaiting her signature: the engine defaults recorded with their errors (the seven above, and the inputs 2.155.0 to 2.162.2 add), and the paternity-pay note.

🤖 Generated with Claude Code

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

@juaristi22 round 1, at 46e40709d.

Each #1095 item does what the body says, and the lock is consistent. One must: a stale reference count that the body doesn't mention, which turns engine-free CI red and can be fixed without the regeneration. Engine-uk is red too, for a reason I couldn't pin down. The rest are shoulds and nits.

Findings

  1. Stale reference count (must). test_country_spec.py:1188 asserts len(references) == 1336, but the committed target_references.json holds 1342. The count was 1336 on main, became 1337 after the regeneration (6ba3f1a26) and 1342 after the Child Benefit rows (27f49b899, whose message already says "1,342 active references"). Neither commit updated the assertion or its comment. It fails locally, and it is probably why engine-free fails alongside the battery test you already disclose. It needs no regeneration: update the count and add the + 1 and + 5 lines to the comment.
  2. Engine-uk is red (should). Both engine-uk jobs fail (exit 1). The logs aren't available while the engine-us jobs run. The six touched engine-tier files pass locally (42 tests), so the failure is elsewhere in the tier. Please post the failing tests, or note them under "Before ready" if they wait on the regeneration.
  3. Child Benefit counts against the spending row (should). The five rows sum to 6,867,705 families, which matches HMRC's August 2025 headline of 6.87m in payment. With about 11.73m children, the counts imply about £13.61bn at the calendar-2025 window (a quarter at £25.60/£16.95 a week, three quarters at £26.05/£17.25). The obr.child_benefit row is £13.37bn, so the counts ask for about 1.8% more spending than the spending row allows. Both bind, so the solve splits the gap. That's fine, but please record it in the rows' notes and give both residuals on the final arm. The filter is right: the engine's child_benefit is zero where would_claim_child_benefit is false and for a unit that stays opted out (entitlement * ~(opts_out & remains_opted_out)), so the rows count families in payment, as HMRC does.
  4. Two-child-limit exceptions at false (should, as a UK: every open data item: uk-data parity, engine locks, the local surface, consumption and high-value homes #1095 follow-up). policyengine_uk_concepts.py:233 records the five 2.161.0 exception inputs at their default. That isn't a regression, because 2.151.0 modelled no exceptions either (its is_CTC_child_limit_exempt was birth date only). But the two-child-limit rows bind, and with no exceptions the model counts every excepted child as affected, so the weights lean against larger UC families by DWP's exception share. Multiple births (same-age siblings) can be read from the FRS, as the note says. Please add it to UK: every open data item: uk-data parity, engine locks, the local surface, consumption and high-value homes #1095 with its size from DWP's exceptions table, so it doesn't stay only a note.
  5. CTBAND is still mapped silently (nit). frs_spine.py:1446 keeps _map_codes for CTBAND, so a code above 10 or below 1 becomes band D, where uk-data#636 refuses one outside the dictionary. The UK: every open data item: uk-data parity, engine locks, the local surface, consumption and high-value homes #1095 item names only the three codes, so this is about wording. The body and changelog say "FRS household codes outside the data dictionary refuse", which reads as all of them.
  6. DfE wording (nit). Only the universal measure reads the engine's eligibility (dfe_spring_term.py:204); early learning for 2-year-olds keeps its DfE-age gate (:210). The docstring says so, but the body's "the DfE spring-term measures read the engine's own eligibility" covers both.
  7. Single-year ages suppression (nit). single_year_ages.py:159 withholds a tail band that holds a withheld year, but not the sex's other years. So a withheld year can be recovered from the file's population by sex, which other reports give. This is unlikely to bite, since ages 0 to 89 all carry many households, but withholding the whole sex block when any single year is withheld would close it.

Checked and fine

  • Lock. policyengine-uk 2.161.0 in uv.lock and the three extras; uv.lock hashes to 97763f71…, the new pinned value. The coverage golden goes from 335 to 344 inputs. Each of the nine new ones (five child-limit exceptions, housing_benefit_has_main_phase_esa, severe_disability_premium_joint_occupier_exclusion_applies, housing_benefit_family_premium_category, would_claim_wales_childcare_offer) is in UK_ENGINE_DEFAULT_INPUTS or the take-up register with its error. No silent default.
  • FRS codes (uk-data#636). _map_dictionary_codes (frs_spine.py:1462) refuses a numeric code outside the table and names the variable. Blanks keep the same defaults as uk-data#636 (region unknown, private renter, detached house), and the maps match its 1, 2, 4 to 13 / 1 to 6 / 1 to 7 codes.
  • obr.domestic_rates. Pinned to the UK (generate_uk_target_references.py:573) and held (calibration_measure_exclusions.json:361). It now compiles with no signed difference from the incumbent's £494m. Against Land & Property Services' £0.89bn whole bill, that supports your district-only reading.
  • ORR. The selector reads 7271's all_sources support, Great Britain, FY2024-25 (£11.86bn against 7270's £21.62bn), and the row stays held. I couldn't open table 7271 itself.
  • Single adult discount cells. They share the national row's measurement, with England as the control and no region tier, and the subset declaration onto England's households is added. So the cells reconcile onto the national row rather than binding twice. The cells' references wait on the regeneration, as the body says. MHCLG's release was revised on 21 January 2026, which matches the national row's vintage. I couldn't check the 8,496,178 line 8 total against the ODS.
  • Winter Fuel and income tax. The compiled selector is fiscal_year 2025 with no window. No row still reads income_tax with the charge in: all 44 HMRC and OBR income tax uses read income_tax_before_winter_fuel_payment_charge_annual.
  • obr.vat. In 2.161.0, vat excludes the school-fee share, so vat + private_school_vat doesn't double count.
  • Grammar. max(<sum>, 0) refuses constants, products and a leading sign (target_materialization.py:1183). The loss-relief term is the same max(profit - relief, 0) as before.
  • The rest. Moving --communal-only into communal_residents.py is a move, not a deletion. The ONS mid-2025 file sums to 69.48m.

What I ran

  • gh auth status as vahid-ahmadi. Fresh worktree at 46e40709d, uv sync --all-packages --locked --extra uk.
  • Every touched engine-free test file plus test_uk_hmrc_income.py: 931 passed, 2 failed, 7 skipped. The failures are finding 1, and test_engine_is_reported_unavailable_without_the_uk_extra, which fails only because my venv has the uk extra.
  • test_uk_battery_bindings.py, test_uk_terminal_gates.py and test_uk_source_stages.py: only the disclosed test_local_default_target_surface_uses_metric_names_and_signed_deferrals fails (296 missing single adult cells).
  • The six touched engine-tier files, including test_concept_mapping_policyengine_uk.py: 42 passed. tools/ci_test_plan.py verify: ok.
  • CI at 46e40709d: lint, wheels, integration-uk and select-countries pass; engine-free and engine-uk fail; engine-us was pending.
  • Not run or read: no build, spine or calibration. I didn't read the regenerated target_reference_membership.json, the parity receipt or target_references.json beyond the rows named above, or the 8,419-line ONS file beyond its total. I skimmed communal_residents.py and the CGT and bonus-anchor re-pins; I didn't check them line by line. The interim arm's figures are taken as reported.

Verdict

The code is in good shape for a draft. Fix item 1 now; items 2 to 4 before ready; the nits as you like. Nothing here needs a ruling from Vahid.

🤖 Generated with Claude Code

juaristi22 and others added 25 commits October 26, 2026 11:43
The spine maps GVTREGNO, PTENTYP2 and TYPEACC through their data-dictionary
tables and now stops on a code outside them, naming the FRS variable. Blank
values keep their defaults. The 2023-24 and 2024-25 household tabs carry no
such code, so the spine does not change.

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

uc_receives_new_claimant_health_element, the three regulation 77 company
stocks, the two intermediaries inputs and benunit_reported_capital join
UK_ENGINE_DEFAULT_INPUTS, each with the error it can make (microcosm#1095).
The texts await María's signature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/compare_uk_council_tax_liability.py reads simulated_council_tax,
simulated_council_tax_less_discounts and the stored council_tax on a dense
release, and writes totals and ratios by nation and region, and means and
ratios by local authority and band where ten or more distinct source
households carry the cell (microcosm#1095, #953, #736). Cells give no totals
or weighted counts, so no published difference gives a withheld cell back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The incumbent evaluation measures each local age row against the mid-year
estimate and against that estimate less the area's census communal share for
the band (María's ruling of 2026-10-25, microcosm#1095). The census
residence-type computation moves into uk_runtime.communal_residents, which the
target-surface receipt now reads too; its output is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OBR's table 4.1 Northern Ireland domestic rates row is stamped K02000001, so
the substring rule's Northern Ireland pin compiled no fact. The row now pins
to the UK, as obr.council_tax does; domestic_rates is zero outside Northern
Ireland. The row looks like the district rate only, and domestic_rates is the
whole bill, so a measure exclusion holds it as a diagnostic (María's ruling
of 2026-10-25, microcosm#1095, uk-data#520). The references and membership
regenerate with the bump's shared regeneration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
María's rulings of 2026-10-25 (microcosm#1095): mortgage_debt stays all
secured debt, and an engine test now fails if a policyengine-uk formula reads
it beside a property's secured debt, which would count the nested debt twice;
statutory paternity pay stays annualised at 52 weeks, with the error noted
where the spine sets it, for her signature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
María's basis ruling of 2026-10-25 (microcosm#890 G, microcosm#1095): the row
reads table 7271's net operational support from all sources, not table
7270's broad total, which adds investment no household's rail use carries.
It stays held as a measured diagnostic, since bound on 7271 it failed the
weight-ratio gate on #1383's arm 2c; its expiry does not move. The national
references and membership regenerate with the bump's shared regeneration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UK lock moves from 2.151.0 to 2.160.0 (policyengine-core stays at
3.33.3). The uprating pins, their dataset projection and the bonus sacrifice
anchor follow 2.155.0's OBR population vintage; the concept coverage, the
cited versions and APPROVED_UV_LOCK_SHA256 re-derive. The Childcare Offer
for Wales take-up flag stays at its default, recorded with its error, until
Chronicle carries the Offer's take-up (María's ruling of 2026-10-25,
microcosm#1095), and the three new Housing Benefit inputs are recorded at
their defaults. The coverage manifest and the H2 fixture regenerate with the
bump's shared regeneration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine-uk 2.157.0 adds the charge to income_tax. The OBR income tax
row, the HMRC income tax rows and the HMRC taxpayer filters now read
income_tax_before_winter_fuel_payment_charge_annual, a new annual measure
(María's ruling of 2026-10-25, microcosm#1095): HMRC's statistics predate
the charge, and the OBR window's receipts hold almost none of 2025's
recovery. The Winter Fuel row's notes record that its column is now gross.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From policyengine-uk 2.157.1 the engine charges VAT on private school fees as
private_school_vat beside its general vat (pe-uk#2319), and OBR's receipts
include it, so the row's value_expression is vat + private_school_vat, as
obr.esa and obr.pip sum their parts. The row stays held (María's ruling of
2026-10-24), its notes no longer quote model figures, and the consumption-tax
diagnostic sums both parts (microcosm#1095).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The engine's England-outside-London yield now takes off only BUS01c's older
and disabled concessionary journeys, so both stages' fare pricing declare that
category (dft_bus_journeys.json vendors it) and the engine parity test holds
again (microcosm#1095). The support pricing's concessionary denominator is
unchanged. The spine moves; the head arm measures it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since policyengine-uk 2.156.1 the engine starts the universal hours in the
term after the third birthday, so its eligibility for a child still before
school in the spring term is the spring-term eligibility DfE counts. The
universal measure reads it instead of its own DfE-age gate, the engine test
follows the engine's new payment to a child born after 6 October, and the
resolver's stub supplies the new input list (microcosm#1095).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2.161.0 changes only the two-child limit: it models the limit's exceptions
as five person-level inputs that default to false, which the build leaves at
their defaults, recorded with their errors (microcosm#1095). The uprating
pins and the bonus anchor keep their values; their stamps, the concept
coverage, the cited versions and APPROVED_UV_LOCK_SHA256 re-derive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A local-authority row on MHCLG's Council Taxbase line 8, all bands (the
line_08 spec's total), England only (microcosm#1095). Its measurement is
the national row's, so the England row controls the cells by exact
signature with no region tier, and the cross-grain declarations add the
cells' subset relation to England's households. The metric
council_tax/single_adult_discount is appended after the energy metrics,
under the append-only rule. The census lists the metric; its regenerated
JSON also carries the HMRC filter text of the Winter Fuel commit. The
cells take the support-floor deferral on the Isles of Scilly and the City
of London, as the band rows do.

The local references, the membership and the cells' in-year hold land
with the regenerated surfaces; the hold needs María's signature first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1299 left the profit after losses brought forward, and total income
with it, as two measures the UK resolver computed by name (its
deviation L3), with the grammar change as a follow-up (microcosm#1095).
The value-expression grammar gains one function, max(<sum>, 0), and the
shared module evaluates named terms: measures declared once as
expressions, with nested terms expanded. hmrc_income declares the two
terms, and the stage helper evaluates the same declaration.

The resolver's four loss-relief branches give way to one named-term
route. A term reads all its leaves from one source: the frame's columns
at the frame's own period when the frame stores every leaf, the engine
otherwise. Its _annual form evaluates on the annual income simulation.
The bindings keep their names, so a term still costs one resolution
round. New tests hold the floats bit for bit against the former
formulas, at the frame's period and at another one, on the current and
the annual simulation.

One value moves: at the frame's own period, on a frame without a
loss_relief column, the profit after the relief now reads the engine's
float32 copy of the stored profit instead of the column. Production reads
both terms at the calibration year, never at the frame's period.

The measurement-period receipt reads a value expression with the
interpreter's parser; its split on "+" read a subtraction as one name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
uk-data#600 checks each build's single years of age against ONS, which
nothing here measured (microcosm#1095). A new vendored resource holds
ONS's mid-2025 UK persons by sex and single year of age (MYE2, the 182
rows of the edition of 1 October 2026, from the pinned feed); the SPI
stage's mid-2023 resource is untouched. uk_runtime.single_year_ages and
tools/report_uk_single_year_ages.py report a built file against it, by
single year and in the bands the age_tail stage draws, report-only.
Cells under 10 source households read "<10", and so do the bands that
hold one, so no difference gives a withheld age back.

The resource comes from a cut of the pinned feed to its mid-year
estimate rows, because the vendor tool loads a whole artifact into
memory; the cut reproduces the committed mid-2023 resource byte for
byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The national half of the regeneration (microcosm#1095), on the abf0921
pin. obr.domestic_rates now compiles, pinned to the UK, so 1,337
references are active and four have no fact at or before the period;
the ORR row reads table 7271's support from all sources. The
compile-parity receipt also takes policyengine-uk 2.160.0's per-capita
indices, which move the uprated SPI savings interest and dividend band
rows. The release-input coverage manifest, the H2 spine fixture and five
mirrored gate digests follow the branch. The local references, membership
and parity regenerate with the final rebase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Five national rows from HMRC's Child Benefit Statistics of August 2025
(Table 2, United Kingdom): families in payment with one, two, three,
four, and five or more children, the last HMRC's five-to-nine and
ten-or-more columns together (microcosm#1095). Only Child Benefit
spending bound before, so nothing held the claiming families' size.

Each row counts the benefit units whose child_benefit is above zero by
their children on policyengine-uk's own definition: the new resolver
measure child_benefit_child_count sums
is_child_or_qualifying_young_person_for_child_benefit over each unit.
The rows carry an in-year snapshot hold, which María signed on
2026-10-25, and compile to HMRC's published counts. The references,
membership and compile-parity receipt are regenerated: 1,342 active
references.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From 2.155.0 the engine takes population growth from the OBR's March
2026 determinants, so the OBR per-capita GDP path that the
uk_cgt_projection_entrants gate pins moves: 2025 from 4.38 to 4.74
percent, and 2023, 2024, 2028 and 2029 beyond the gate's 0.0005 drift
(microcosm#1095). The head arm's calibration refused on the old pins, as
the gate intends. The pins, the gate's note, the two tests that state the
path and five mirrored gate digests follow the locked engine; the exempt
amounts are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
María's ruling of 2026-10-26 (microcosm#1095): obr.winter_fuel_allowance
takes OBR's FY2025-26 fact instead of the calendar-2025 window of
FY2024-25 and FY2025-26, an exception to her window rule of 2026-09-22.
The payment falls in November and December, so calendar 2025 holds the
2025-26 winter, when every eligible pensioner is paid and the charge
recovers the payment above GBP 35,000, as policyengine-uk models it from
2.157.0. The window mixed in a quarter of FY2024-25, when only pensioners
on Pension Credit or another means-tested benefit were paid, and left the
row outside 10 percent on the head arm. The references, membership and
compile-parity receipt are regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The newest release on PyPI. From 2.162.2 the engine's
savings_interest_income is the interest outside tax-free accounts: a file
with no ISA income contract is read as gross, and the engine takes
tax_free_savings_income out of it when it loads the file. The frame still
writes gross interest, so the HMRC SPI interest amounts by income band now
measure the interest outside ISAs, as SPI Table 3.7 counts it, and the ONS
interest diagnostic adds the ISA part back. individual_savings_account_
interest_income has a formula from 2.162.2, so the concept mapping no
longer binds it.

The 13 new inputs stay at their defaults, recorded with their errors for
María's signature: Northern Ireland's Energy Price Guarantee inputs
(2.161.1), and the ISA dividend and non-cash ISA interest inputs, the
Lifetime ISA's flows and the two purchase flags (2.162.2).

The uprating pins, the bonus sacrifice anchor and the CGT projection
gate's growth path do not move from 2.161.0. Two engine tests pinned the
factors from before 2.155.0's population vintage (per-capita mixed income
and the savings interest target path) and now pin the engine's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vahid's review of microcosm#1400 (7): the age report withheld a band that
holds a withheld year, but not the sex's other years, so a withheld year
could come back as the sex's persons, which other reports give, less its
shown years. When any single year of a sex rests on fewer source households
than the floor, every cell of that sex now reads "<10", its bands too
(microcosm#1095).

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

Vahid's review of microcosm#1400 (3, 5). The five Child Benefit family rows'
notes record that HMRC's August 2025 counts (6.87m families, about 11.73m
children in payment) imply about £13.61bn at the calendar-2025 window, about
1.8 percent more than obr.child_benefit's £13.37bn; both rows bind, so the
solve splits the gap. The changelog of the FRS code refusals says the council
tax band keeps its mapping: a code outside bands A to I still takes band D.

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

The re-lock removed the binding of individual_savings_account_interest_income,
which 2.162.2 computes with a formula, so it is no engine input to bind; the
release frame writes its own tax_free_savings_income. The engine-free test
still expected the ISA share; it now checks that the mapping writes all
interest to savings_interest_income and binds no ISA part (microcosm#1095).
No caller supplies the uk.isa_interest_fraction share, so no build changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…count cells' signed hold

The rebase onto main brings #1381's feed pin (272c987). The national and
local references and membership, the local census, both compile-parity
receipts, the release-input coverage manifest, the H2 fixture and the gate
digests are regenerated on that pin, from #1381's national and local-levels
cuts; ons_mid2025_single_year_age_by_sex.json is vendored again from it
(rows unchanged, provenance moved).

The single adult discount cells by billing authority (microcosm#1095) now
compile, with María's local in-year hold (signed 2026-10-25). Their one
operand, line 8's total, makes each cell take the latest return across its
authority's codes, as the band cells do: Barnsley and Sheffield carry their
earlier returns under their former codes, which a plain selector reads as
two facts.

The reference counts move to 1,347 active (+ 1 obr.domestic_rates, + 5
Child Benefit family rows on #1381's 1,341), in test_country_spec.py (Vahid's
review of microcosm#1400, 1) and test_uk_national_calibration.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@juaristi22 juaristi22 changed the title Lock policyengine-uk 2.161.0 and take the actionable #1095 items Lock policyengine-uk 2.162.3 and take the actionable #1095 items Oct 26, 2026
@juaristi22

Copy link
Copy Markdown
Collaborator Author

@vahid-ahmadi thanks. Round 1 is addressed at 005ae0a8a. Since your review, María merged #1381, so the branch is rebased on main and every derived file is regenerated on the 272c987 pin. It also re-locks policyengine-uk 2.162.3, the newest release, so there is new code to look at (below).

  1. Reference count (must). Fixed: test_country_spec.py asserts 1,347, with the + 1 (obr.domestic_rates) and + 5 (Child Benefit families) lines in the comment. The rebase made a trap worth knowing: Write policyengine-uk's alcohol, tobacco and gambling duty inputs from the LCFS stage #1381 adds five national rows and this branch adds five, so several count lines merged silently to the same wrong number. I recomputed each from the merged contract (446 targets, 386 in registry scope, 203 unmapped) rather than trusting the merge.
  2. Engine-uk (should). The failures were the two pinned factors in test_uk_hmrc_uprating.py: per-capita mixed income (now 1.0384) and the savings-interest target path (now 1.1961). Both were pinned before 2.155.0 took population from the OBR's March 2026 determinants (Take population growth from the OBR's March 2026 long-term determinants, and re-derive the per-capita series from it policyengine-uk#2611), and both now pin the engine's value. The touched engine-tier files pass locally on 2.162.3 (concept mapping, uprating, CGT projection and DfE: 37 tests), and CI runs the whole tier on this push.
  3. Child Benefit against the spending row (should). Recorded in the five rows' notes: HMRC's counts imply about £13.61bn at the calendar-2025 window, about 1.8 percent above obr.child_benefit's £13.37bn, and both bind. I checked your arithmetic with the vendored children in payment (11.73m) and got the same. The final arm is running; I will give both residuals when it ends, as classes, since these rows rest in part on the SPI support channel (SN 9422).
  4. Two-child-limit exceptions (should, UK: every open data item: uk-data parity, engine locks, the local surface, consumption and high-value homes #1095). Added to the tracker with DWP's April 2025 counts (#1095 comment): 26,300 of the 469,780 affected households had an exception, two thirds of them for multiple births.
  5. CTBAND wording (nit). The body and the changelog now name the three codes and say CTBAND keeps its mapping (a code outside A to I still takes band D).
  6. DfE wording (nit). The body now says only the universal measure reads the engine's eligibility; the measure for 2-year-olds keeps its DfE-age gate.
  7. Single-year ages (nit). Done: when any single year of a sex is withheld, the whole sex block reads "<10", its bands too. The test builds one sex with every year above the floor and one with a single thin year.

New since round 1 (please review):

  • policyengine-uk 2.162.3. 2.162.2 changes what savings_interest_income means: it is now the interest outside tax-free accounts. A file with no ISA income contract (ours) is read as gross, and the engine takes tax_free_savings_income out of it when it loads the file. The frame keeps writing gross interest. The calibration reads value columns through the engine at 2025, so the SPI Table 3.7 interest amounts by band now measure interest outside ISAs, which is SPI's concept (through 2.161.0 they measured gross). Filters read stored frame columns, so the count row's savings_interest_income > 0 filter is unchanged. The ONS D.41 diagnostic now reads savings_interest_income + tax_free_savings_income, to keep measuring all cash interest. individual_savings_account_interest_income has a formula from 2.162.2, so the concept mapping no longer binds it.
  • The 13 new inputs (NI Energy Price Guarantee, ISA dividends and non-cash ISA interest, the Lifetime ISA flows and two purchase flags) are recorded at their defaults with their errors, for María's signature.
  • The A4 local hold (signed 2026-10-25) and the local references, membership and parity receipt land with the regeneration. The single adult discount cells needed one change to compile: Barnsley and Sheffield carry their earlier returns under their former codes (E08000016, E08000019), so a plain selector matched two facts. Line 8's total is now the cells' one linear_combination operand, so each cell takes the latest return across its authority's codes, as the band cells do. On your open check: the 296 billing-authority cells of the 2025 return (294 bound, plus the Isles of Scilly and the City of London, which the support floor defers) sum exactly to the England total of 8,496,178.

Final arm: a spine and a calibration at 005ae0a8a, with #1381 and 2.162.3, are running now (control b2572-a3c, #1383's head on 2.151.0, before #1381). I will post the results when they end.

What I ran: every engine-free test file (431, in chunks of 30). Each failure is fixed and its file re-run, except six that fail only on this machine: four need the Orrery Node package, one needs policyengine-us, and one fails because the uk extra is installed.

🤖 Generated with Claude Code

@vahid-ahmadi

vahid-ahmadi commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

@juaristi22 round 2, at 005ae0a8a.

All seven round 1 items are dealt with, and the count and factor checks hold. Engine-uk is still red, now for a new reason the 2.162.3 re-lock brings in: one must, and the fix is a single line. The ISA change is read correctly, but it leaves the SPI interest count on a different basis from its amount rows.

Round 1 status

  1. Reference count (must): fixed. test_country_spec.py:1193 and test_uk_national_calibration.py:69 assert 1,347, and the comment has both lines. I recomputed from the merged contract: target_references.json holds 1,347, uk_population_targets.json has 446 targets, and registry_parity gives 386 in scope and 203 unmapped declarations. Thanks for catching the silent merge of the count lines.
  2. Engine-uk (should): the old cause is fixed, but CI is red for a new one (new item 1 below). The two re-pins are the engine's values, not numbers chosen to pass. hmrc_uprating_engine_pins.json, which the tool writes from the engine, gives 1.0619 / 1.02263 = 1.0384 for per-capita mixed income, and 1.14200 x 1.04740 = 1.1961 for the savings path, the same as test_uk_hmrc_uprating.py:20 and :100. Main's 2.151.0 pins give 1.0344 and 1.1920, and population growth for 2025 moves from 0.0072 to 0.003. policyengine-uk#2611 merged on 24 October and shipped in 2.155.0 (CHANGELOG). The CGT gate's 2025 growth rate moves from 0.0438 to 0.0474 (gates.json), which is the same per-capita GDP change. The pins had already moved at round 1's 2.161.0 lock; only the tests were stale.
  3. Child Benefit against the spending row (should): recorded in all five rows' notes (uk_population_targets.json:30031 onwards). I'm waiting for both residuals from the final arm.
  4. Two-child-limit exceptions (should): filed on UK: every open data item: uk-data parity, engine locks, the local surface, consumption and high-value homes #1095 (comment) with DWP's April 2025 counts and a four-step plan. Done.
  5. CTBAND wording (nit): fixed in the body and changelog.d/1095-frs-household-codes-refuse-unknown.fixed.md.
  6. DfE wording (nit): fixed in the body.
  7. Single-year ages (nit): fixed. single_year_ages.py:153 now withholds the whole block for a sex, bands included, and the test covers both cases.

Our miss in round 1: through 2.161.0 the SPI Table 3.7 amount rows bound gross interest, ISA included, against a target that excludes ISA interest. That was already on main, and I didn't notice it. The re-lock fixes it.

New findings

  1. The HBAI subtraction list drifted, so engine-uk is red (must). CI at 005ae0a8a shows 1 failure and 106 passes on engine-uk: test_uk_hbai_gross_income_engine.py::test_gross_income_deductions_are_the_engines_hbai_subtracts. 2.162.x adds isa_cash_interest_flat_rate_charge to HBAI_HOUSEHOLD_NET_INCOME_SUBTRACTS, and UK_HBAI_GROSS_INCOME_DEDUCTIONS (household_income.py:33) doesn't have it. I reproduced it locally on 2.162.3. The test exists to catch exactly this. The new charge is a tax, like income_tax, so it should go in the deductions list rather than the not-added-back one. As the test docstring says, the stages' materialize_rules_engine_predictors need to move with it. The charge is zero before 2027-28 (in_effect), so no build-year figure changes. This wasn't in the "new since round 1" list.
  2. The SPI interest count is on a different basis from its amount rows (should, or a UK: every open data item: uk-data parity, engine locks, the local surface, consumption and high-value homes #1095 follow-up). Filters read stored frame columns (target_materialization.py:712), so the count row's savings_interest_income > 0 (uk_population_targets.json:21571) tests gross interest. Someone with ISA interest and nothing else is counted. Table 3.7 counts recipients of taxable interest, and from this re-lock the amount rows measure interest outside ISAs. The dedicated HMRC path already counts on the taxable basis (hmrc_calibration.py:260, where component is gross less tax_free_savings_income). Your "the filter is unchanged" is accurate, but the filter was already on the wrong basis. Please either filter on interest outside ISAs, or note the gap on the row if filters can't take an expression.
  3. The frame's interest basis rests on a default the engine doesn't promise (should). Neither UKSingleYearDataset the build makes (policyengine_uk.py:214, hmrc_calibration.py:454) declares an ISA income contract. 2.162.3 reads a missing contract as legacy_gross and takes tax_free_savings_income off (data/isa_income.py, resolve_contracts). If a later release changed the missing-contract default to additive, ISA interest would count twice in income tax and the D.41 row, and no test would fail. No test pins how the engine reads the frame's interest. Please pass the legacy contract explicitly, and add one engine test: a person with gross 1,000 and ISA 300 reads 700 at the frame's year. I checked that this holds on 2.162.3 on a two-person toy.
  4. ISA growth goes into the taxable part (nit). Under legacy_gross the engine projects gross interest and subtracts a flat tax_free_savings_income, which has no uprating. In my toy, 1,000 gross with 300 ISA at 2024 reads 747.4 at 2025, where 700 x 1.0474 = 733.2. So the 2025 SPI amount measure slightly overstates interest outside ISAs, by the ISA share times 4.7%. It's a small error and better than 2.161.0. Declaring the additive contract and writing ordinary interest would remove it. That could be part of item 3 or noted for later.
  5. The ISA detail register note overclaims (nit). policyengine_uk_concepts.py:362-368 says the zero non_cash_isa_cash_interest "changes nothing". From 2027-28 it drives isa_cash_interest_flat_rate_charge, so projections from that year will show no charge. Please say so in the note.

Checked and fine

  • ISA reading. In 2.162.3 a dataset without a contract resolves to legacy_gross, and the loader converts it to ordinary interest. market_income, capital_income and HBAI add tax_free_savings_income back, so income totals don't change. Income tax gives the same result as before. Value columns read at 2025 go through the engine, because a stored column holds only the frame's own period (ledger_targets.py:1817). So your account of the amount rows is right. The ONS D.41 expression adds the ISA part back, and the row stays held (calibration_measure_exclusions.json:253). The HMRC path still reads the frame's gross and takes the ISA part off once.
  • 13 new inputs. All are in UK_ENGINE_DEFAULT_INPUTS. The golden goes from 344 to 356 inputs (13 added, and ISA interest becomes formula-owned), and covered inputs go from 42 to 41. The engine's defaults match the notes: kWh -1, ni_epg_full_rate_applies false. uk.isa_interest_fraction has no remaining reference.
  • Single adult discount cells. There are 294 cells. Barnsley and Sheffield each select both their old and new codes through the one linear_combination operand. The subset declaration onto the English regions and the local hold (signed 25 October) are in place. I still can't check the 8,496,178 total, because the cell values live in the feed, not the repo.
  • Holds and digests. The two new uprating_holds.json entries (local discount cells, national Child Benefit rows) are in-year snapshots with expiry dates. The gate digests move with the gates.json CGT re-pin.

What I ran

  • gh auth status as vahid-ahmadi. Fresh worktree at 005ae0a8a, uv sync --all-packages --locked --extra uk (policyengine-uk 2.162.3).
  • Every engine-free test file the PR touches, plus test_uk_hmrc_income.py, test_uk_terminal_gates.py and test_uk_source_stages.py: 1,188 passed, 1 failed, 7 skipped. The failure is test_engine_is_reported_unavailable_without_the_uk_extra, which fails only because my venv has the uk extra.
  • The six engine-tier files the PR touches, plus test_concept_mapping_policyengine_uk.py: 45 passed. test_policyengine_uk_adapter.py: passed. test_uk_hbai_gross_income_engine.py: 1 failed (new item 1).
  • A two-person toy dataset through Microsimulation on 2.162.3 for items 3 and 4.
  • CI at 005ae0a8a: lint, wheels, integration-uk and select-countries pass. Engine-uk fails on new item 1. Engine-free and engine-us pass (both Pythons), so new item 1 is the only red.
  • Not run or read: no build, spine or calibration. I used a range-diff against round 1: the CT-C tool, private-household rows, obr.vat, bus fares, DfE, loss relief, Winter Fuel and the CGT re-pin are unchanged since round 1 apart from the rebase, so I didn't re-review them. I only skimmed the regenerated local_target_reference_membership.json (135k lines), local_target_references.json, the two compile-parity receipts, release_input_coverage_manifest.json, the H2 fixture and the re-vendored ONS file. I'm taking the final arm's figures as you'll report them.

Verdict

Close. Fix item 1 so engine-uk goes green. Items 2 and 3 should be done before ready, or filed on #1095 if you'd rather keep this PR to the lock. The nits are up to you. Keep it as a draft until CI is green and the final arm's Child Benefit residuals are posted. Nothing here needs a ruling from Vahid.

🤖 Generated with Claude Code

juaristi22 and others added 3 commits October 26, 2026 15:50
Vahid's review of #1400, round 2 (1). policyengine-uk 2.162.2 adds
isa_cash_interest_flat_rate_charge, the final charge on the cash interest in
non-cash ISAs, to HBAI_HOUSEHOLD_NET_INCOME_SUBTRACTS, so the engine-tier
test that holds UK_HBAI_GROSS_INCOME_DEDUCTIONS to the engine's list failed.
The charge is a tax, like income tax, so the survey-concept gross income
adds it back; the three stages that materialize the deductions (LCFS
consumption, NTS bus travel, ETB VAT) declare it too. It is zero before
2027-28, so no build-year figure moves. source_stages.json and the
release-input coverage manifest are regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vahid's review of #1400, round 2 (3, 5). The frame stores
savings_interest_income gross, ISA interest included, with the ISA part in
tax_free_savings_income. policyengine-uk 2.162.2 reads a dataset that
declares no ISA income contract as legacy_gross and takes the ISA part off
when it loads it, but that is a default, not a promise. The build now
declares the contract: UK_ISA_INCOME_CONTRACT goes into both
UKSingleYearDatasets it makes (the UK adapter and the HMRC materialization),
and every H5 it writes carries it in the root attribute
policyengine_uk.isa_income_contract, beside energy_price_basis, so the
measure resolver's loads declare it too. A new engine test reads gross
interest of 1,000 with 300 in an ISA as 700 at the frame's year, through
the adapter and through a written H5.

The note on non_cash_isa_cash_interest now says that, held at zero, it
leaves projections from 2027-28 without the final charge on that interest.
The H2 spine fixture is regenerated: the adapter is part of several stages'
implementation hashes, and three stages declare a new predictor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vahid's review of #1400, round 2 (2, 4). SPI Table 3.7 counts the
recipients of taxable interest, and from the 2.162.3 lock its amount rows
measure interest outside ISAs, but the count rows' filter read the frame's
stored gross savings_interest_income, so a person whose only interest is
ISA interest was counted. The filter now reads
savings_interest_income_outside_isas, a resolver measure of the stored
gross interest less its tax_free_savings_income, read at the frame as a
filter's stored columns are; a persisted column of that name is refused.
The amount row's notes record that, under the legacy-gross contract, the
engine grows the gross interest and takes off the stored ISA part
unchanged, so the measure overstates interest outside ISAs a little; the
additive contract would remove that (microcosm#1095).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
María's ruling of 2026-10-26 (microcosm#1095). The gambling participation
gate (uk_stage_lcfs_consumption_gambling_participation, microcosm#1359)
bounds the scale that brings the added regular players' drawn stakes to
the seen players' mean, so that a broken stake draw the anchoring would
hide still fails. #1381 set the bound at [0.5, 2.0] on stage-only runs
over an older spine, where the National Lottery's scale was 1.82 to 1.86.
Full spine builds put it at about 2.03, on main after #1381 and on this
branch alike, so every full UK spine stopped at the gate. The upper bound
is now 2.5: a draw more than 2.5 times off the seen players' mean still
fails. The battery's mirrored digests are re-pinned.

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

Copy link
Copy Markdown
Collaborator Author

@vahid-ahmadi thanks. Round 2 is addressed at 3c0832f5b; the head is now b7f089d69, with the gambling bound below.

  1. HBAI subtraction list (must). isa_cash_interest_flat_rate_charge joins UK_HBAI_GROSS_INCOME_DEDUCTIONS, after income_tax, as a tax. The three stages that declare the deductions in materialize_rules_engine_predictors (lcfs_consumption, nts_bus_travel, etb_vat) list it too, and source_stages.json, the coverage manifest and the H2 fixture are regenerated. test_uk_hbai_gross_income_engine.py passes on 2.162.3. The charge is zero before 2027-28, so no build-year figure moves.
  2. SPI interest count basis (should). Done in this PR rather than filed. The count rows now filter on savings_interest_income_outside_isas > 0: a resolver measure that reads the frame's stored gross savings_interest_income less its tax_free_savings_income, at the frame, as other filters read stored columns (measure_simulation.py, beside the high-value-home measures; a persisted column of that name is refused). Someone whose only interest is ISA interest is no longer counted. An engine-free test covers the measure, its route and the row's filter. The final arm measures the change.
  3. Explicit contract (should). The frame now declares legacy_gross itself. UK_ISA_INCOME_CONTRACT (in the UK adapter) goes into both UKSingleYearDatasets the build makes (policyengine_uk.py, hmrc_calibration.py), and every H5 the build writes carries it in the root attribute policyengine_uk.isa_income_contract, beside energy_price_basis. The measure resolver loads the H5, so its path is covered too. The new engine test test_uk_isa_income_contract.py builds your case (gross 1,000 with ISA 300, plus an ISA-only saver) and reads 700 and 0 at the frame's year, through the adapter and through a written H5 that declares the contract.
  4. ISA growth (nit). Noted on the amount row: under legacy_gross the engine grows the gross interest and takes off the stored ISA part unchanged, so the 2025 measure overstates interest outside ISAs a little, by about the ISA share times the year's interest growth. Moving the frame to the additive contract with ordinary interest would remove it, but it changes what the frame stores for every stage that reads gross interest, so I left it for UK: every open data item: uk-data parity, engine locks, the local surface, consumption and high-value homes #1095 (note).
  5. ISA detail note (nit). Fixed: the note now says the zero non_cash_isa_cash_interest changes nothing until 2027-28, and from then means projections show no charge.

The final arm is still to come. Its spine stopped at #1381's gambling gate (uk_stage_lcfs_consumption_gambling_participation): the National Lottery amount_scale is about 2.03 against the bound [0.5, 2.0]. A control on main after #1381 stops at the same gate, so it is not this PR: #1381 set the bound on stage-only runs over an older spine (1.82 to 1.86), and no full spine at its head had run. On María's ruling, this PR raises the upper bound to 2.5 (b7f089d69); a draw more than 2.5 times off the seen players' mean still fails. Until this PR merges, every full UK spine on main stops at that gate. The arm reruns now, and I will post its results with both Child Benefit residuals as classes.

What I ran: the engine tests for the touched surfaces (the new contract test, the HBAI test, the UK adapter and the concept mapping: 18 tests), and the engine-free files that read the changed modules, contract rows, stage declarations or fixtures (158 files, in chunks, 6,698 tests): all pass except two that fail only on this machine, one because the uk extra is installed and one because the Orrery Node package is missing.

🤖 Generated with Claude Code

CI's engine-uk failed on test_the_engine_relabels_course_dated_plans_by_age_at_2025,
which pinned policyengine-uk's old projection: it relabelled every holder by
a course started at 18 and wrote some loans off, where the stage dates a
course from the age its borrower left full-time education. The test was
written to fail once the engine kept the stage's dating, so that the SLC
rows are re-measured. policyengine-uk 2.162.2 does that
(PolicyEngine/policyengine-uk#2636): each holder keeps the recorded plan
and repayment. The test now pins that, and an engine that starts to
relabel again fails it. The final arm on this lock measures the SLC rows
(microcosm#1095).

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

Copy link
Copy Markdown
Contributor

@juaristi22 round 3, at a5408836f.

All five round 2 items are done, and done well. There is one new must: the ISA contract writer is an HDF write site the serializer registry doesn't know, so engine-free goes red. It is a one-entry fix. The gambling bound change is reasonable, but it needs one more number to be justified rather than just passing.

Round 2 status

  1. HBAI subtraction list (must): fixed. household_income.py:40, and the three stages in sources.yaml and source_stages.json. test_uk_hbai_gross_income_engine.py passes locally on 2.162.3.
  2. SPI interest count basis (should): fixed. savings_interest_outside_isas (measure_simulation.py:262-277) is stored gross less tax_free_savings_income, floored at zero, routed at :523 and refused as a persisted column at :1028. The count row filters on it (uk_population_targets.json:21571), with a note at :21584. Reading at the frame is right for a sign filter.
  3. Explicit contract (should): fixed. UK_ISA_INCOME_CONTRACT (policyengine_uk.py:46) goes into the adapter's dataset (:230), the HMRC dataset (hmrc_calibration.py:457) and every H5 through _write_uk_single_year_tables (national_frame.py:527). test_uk_isa_income_contract.py passes. See new item 1 for the registry.
  4. ISA growth (nit): noted on the amount row and on UK: every open data item: uk-data parity, engine locks, the local surface, consumption and high-value homes #1095. Fine as a follow-up.
  5. ISA detail note (nit): fixed (policyengine_uk_concepts.py:365-372).

Our miss in round 2: I wrote that the HBAI test was "the only red" on engine-uk. It wasn't proven. That job runs with --maxfail=1, so it stopped at the HBAI test and never reached test_uk_student_loans.py, which was also red on 2.162.3 (the engine stopped relabelling plans in 2.162.2). I've now checked this: the 005ae0a8a version of that test fails on 2.162.3. I didn't run it in round 2 because the PR hadn't touched it. You found it and fixed it in a5408836f. The new test passes, and no source note still describes the relabelling.

New findings

  1. The ISA contract writer isn't in the HDF serializer registry (must). test_frame_serializer_registry.py::test_registry_classifies_every_writable_production_hdf_site fails at a5408836f. The extra site is national_frame.py::_write_isa_income_contract. It needs an HdfWriteExclusion like uk_energy_price_basis_root_attr (frame_serializer_registry.py:222-231): "adds the ISA income contract root attribute only". The test is in engine_free/shared, so your run of the files that read the changed modules wouldn't have picked it up.
  2. The gambling bound (should): justified in direction, not yet in size. First, our part: the [0.5, 2] bound was my round 4 nit on Write policyengine-uk's alcohol, tobacco and gambling duty inputs from the LCFS stage #1381 ("say [0.5, 2]"), based on one receipt at 1.82. So 2.0 had no stronger basis than that, and a full spine at 2.03 failing it is our miscalibration, not a broken draw. Raising it is fair. But "2.5 because 2.03 fails 2.0" is a bound tuned to pass, and the receipt can't show why the scale moved. It records only the ratio (gambling_participation.py:785), not the seen households' mean (:737) or the added regular players' drawn mean (:743). Between the stage-only runs and the full spine, did the diary-seen mean rise, or did the draws fall? Only the second points to the draw. Please add both means to the receipt, or post them from the final arm for both runs. The comment at :733-736 explains why the anchor is the seen mean, but not why the seen mean would be about twice the added players' draws. One sentence on that would help, for example if the two-week diary mostly sees heavy players. One more point: main's spine is blocked until this PR merges. A small PR to main with the bound (gates.json, the digests in contract.py and test_contract.py, the lcfs fixture and the changelog) would unblock it sooner, if Lock policyengine-uk 2.162.3 and take the actionable #1095 items #1400 has to wait for the final arm.
  3. The contract test doesn't exercise the projection the build uses (nit). Both tests build a 2025 frame and read it at 2025, but the build's frame is 2024 (frs_release.json:9) and is read at 2025. I wrote a 2024 frame through write_uk_national_frame and read it with Microsimulation at 2025 on 2.162.3. It gives 747.4 for the 1,000/300 saver and 14.22 for the ISA-only saver. So the amount rows still give an ISA-only saver about 4.7% of their ISA interest as taxable interest, while the count row now excludes them. That is round 2 item 4 seen from the count side. It's small, and it goes away with the UK: every open data item: uk-data parity, engine locks, the local surface, consumption and high-value homes #1095 move to additive. Pinning those two values in a third test would make that follow-up show up as a test change. (The adapter's materialize at 2025 from a 2024 frame gives 700 and 0 with no growth, because it stamps fiscal_year=period (policyengine_uk.py:229). That's existing behaviour, not this PR's.)

Checked and fine

  • Student loan pin. The test now asserts the stage's plans and repayments survive the 2025 projection. That matches the changelog line. The SLC rows' move will show in the final arm.
  • Digests. The gate battery policy, manifest and spec fingerprints move together in contract.py and test_contract.py, and so do the spine certification digests. test_contract.py passes.
  • test_uk_measurement_period.py swaps savings_interest_income for the new measure in the reviewed sign-filter set. That's right, because a sign survives uprating.

What I ran

  • gh auth status as vahid-ahmadi. A fresh worktree at a5408836f, uv sync --all-packages --locked --extra uk (policyengine-uk 2.162.3).
  • test_frame_serializer_registry.py, test_uk_isa_income_contract.py, test_uk_student_loans.py, test_uk_hbai_gross_income_engine.py, test_uk_measure_simulation.py, test_uk_measurement_period.py, test_contract.py (data), test_policyengine_uk_adapter.py and test_concept_mapping_policyengine_uk.py: 391 passed, 1 failed (new item 1).
  • A two-person toy through a written 2024 H5 and Microsimulation for item 3.
  • CI at a5408836f: lint, wheels, integration-uk and select-countries pass. Engine-free, engine-uk and engine-us were still running when I posted. Engine-free will fail on new item 1, which I reproduced locally.
  • Not run or read: no build, spine or calibration. I read the full diff from 005ae0a8a, except the regenerated release_input_coverage_manifest.json, the H2 fixture and uk_spine.json, which I only skimmed. I can't check the 2.03 figure, because it comes from a spine run.

Verdict

Close. Add the registry entry (item 1) so engine-free stays green, and give the scale's two means (item 2) before this is marked ready. Item 3 is up to you. Keep it as a draft until CI is green (engine-uk was still running, so I can't yet say the student loan fix is its last red) and the final arm's Child Benefit residuals are posted. Nothing here needs a ruling from Vahid. The bound itself was María's call, and I agree with the direction.

🤖 Generated with Claude Code

juaristi22 and others added 3 commits October 26, 2026 17:41
Vahid's review of #1400, round 3 (1). national_frame's
_write_isa_income_contract adds a root attribute to every H5 the build
writes, and the serializer registry classifies every writable production
HDF site, so test_registry_classifies_every_writable_production_hdf_site
failed. It joins the exclusions beside the energy price basis writer: it
adds the ISA income contract root attribute only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vahid's review of #1400, round 3 (2). The receipt gave only the scale
that brings the added regular players' drawn stakes to the seen
households' mean, so a moved scale could not show which side moved. It
now also gives the anchor's mean stake, the added regular players' drawn
mean and the stake model's drawn mean for the households the diary saw;
the last separates a level gap between the two draws from the walk's
selection of households. They are means over the same households as the
key's shipped entry, which rests on ten or more source households in
every cell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vahid's review of #1400, round 3 (3). The build's frame is FRS 2024-25 and
is read at 2025. Under the legacy-gross contract the engine grows the
gross interest and takes off the stored ISA part unchanged, so a saver
with 1,000 gross and 300 in an ISA reads 747.4, and an ISA-only saver
keeps 14.22 as interest outside ISAs. The test pins both, so the #1095
move to the additive contract shows up as a test change.

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

Copy link
Copy Markdown
Collaborator Author

Final arm for #1400: one spine and one calibration at b7f089d69 (the head's code; later commits change only tests and the gambling receipt's fields), on #1381's feed (272c987) and policyengine-uk 2.162.3, review date 2026-10-25. Control: b2572-a3c, #1383's head on 2.151.0, before #1381. Local, licensed runs; their outputs stay with the spines.

  • Gates: all seven terminal gates pass, on both arms.
  • Fit: the share of rows within 10 percent goes from 99.2 percent of 1,268 to 99.0 percent of 1,273, and the final loss from 0.0041 to 0.0046. The effective sample size and the spread of the weights stay where they were.
  • Child Benefit (round 1, item 3): the five families-in-payment rows and obr.child_benefit are all within 10 percent. The solve meets the counts closely and carries the counts' gap on the spending row, which sits above its target. These rows rest in part on the SPI support channel, so I give classes, not figures (SN 9422).
  • Winter Fuel at FY2025-26: within 10 percent.
  • Rows newly outside 10 percent (7): two SPI interest count bands (£20,000 and £40,000), which now count only the recipients of interest outside ISAs (round 2, item 2); three SPI regional cells; one HMRC employment-income count band; and SLC's Parents' Learning Allowance recipients.
  • Rows newly inside 10 percent (4): DWP's UC households with two children, one HMRC employment-income band and the two HMRC State Pension rows for £30,000 to £40,000.

The control predates #1381, so the differences mix this PR's changes with #1381's. A control on main after #1381 stopped at the gambling gate, which this PR raises.

🤖 Generated with Claude Code

@juaristi22

Copy link
Copy Markdown
Collaborator Author

@vahid-ahmadi thanks. Round 3 is addressed at 6c31d2787, and the final arm's results are in the comment above.

  1. Serializer registry (must). _write_isa_income_contract joins the HDF write exclusions beside the energy price basis writer, as "adds the ISA income contract root attribute only" (59dd2e317). test_frame_serializer_registry.py passes. Thanks for the --maxfail=1 point on round 2; the student loan pin was the second red behind the HBAI one.
  2. The gambling scale's size (should). The receipt now gives the scale's terms (be5fcde2c): the anchor's mean stake, the added regular players' drawn mean, and the stake model's drawn mean for the households the diary saw. The last one separates a level gap between the two draws from the walk's selection: if the model draws about the same for the seen households as for the added ones, the gap is between the diary-based stakes and the stake model, not in who the walk picks. I will run the stage alone on Lock policyengine-uk 2.145.0, follow its bus fares and bind thin target rows #1333's spine (the run Write policyengine-uk's alcohol, tobacco and gambling duty inputs from the LCFS stage #1381 measured at 1.82) and on this arm's spine, and post the three means for both, as relative changes, once the machine is free. The bound stays in this PR: María hopes to merge it soon, which unblocks main's full spines.
  3. The projection the build uses (nit). Pinned (6c31d2787): a 2024 frame written through write_uk_national_frame and read at 2025 gives 747.4 for the 1,000/300 saver and 14.22 for the ISA-only saver, so the move to additive on UK: every open data item: uk-data parity, engine locks, the local surface, consumption and high-value homes #1095 will show up as a test change.

🤖 Generated with Claude Code

@juaristi22

Copy link
Copy Markdown
Collaborator Author

@vahid-ahmadi the gambling scale's terms, as promised (round 3, item 2). I ran lcfs_consumption alone, as declared at 6c31d2787 (the receipt now carries the terms), once on #1333's spine (the spine #1381 measured on) and once on this PR's final-arm spine. Relative figures only.

The scale barely moved between the two spines. Run alone, the National Lottery's scale is 1.86 on #1333's spine and 1.83 on the final-arm spine. From one spine to the other, the anchor (the seen households' mean stake) moves by -1.0 percent, the added regular players' drawn mean by +0.3 percent, and the stake model's drawn mean for the seen households by -4.3 percent. Betting and gaming moves from 0.64 to 0.61.

What makes the lottery's scale about 2: a level gap between the two draws, not the walk's selection. On both spines, the stake model draws nearly the same mean for the households the diary saw as for the added regular players (the ratio is 1.06, then 1.01). The diary-based stakes of those same seen households average 1.75 to 1.81 times the model's draws for them. So the walk is not picking light players; the stake model draws below the diary-based stake level throughout. That is most likely the forest smoothing the right tail of the stakes, which the diary's heavier players carry. For betting and gaming, the model draws above the diary-based level for the seen households, and higher again for the added players, as the code comment at gambling_participation.py:733-736 expects.

Why 2.0 failed: the stage run alone on a finished spine reads the scale about a tenth lower than the same stage inside the full build that made that spine: 1.83 alone against 2.02 in the final arm's own build, with no change to the stage between the two. #1381 set the bound on stage-only runs, where 1.82 to 1.86 left room under 2.0; full builds sit at 2.02 to 2.03, on main and here alike. The 2.5 bound leaves room for that difference, and a draw more than 2.5 times off the anchor still fails. From this PR on, every build's receipt carries the three terms, so the next build shows them for the in-build run too.

🤖 Generated with Claude Code

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

@juaristi22 round 4, at 6c31d2787.

All three round 3 items are addressed. No new musts or shoulds, two small nits. The only thing still owed is the two-run comparison of the scale's terms, and I no longer think it should hold this PR back (item 2).

Round 3 status

  1. Serializer registry (must): fixed. uk_isa_income_contract_root_attr (frame_serializer_registry.py:233). test_frame_serializer_registry.py passes locally.
  2. The gambling scale's size (should): receipt done, comparison still to come. The receipt now records the anchor, the added regular players' drawn mean and the stake model's drawn mean for the seen households (gambling_participation.py:787-801), and the engine-free test checks that the scale is the ratio of the first two (test_uk_gambling_participation.py:348-359). The third term is a good addition: it separates a level gap between the diary stakes and the stake model from the walk's selection, which my two terms could not. The 1.82 vs 2.03 comparison is still to be posted. In round 3 I asked for it before this is marked ready. I'm softening that: any future move in the scale now explains itself in the receipt, we agreed on the direction, and main's spines are waiting on the bound. So it can follow as a comment after this PR, not before.
  3. The projection the build uses (nit): fixed. test_uk_isa_income_contract.py:89-105 pins 747.4 and 14.22 for a 2024 frame read at 2025.

The final arm

New findings

  1. The pinned values follow the engine's uprating index, not only the contract (nit). 747.4 and 14.22 are 1,000 × 1.0474 − 300 and 300 × 0.0474, using 2.162.3's growth to 2025 for savings_interest_income. So the test will also change on any pe-uk bump that moves that index, not only on the UK: every open data item: uk-data parity, engine locks, the local surface, consumption and high-value homes #1095 move to additive. Reading the index from the parameters and asserting 300 × (index − 1) would make it fail only for the reason the docstring gives. I asked for the pin, so this is a refinement of my own ask.
  2. The receipt now carries pound amounts (nit, a question). The three terms are mean stakes in pounds, where the stage's other receipt figures are shares and probabilities. Each cell passes the 10-household floor (_ships, :586-600, applied at :770), and the donor anchor needs 10 or more donor players (:666). So I think this is within CD171. But lcfs_consumption.py:4083 says a donor stage receipt "gives no donor value", and the stake model is trained on LCF stakes (:566). Please confirm the shipped receipt is fine with pound levels. If you'd rather keep it unit-free, ratios to the donor's positive mean would still compare across runs, since both runs use the same donor. Your plan to post relative changes in the comment is right either way. I asked for "both means" without saying levels or ratios, so that is on me.

Checked and fine

  • The refactor keeps the scale the same. added_mean (:742-747) is the same weighted mean as before, and scale is the same ratio. The commits since b7f089d69 change only tests, the registry entry and receipt fields (I diffed b7f089d69..6c31d2787). So the final arm's code is the head's code, as you say.

What I ran

  • gh auth status as vahid-ahmadi. A fresh worktree at 6c31d2787, uv sync --all-packages --locked --extra uk (policyengine-uk 2.162.3).
  • test_frame_serializer_registry.py, test_uk_gambling_participation.py, test_uk_isa_income_contract.py, test_uk_stage_health.py and test_contract.py (data): 411 passed.
  • CI at 6c31d2787: lint, wheels, integration-uk, select-countries, both engine-uk jobs and engine-free (3.14) pass. Engine-free (3.13) and both engine-us jobs were still running when I posted. Engine-uk passing on both Pythons confirms the student loan pin was its last red.
  • Not run: no build, spine or calibration. I can't check the final arm's figures or the 2.03, because they come from spine runs.

Verdict

Ready to come out of draft once engine-free (3.13) finishes green. Item 2's comparison can follow as a comment, and both nits are up to you. Nothing here needs a ruling from Vahid.

🤖 Generated with Claude Code

@juaristi22
juaristi22 marked this pull request as ready for review October 26, 2026 18:49
@juaristi22
juaristi22 merged commit 5810fce into main Oct 26, 2026
10 checks passed
juaristi22 added a commit that referenced this pull request Oct 26, 2026
The rebase onto main after the policyengine-uk bump (microcosm#1400) took
main's generated files where they conflicted. These are regenerated from a
cut of the 272c987 artifact with the rebased contract, the local-level rows
a local contract selector matches (306,418 rows). On main with the band-H
commit the same cut reproduces main's local references, membership and
parity receipt byte for byte. The local references and the data-target
parity register regenerate unchanged. Counts: 25,192 compiled cells, 29,325
references, 4,133 signed exclusions.

Co-Authored-By: Claude Opus 5.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.

2 participants