Skip to content

Budget 2026 ISA data, PR B of #1418: ISA subscriptions per adult, the Lifetime ISA anchor, additive ISA income, policyengine-uk 2.164.2 - #1421

Open
juaristi22 wants to merge 9 commits into
mainfrom
uk-isa-subscriptions-1415
Open

juaristi22 wants to merge 9 commits into
mainfrom
uk-isa-subscriptions-1415

Conversation

@juaristi22

@juaristi22 juaristi22 commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

PR B of #1418, after PR A (#1419), which has merged. Part of #1415; implements #1360.

Why

The UK build carried ISA balances but no subscriptions: household cash and stocks-and-shares ISA balances from was_wealth and person Lifetime ISA balances from was_lisa (#1003). With no subscription data every ISA-limit reform scored £0 on Microcosm, and so did policyengine-uk's Lifetime ISA bonus. The Lifetime ISA holders were also about 0.66 million against HMRC's 1.14 million accounts paid into in 2024-25.

What this changes

  • ISA subscriptions per adult. A new last spine stage, isa_subscriptions, does three things:

    • splits each household's cash and stocks-and-shares ISA balances across its adults, by a WAS round-8 donor household's holding pattern, with exact sums;
    • draws who paid into which type in 2024-25 and how much, from HMRC's 2026 ISA statistics;
    • draws Lifetime ISA payments for every holder aged 18 to 47 (the ages that can hold one in 2024-25) with room under the £20,000 limit.

    It adds five person columns: cash_isa_balance, stocks_and_shares_isa_balance, cash_isa_subscriptions, stocks_and_shares_isa_subscriptions and lifetime_isa_payments.

  • Lifetime ISA anchor. was_lisa shifts its ownership logit up on ages 18 to 47 until the holders at those ages who survive the financial-wealth cap equal HMRC's 2024-25 payers. It only adds holders. A target it cannot reach is named in the receipt instead of failing the stage.

  • Three stage gates.

    • uk_stage_isa_subscriptions_invariants fails on a non-finite value, a broken limit, age or holding rule, or person balances that miss the household totals. These are rules any population keeps, so the synthetic smoke build blocks on it.
    • uk_stage_isa_subscriptions fails on 2024-25, residual or Lifetime ISA totals off Table 9.4, as a population fact check. It names unmet subscriber shortfalls.
    • uk_stage_was_lisa_anchor fails when the anchor removes a holder or misses HMRC's payers. It names unshifted holders above the target.
    • Neither compares the calibrated file with HMRC's tables.
  • Additive ISA income (Export versioned additive UK ISA income and migrate calibration and projection together #1360). The producer stores interest outside ISAs in savings_interest_income and ISA interest in individual_savings_account_interest_income. It declares policyengine-uk's {"version": 1, "representation": "additive", "projection": "additive"} contract on every frame and H5, and no longer stores tax_free_savings_income. Loaded files keep their own contract: the enhanced-FRS reference stays legacy gross, and a build refuses legacy inputs.

  • Re-pin. The UK Chronicle feed moves to Add HMRC's 2026 ISA, Lifetime ISA and impact-note facts (PolicyEngine/microcosm#1415) chronicle#392's merge commit (0b721a4), which adds HMRC's ISA, Lifetime ISA and impact-note facts.

  • policyengine-uk 2.164.2. The lock moves from 2.162.14. 2.164.0 (policyengine-uk#2712) reads the five person columns: the subscriptions against its new ISA limits, and the balances, which its household cash_isa and stocks_and_shares_isa now sum. The release therefore stops storing those household totals, as for property_wealth. 2.164.1 moves the economic assumptions to the OBR's October 2026 EFO.

Method and limits: docs/uk-isa-subscriptions-1415.md. Lifetime ISA notes: docs/uk-lisa-1003.md.

Commits

  1. Runtime, not yet declared.
    • The typed ISA facts loader.
    • The subscriptions stage, the Lifetime ISA anchor and the vendored-facts declaration checks.
    • The reviewed fence: all 1,074 vendored ISA facts are measured only.
    • Stage-health checks and tests.
  2. Register.
    • The two gates.
    • The export columns and nine operation kinds with closed schema branches.
    • The contract mirrors and the digests they move.
  3. Export versioned additive UK ISA income and migrate calibration and projection together #1360.
    • The producer, the SPI channel, the HMRC 3.7 bridge, the outside-ISA measure, the writer and loader, the frame adapter and the registers.
    • Engine acceptance tests: identical base-year cash and tax in both layouts, the H5 round trip, legacy refusal, malformed markers, and projection by component.
  4. Chronicle re-pin to chronicle#392's head.
  5. Declare and wire the stage, with the anchor on was_lisa.
  6. The pin moves to Decouple the live latest-pointer loader test from which release is current #392's merge commit. Its tree is the head's, so only what restates or hashes the commit moves.
  7. policyengine-uk 2.164.2.
    • The lock, the version mentions and the vendored uprating and bonus-sacrifice pins.
    • The release boundary drops the household ISA totals.
    • The CGT projection gate's pinned growth path, re-pinned to the October path.
    • The concept coverage, the re-signed national compile-parity receipt, the coverage manifest and the H2 fixture.
  8. Anthony's review.
    • Cash grows to Table 9.4's level, holding the limit.
    • Lifetime ISA ages 18 to 47.
    • The NaN and naming guards, the faster cell walk, and the end-to-end, reordering and fence tests.
  9. Anthony's second round.
    • Amounts above £18,000 taper up to the limit.
    • The invariants gate is split from the totals gate.
    • The paying weight, and the fence test on concepts.

Decisions (María, 28 October)

  • Measure, bind none. Binding the cells the stage draws to would be circular, so no ISA fact enters the weight solve. How far the calibrated file sits from HMRC's tables is reported and never blocks a build.
  • Table 9.7's people, the stocks-and-shares residual. Table 9.7's bands imply more money than Table 9.4 records, so 9.4 supplies growth ratios only. The both class's stocks-and-shares total is 9.4's amount less the stocks-and-shares-only payments.
  • People grow by accounts. This is an upper bound on people, since from 6 April 2024 a person may pay into several ISAs of one type. The flat-people bound is reported.
  • Lifetime ISA anchor and payments here, with the next pair build as the evidence.

Decisions (María, 29 October, on Anthony's review)

  • Cash grows to Table 9.4's 2024-25 level, holding the £20,000 limit.
    • Growth by 9.4's ratio carried Table 9.7's cash excess forward and scaled everyone below the limit by about 2.05: £127bn against £95.6bn, with 51% of cash subscribers above £12,000.
    • Stocks and shares come to the same amount under both rules.
  • Amounts taper to the limit above £18,000. One factor of 0.89 below the limit left nobody between about £17,750 and £20,000. Combined amounts above £18,000 (Table 9.7's last band floor) now move on a line up to the limit. On HMRC's tables that gives a cash factor of 0.878, with 36.9% of cash subscribers above £12,000 and 28.8% at the limit.
  • The Lifetime ISA anchor's reach is counted on the pair. Three later stages read the whole frame and count a Lifetime ISA as capital. The pair's national counts, at its final weights, the benefit units whose UC or Pension Credit entitlement changes without the added holders' balances.

What moves

  • Base-year cash and tax are the same in both ISA income layouts (the engine acceptance tests). Predictors that read gross interest read both parts, and the SPI stage-2 draws keep their salts.
  • Projected years. Taxable interest loses an overstatement: policyengine-uk grew gross interest and then took off an ISA part held flat. ISA interest now grows by its own index.
  • The re-pin moves no compiled target. Both reference surfaces are byte-identical, and both compile-parity receipts are unchanged by it. Each membership moves its feed label, and the local one also its DESNZ keys. The 33 existing vendored resources keep their rows.
  • ISA limits. policyengine-uk 2.164.2 reads the subscriptions and the person balances, so a limit reform changes isa_subscriptions_above_limit and cash_isa_subscriptions_above_limit. No tax or spending variable reads those yet, so such a reform costs £0 until the engine models where the excess goes. The subscriptions aren't uprated. lifetime_isa_bonus reads the payments and reaches government spending.
  • The October 2026 EFO moves the engine's uprating pins:
    • average earnings at 2025 fall 1.1% and household interest income 6.1%, while GDP per head grows by the same 8.8% from 2023 to 2025;
    • the 13 SPI savings interest bands compile 0.25% lower, and the rows GDP per head uprates move by under 0.001%; the local surface reads no engine pin;
    • the annual-income earnings index, the weight-year population path and the bonus-sacrifice anchor (factor 1.1149 to 1.1123) move with them;
    • the CGT projection gate's GDP-per-head path is re-pinned (2025: 4.62% against 4.74%). The October path runs to 2031, one year past the gate's 2030 horizon, which this PR keeps.

Still to come

Verification

  • Touched tests run locally, and CI runs on main. The engine-UK suite also ran locally on 2.164.2 while this PR was stacked.
  • tools/run_integration_tests.sh: the synthetic spine runs both changed stages.
  • Regenerated:
    • the H2 parity fixture;
    • the release input coverage manifest;
    • source_stages.json, the typed projection of spec/sources.yaml.
  • The gate-battery and certification digests are recomputed.

Disclosure

The WAS round-8 tabs are UKDS-licensed (SN 7215). No WAS unit-record value, minimum, maximum or cell under 10 appears in the code, the docs or the receipts. Receipt counts go through receipt_counts.rounded_count. Every HMRC figure quoted is published.

🤖 Generated with Claude Code

juaristi22 and others added 5 commits October 29, 2026 05:21
The first piece of microcosm#1415, not yet declared in the manifest:

- uk_runtime/hmrc_isa_statistics.py types the vendored HMRC ISA facts
  (Tables 9.4, 9.6 and 9.7 to 9.9, the impact note, Lifetime ISA Table 1a),
  refusing a resource off the feed pin, a drifted band roster, or tables
  that no longer restate each other. It also checks a stage's
  verify_vendored_fact_resource declaration: the resource, artifact role
  and record sets, and the 2024 build period.
- uk_runtime/isa_subscriptions.py splits each household's cash and
  stocks-and-shares ISA totals across its adults by a WAS round-8 donor
  household's pattern (exact sums), rakes subscribers by type to Tables
  9.7, 9.8 and 9.9 and walks Table 9.7's bands, splits the both-type
  amounts by balance shares shifted to Table 9.4's stocks-and-shares
  residual, grows people by Table 9.4's account growth and amounts by its
  amount growth (capped at GBP 20,000), and draws Lifetime ISA payments to
  Table 9.4's amount within the overall limit.
- was_lisa gains anchor_lifetime_isa_ownership. When the stage declares
  it, the ownership logit shifts up on ages 18 to 47 until the holders aged
  18 to 49 who survive the financial-wealth cap equal HMRC's 2024-25
  payers. The shift never removes a holder, leaves holders already at or
  above the target as drawn, and names a target it cannot reach.
- The stage refuses a declaration whose fact fence differs from the
  reviewed one (classify_isa_facts_with_reviewed_fence): no ISA fact is a
  calibration target, since binding the cells the stage draws to would be
  circular.
- stage_health gains the isa_subscriptions and was_lisa_anchor checks.
  They fail on broken invariants only; shortfalls and saturations are
  reported.

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

For microcosm#1415, before the stage is declared:

- gates.json: uk_stage_isa_subscriptions and uk_stage_was_lisa_anchor
  (stage_health, transferred, release-blocking, population fact checks),
  the spine gate scope, the battery's two new parameter keys, the
  microcosm-data contract mirrors and every digest the edit moves (the
  battery's policy, manifest and fingerprint, the spine and release-cut
  part digests).
- The export surface allows the five person inputs policyengine-uk#2712
  defines or already reads: cash_isa_balance, stocks_and_shares_isa_balance,
  cash_isa_subscriptions, stocks_and_shares_isa_subscriptions and
  lifetime_isa_payments (both allow-lists, in lockstep).
- The source manifest's operation kinds and the sources schema gain nine
  new kinds, each a closed branch: the stage's seven operations, the
  Lifetime ISA anchor and the reviewed fence over the ISA facts.

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

microcosm#1360, folded into #1418's PR B:

- The FRS spine stores interest outside ISAs in savings_interest_income
  and ISA interest in individual_savings_account_interest_income.
  Non-ISA interest is gross interest less the ISA part, as policyengine-uk
  normalises the legacy layout, so base-year amounts match it.
  tax_free_savings_income, the engine's sum of the ISA parts, is never
  stored.
- Frames carry the ISA income contract in their metadata. In-process
  frames carry the producer's additive/additive; a loaded frame carries
  the file's declaration, and an unmarked H5 reads as legacy gross.
- The H5 writer, the row-wise local path, the graph export, the HMRC
  calibration dataset and the frame adapter all use the frame's own
  contract. The build refuses an input whose contract is not the
  producer's.
- The SPI channel keeps the stage-1 taxable draw as interest outside
  ISAs and the stage-2 ISA interest draw beside it. Stage 2 still trains
  on gross interest and keeps the legacy salt name, so no draw moves.
  The HMRC accounting identities, the recipient rule and the
  rank-transport analogue read interest outside ISAs directly.
- Predictors that read gross interest read the two parts together: the
  CGT income proxy, UC capital coherence, the UC reporter redraw and the
  SPI housing shell.
- The Table 3.7 bridge and the outside-ISA measure count non-ISA interest
  once under either contract. The legacy enhanced-FRS reference still
  measures gross interest less the ISA part.
- Registers:
  - the export surface allows the ISA interest column;
  - tax_free_savings_income is dropped as a reviewed exclusion, with an
    engine-derived exclusion in the coverage manifest and an entry in
    the input-mass register;
  - the battery, release-cut and input-mass digests are re-pinned;
  - the H2 parity fixture is regenerated.

Base-year cash and tax are identical between the layouts (new engine
tests). In projected years each part grows by its own index, instead of
gross interest growing with an unchanged ISA part taken off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For microcosm#1415, the pin names PolicyEngine/chronicle#392's head
(e56af9a). Its base is 272c987, the previous pin, so the artifact was
built from the head's bundle. When #392 merges, the pin moves to the
merge commit. The facts and manifest digests stay the same, and only the
files that restate the commit move.

- uk/chronicle_feed.json:
  - 1,506,587 rows, with 3,272 new facts from three HMRC sources: the ISA
    statistics, the Lifetime ISA statistics and the cash ISA limit
    reduction impact note;
  - the consumer-fact schema digest is unchanged;
  - the two tests restating the pin, the local validation levels and the
    local census follow.
- Vendored resources:
  - a new hmrc_isa_statistics_facts.json (1,074 rows in seven
    selections), registered in the country package;
  - the 33 other vendored resources keep their rows under the new feed
    identity;
  - the DESNZ constituency resource keeps its values, under keys
    re-derived from the new identity.
- References and receipts:
  - the national and local references and both compile-parity receipts
    are byte-identical;
  - each membership moves its feed label, and the local one also moves
    its DESNZ keys;
  - both surfaces were regenerated from cuts of the artifact, by the
    272c987 rules.
- Other files: the coverage manifest and the H2 fixture's 16 stage
  contract hashes follow, and the runbook gains a history entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For microcosm#1415:

- uk/spec/sources.yaml, and source_stages.json (its typed projection),
  declare isa_subscriptions as the last stage. It reads the vendored ISA
  facts and the WAS household and person tabs, and has ten operations:
  - the facts check;
  - the donor;
  - the balance split;
  - the subscriber walk;
  - the amounts;
  - the both-class split;
  - growth;
  - Lifetime ISA payments;
  - the mass receipt;
  - the reviewed fence.
  It writes five non-negative person outputs, with notes.
- was_lisa gains the facts check and the anchor to Table 9.4's 2024-25
  payers.
- Graph and build wiring:
  - the graph gets the stage's reads, cells and WAS sources;
  - the kernel binds the stage's module and its helpers: the CGT walk and
    income proxy, the HMRC income helpers, the typed ISA facts and the
    receipt rounding. The facts loader is also bound to was_lisa;
  - the spine build gets the stage's WAS inputs, its implementation and
    its identity-keyed seeds.
- lifetime_isa_payments leaves the engine-default register, because the
  stage now draws it.
- The private-school attendance's notes, error and stage-order test no
  longer call it the spine's last stage.
- Fixtures and tests:
  - the H2 fixture's WAS person donor carries the two ISA values;
  - the fixture, the coverage manifest family and the stage-roster tests
    follow;
  - an invalidation test binds the facts loader to both stages.
- docs/uk-isa-subscriptions-1415.md, the Lifetime ISA notes and the
  changelog fragments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
juaristi22 and others added 2 commits October 29, 2026 10:37
PolicyEngine/chronicle#392 merged as 0b721a4. Its tree is the head's
(e56af9a), merged onto the unmoved base 272c987, so the artifact is the
same bytes: the row count and the facts and manifest digests are
unchanged. Only what restates or hashes the commit moves:

- uk/chronicle_feed.json and the two tests that restate it;
- the vendored resources' feed headers, re-vendored with their rows
  unchanged;
- the DESNZ constituency resource, whose keys are re-derived from the
  feed identity, and the local membership's keys for it;
- both memberships' feed labels, the local census and the local
  validation levels;
- the coverage manifest's conditioning-resource digest and the H2
  fixture's stage contract hashes.

The local references, regenerated from the local cut, are byte-identical,
and neither compile-parity receipt names the commit. The runbook's
history entry now names the merge commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For microcosm#1415, the UK lock moves from policyengine-uk 2.162.14 to
2.164.2:

- 2.164.0 (policyengine-uk#2712) reads the isa_subscriptions stage's
  person columns: the cash and stocks-and-shares subscriptions, against
  its new ISA limits, and the per-person balances.
- Its household cash_isa and stocks_and_shares_isa now sum those
  balances, so the release boundary drops the stored household totals. A
  stored total would override the sum, and a reform or situation that
  edits a person's balance would leave it unchanged. The spine keeps both.
- 2.164.1 moves the economic assumptions to the OBR's October 2026 EFO:
  - the vendored uprating pins move: average earnings at 2025 -1.1% and
    household interest income -6.1%, with GDP per head from 2023 to 2025
    growing by the same 8.8%;
  - the 13 SPI savings interest bands compile 0.25% lower, so the
    national compile-parity receipt is re-signed; the local surface reads
    no engine pin;
  - the bonus-sacrifice anchor's earnings and population growth move
    (factor 1.1149 to 1.1123);
  - the CGT projection gate's pinned GDP-per-head path moves to the
    October path, a reviewed re-pin as at the 2.161.0 lock. Its horizon
    stays 2030, though the path now runs to 2031;
  - the engine-number tests (the uprating factors, the SPI savings
    rebase, the CGT path) record the October values beside the old ones.
- Concept coverage drops the two household ISA totals as inputs and gains
  the four person ISA columns and Housing Benefit's pre-assessed
  permitted-work disregard, left at its default. The liquid-assets note
  names the person ISA balances.
- The version mentions, the US worker identity's lock digest, the
  coverage manifest, source_stages.json, the H2 fixture and the ISA
  income test's 2025 projection (GDP per head 1.0462 to 2025) follow. The
  ISA and Lifetime ISA docs say what 2.164.2 reads and which means tests
  count each balance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@juaristi22 juaristi22 changed the title Budget 2026 ISA data, PR B of #1418: ISA subscriptions per adult, the Lifetime ISA anchor, additive ISA income Budget 2026 ISA data, PR B of #1418: ISA subscriptions per adult, the Lifetime ISA anchor, additive ISA income, policyengine-uk 2.164.2 Oct 29, 2026
@anth-volk

Copy link
Copy Markdown
Contributor

Review of #1421 at b89d3aab8

This is an adversarial review of the PR's head. It focused on three things: where the ISA changes sit in the build order, how they interact with calibration, and which checks would stop implausible values from being published. Each finding below was re-checked in the code. Sizes marked as estimates come from the reviewer's probes, not from a real build. Paths are relative to packages/microcosm-build/src/microcosm/build/uk_runtime/ unless stated.

This PR still targets uk-budget-2026-static-fixes-1418, which merged in #1419. CI only runs on PRs into main, so it has never run here. Retargeting to main would run it.

Most important

  1. Cash subscriptions carry Table 9.7's excess into 2024-25, and the £20,000 limit concentrates them at the top.

    • The growth step (isa_subscriptions.py:1406-1426) sets each type's 2024-25 target to the model's own 2023-24 total times Table 9.4's amount growth.
    • The 2023-24 cash total comes from Table 9.7's bands. The method doc already notes that those come to about £92.5bn against Table 9.4's £69.5bn. Growth by ratio keeps that 33% excess: about £127bn against HMRC's £95.6bn for 2024-25.
    • Subscribers already at £20,000 can't grow, so everyone else grows faster. On populations built from the vendored Table 9.7 bands, the stage's own scale_to_amount_growth gives (estimate):
      • a cash factor of about 2.05;
      • about 51% of cash subscribers above £12,000 after growth, against 22% in HMRC's impact note;
      • about 40% of all subscribers ending at the overall limit.
    • That share drives the £12,000 cash-limit measure directly.
    • The stage reports impact_note_under_65_cash_share_over_12000 next to the published 22% (isa_subscriptions.py:1506), but no check reads it. The growth check (stage_health.py:4716-4728) compares the result only with that self-made target. The check's notes call it a check against Table 9.4's targets.
    • Suggestion: grow to Table 9.4's 2024-25 level rather than its growth ratio, or add a release-blocking bound on the cash total against Table 9.4 and on the share above £12,000 against the impact note.
  2. ISA-limit reforms produce excess amounts, but no revenue yet. In policyengine-uk 2.164.2, isa_subscriptions_above_limit and cash_isa_subscriptions_above_limit appear only in their own definitions and in programs.yaml. No tax or spending variable reads them, so a limit reform still costs £0. lifetime_isa_bonus does reach government spending. The subscription inputs also have no uprating, so a 2027-28 reform reads 2024-25 nominal amounts. This is engine scope rather than a defect here, but "ISA-limit reforms score" in the description and changelog could say that.

  3. The Lifetime ISA ownership shift reaches later benefit stages, and the change is unmeasured.

    • The shift in was_lisa (stage 20) adds about 0.48m holders.
    • policyengine-uk 2.164.2 counts Lifetime ISA balances as capital through lifetime_isa_countable_capital (UC: parameters/gov/dwp/universal_credit/means_test/capital/person_sources.yaml:30; Housing Benefit and JSA likewise).
    • Three later stages compute entitlement through the engine on the whole dataset: uc_reporter_redraw, uc_take_up and pension_credit_take_up. graph.py:375-382 declares them with no fixed input list. So the doc's "the graph's declared cells show nothing else moved" can't cover them.
    • Added holders can change UC and Pension Credit awards, and so the pools those take-up draws choose from. Benefit units with a stored uc_reported_capital are shielded.
    • Separately, the financial-wealth cap scales every Lifetime ISA balance in a household pro rata, so existing holders' balances can fall when a holder is added.
  4. Nothing yet measures the ISA columns after calibration. The stage matches its targets at the weights before calibration. No code compares subscription cells, Lifetime ISA payers or the bonus with HMRC at the final weights; the PR leaves these to the next full build.

    • ISA facts stay out of the weight solve only because no target compiler reads hmrc_isa_statistics_facts.json. The only reader is hmrc_isa_statistics.py.
    • The calibration_permitted: false classification is compared with a constant rather than enforced where targets are compiled. That matches the CGT facts' pattern, but it is worth knowing.

Checks that let bad values through

  1. Unreached targets are reported, not failed.

    • Saturations in the both-type split and the Lifetime ISA total go to saturations_named without a failure (stage_health.py:4729, :4741).
    • In the ownership-shift check (stage_health.py:4801-4807), a saturated shift passes outright. With no shift, only a shortfall fails, so there is no upper bound. Probes passed with holders 91% below target (saturated) and 4.4 times above it (no shift).
  2. NaN amounts get past every violation count. The counts in isa_subscriptions.py:1485-1502 are >/< comparisons, and NaN compares false. No amount array is tested with np.isfinite; only the weights are (:530).

  3. The synthetic build never blocks on these checks.

    • Both new entries set population_fact_check: true. According to country_spec.py:464-468, that means a synthetic smoke build records the failure without blocking.
    • That's right for comparisons with HMRC's tables. But uk_stage_isa_subscriptions also holds rules that don't depend on the population: the £20,000 and £4,000 limits, the minimum age, holding the type paid into, negative amounts and household sums.
    • Suggestion: move those rules into an entry without the flag, so CI's synthetic build fails on them.
  4. Counts are reported only. Subscribers against Table 9.7 (shortfall_weight_unmet) and Lifetime ISA payer weight against Table 9.4's 1,136,000 accounts appear in the stage output report, but no check reads either.

Smaller points

  • The docstring overstates draw stability. It says draws are keyed so that adding rows moves none (isa_subscriptions.py:12). Reordering is invariant (largest change 1.5e-11), but in a probe adding one person changed the amounts of 521 of 1,633 others. Band walks and the growth factor depend on the whole population.
  • A counter's name says the opposite of what it counts. subscribers_below_their_balance_records (isa_subscriptions.py:1513) counts people whose subscription exceeds their balance.
  • Holders aged 48–49 count toward HMRC's payers and get payments. In 2024-25 nobody over 47 can hold a Lifetime ISA, since accounts open at 18 to 39 and began in April 2017. The base ownership model has no upper age bound. Probably small.
  • Households with no adult split adult-ISA balances over children. The count is reported as households_without_adult_split_over_all_persons (isa_subscriptions.py:666) but not checked.
  • The doc says every holder aged 18–49 pays in, but a holder with no room under the £20,000 limit pays £0.
  • Code:
    • The module is 1,679 lines.
    • It imports private helpers from cgt_imputation (_walk_bands, _truncated_exponential_quantile).
    • It uses single-letter names (w, c, s, f_cash).
    • The report keys are plain strings shared between the stage and its check.
    • select_subscribers loops np.flatnonzero(codes == cell) over up to 5,544 cells, six times a run.
  • Tests:
    • No test runs UKISASubscriptionsStageTransform.__call__ or the household-sum path from start to finish.
    • The determinism test runs identical input twice.
    • Nothing bounds the cash total or the share at the limit.
    • The test behind "identical base-year cash and tax in both layouts" uses two people and five measures, with no benefits or net income.

Checked and found true

  • Stage placement:
    • isa_subscriptions runs last (49 of 50) and writes only its five person columns.
    • Person balances sum to the household draws within £0.01. policyengine-uk sums and uprates them the same way as the stored totals, so means tests are unchanged in every year.
    • The release export and the scratch frames used for measure simulation drop the household cash_isa and stocks_and_shares_isa.
  • Random draws: the new draws use their own isa_subscriptions:* salts, declared in uk/spec/sources.yaml.
  • The additive ISA income layout:
    • The salt for drawing ISA interest in the second SPI stage is unchanged.
    • The training predictors still read gross interest.
    • The CGT income proxy and the UC and SPI predictor sets add ISA interest back.
    • The Table 3.7 comparison handles both layouts, and old-layout inputs are refused.
    • Base-year equality holds. At the 2025 calibration year, interest outside ISAs falls by about the ISA share times one year's growth, so calibrated weights will move.
  • Chronicle re-pin: all 2,526 records in desnz_constituency_energy_facts.json keep their values exactly. Only source_sha256 and the keys derived from it change, which re-sorts every record. The local membership changes only resolved_fact_key and the feed label.
  • policyengine-uk 2.164.2: in the re-signed national compile-parity file, 13 SPI savings rows move −0.25% and 13 rows move +0.0004%.
  • Conflicts with Budget 2026 slower data fixes, PR C of #1418: electricity at the ONS level, pension income by area #1420: all four (contract.py, its test, the coverage manifest and uk_spine.json) are digests or generated files. Both PRs edit gates.json, so the digests must be recomputed from the merged file, not taken from either side. No conflict in behaviour was found.
  • Deployment: no CI/CD, Docker or Terraform changes.

Tests run

On Python 3.14 with policyengine-uk 2.164.2:

  • 58 pass: ISA subscriptions, ISA stage health, the ownership shift, was_lisa and the engine ISA income contract.
  • 412 pass and 1 is skipped: SPI income, rank transport, SPI support, FRS, the end-of-build checks, measure simulation and source stages. One more fails for an environmental reason: it asserts the UK extra is not installed.
  • 41 engine tests pass: HMRC calibration, SPI income, CGT projection and HMRC uprating.

Not run:

  • whole suites;
  • tools/run_integration_tests.sh;
  • a real build (no licensed WAS or FRS data);
  • recompiling the reference targets;
  • the size of point 3's effect.

🤖 Generated with Claude Code

Anthony's review of microcosm#1421:

- Growth (María's ruling of 29 October 2026): amounts scale to Table 9.4's
  2024-25 amounts, not its growth ratio on the stage's own 2023-24 totals,
  and subscribers already at GBP 20,000 stay there. On populations built
  from HMRC's tables, ratio growth gave GBP 127bn of cash against 95.6bn,
  a cash factor of 2.05 and 51% of cash subscribers above GBP 12,000. The
  new rule gives a factor of about 0.89 and 37%. The growth check now
  compares with Table 9.4 itself.
- Lifetime ISA ages: holders are counted and paid at 18 to 47, the ages
  that can hold one in 2024-25 (opened at 18 to 39 from April 2017).
  lifetime_isa_paying_ages derives them, and both stages refuse others.
- Guards:
  - a non-finite amount or balance is a violation;
  - the misnamed counter is subscribers_above_their_balance_records;
  - unmet subscriber shortfalls and unshifted Lifetime ISA holders above
    the target are named, not failed, beside the saturations;
  - the gate details carry the payer weight against HMRC's accounts.
- select_subscribers groups its cells with one stable sort instead of a
  scan per cell, with the same rows in the same order. Single-letter names
  in the solver and the payment draw are spelled out.
- Tests:
  - the whole transform runs end to end within every invariant, and its
    gate passes;
  - reordering rows moves no draw;
  - the growth rule holds the limit both ways, and NaN is counted;
  - no calibration target reads an ISA fact;
  - the paying ages follow the 2017 opening;
  - the #1360 layouts also agree on Universal Credit and household net
    income.
- Docs, notes and changelog:
  - the anchor's reach into the three open-surface stages, which the pair
    counts (María's ruling);
  - the pro-rata cap on existing holders;
  - every holder with room pays;
  - an ISA limit's excess, which policyengine-uk 2.164.2 does not yet tax.
- source_stages.json, the coverage manifest, the H2 fixture and three gate
  digests follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@juaristi22
juaristi22 changed the base branch from uk-budget-2026-static-fixes-1418 to main October 29, 2026 15:49
@juaristi22

Copy link
Copy Markdown
Collaborator Author

Thanks, Anthony. Commit a8508d8 addresses this review, and the PR now targets main, so CI runs. It merges cleanly with main.

Most important

  1. Cash growth. Confirmed. A probe that builds populations from HMRC's tables alone (no microdata) through the stage's own solver gives your numbers: £127bn of cash against Table 9.4's £95.6bn, a cash factor of 2.05, 51% of cash subscribers above £12,000 and 40% of all subscribers at £20,000.
    • Ruling (María, 29 October): amounts now scale to Table 9.4's 2024-25 amounts, not its growth ratio, and subscribers already at £20,000 stay there. On the same populations that gives a factor of about 0.89, £95.6bn, 37% above £12,000 and 29% at the limit (28.8% in 2023-24). A plain factor below one would have moved every limit subscriber to about £19,100, leaving 7.6% at the limit.
    • The growth check now compares the totals with Table 9.4 itself.
    • The rest of the gap to the impact note's 22% comes from amounts not varying by age. The method doc records it.
    • The share above £12,000 stays a reported check, not a release-blocking bound, under the measure-only ruling. The pair build compares it after calibration.
  2. Limit reforms raise no revenue yet. Agreed. The description, the changelog and the method doc now say that a limit changes isa_subscriptions_above_limit and cash_isa_subscriptions_above_limit but no tax or spending variable reads them in 2.164.2, and that the subscriptions are flows the engine does not uprate. The Lifetime ISA bonus does reach government spending.
  3. The Lifetime ISA anchor's reach. Agreed: the doc's "declared cells show nothing else moved" was wrong for uc_reporter_redraw, uc_take_up and pension_credit_take_up.
    • Ruling (María, 29 October): the pair's national counts, at its final weights, the benefit units whose UC or Pension Credit entitlement changes without the added holders' balances, and the units that move in or out of the take-up pools. No A/B spine.
    • The docs and the stage notes now say this, and that where the financial-wealth cap binds, an added holder scales the household's existing balances down pro rata.
  4. After calibration. Yes, that measurement is the pair build's. test_no_calibration_target_reads_an_isa_fact now holds the contract and both reference surfaces free of the ISA record sets. The fence is enforced in CI, not just compared with a constant.

Checks

  1. Unreached targets stay non-blocking under María's ruling of 28 October: drift is reported, never gated. Unshifted holders above the target are now named beside the saturations.

  2. NaN. Fixed. non_finite_values counts any non-finite amount or balance, and the gate fails on it. There's a test.

  3. The synthetic build. I didn't split the entry. Instead a new test runs UKISASubscriptionsStageTransform.__call__ end to end on a synthetic 2024 frame. It fails CI on:

    • any violation the receipt counts;
    • a household-sum error above £0.01;
    • a non-finite or negative output;
    • the stage gate failing.

    The comparisons with HMRC's tables stay a population fact check.

  4. Counts. An unmet subscriber shortfall is now named in the gate details, and the details carry the Lifetime ISA payer weight beside HMRC's 1,136,000 accounts. Neither fails the gate.

Smaller points

  • Docstring: corrected. Row order moves no draw. Adding rows can move others' amounts, because the rakes, band walks and growth factors read the whole population.

  • Counter: renamed subscribers_above_their_balance_records.

  • Ages 48 and 49: fixed. lifetime_isa_paying_ages derives 18 to 47 for 2024-25 (opened at 18 to 39 from April 2017), and the anchor's counted ages and the payment draw use it. Both stages refuse other ages. The base ownership model (UK: impute LISA ownership and balances from WAS; document the annual-contribution gap #1003) still has no upper age; its older holders keep their balances but are neither counted nor paid. Changing that model is out of scope here.

  • Households with no adult: still a reported count. Their balances come from was_wealth, so leaving them out would change the household totals. The pair report shows how many there are.

  • "Every holder pays in": now "every holder with room under the overall limit".

  • Code:

    • select_subscribers groups its cells with one stable sort, keeping the same rows in the same order.
    • The single-letter names in the solver and the payment draw are spelled out.
    • The private CGT helpers are shared on purpose, so the walk isn't duplicated. Renaming them would re-key the CGT stages, so that waits for a CGT change.
    • Shared constants for the report keys and splitting the module are noted for later.
  • Tests:

    The cash total is now matched to Table 9.4 by construction, and test_the_imputation_keeps_every_invariant asserts it.

🤖 Generated with Claude Code

@anth-volk

Copy link
Copy Markdown
Contributor

Re-review of #1421 at a8508d837

Thanks for the quick turnaround. This re-review checks each earlier finding against a8508d837 and your reply, then looks for new problems in the new code. The main new findings were re-checked in the code. Paths are relative to packages/microcosm-build/src/microcosm/build/uk_runtime/ unless they start with packages/ or docs/.

CI has not run on a8508d837. There are no workflow runs or check runs for this commit. The push (15:49:20 UTC) arrived while the base was still the feature branch, so the pull_request: branches: [main] filter skipped it. The retarget (15:49:34) is an edited event, which the workflow doesn't trigger on. A new push, or closing and reopening the PR, will start it. main has no branch protection, so GitHub shows the PR as mergeable now, without CI.

Earlier findings

Finding Status Evidence
Cash carried Table 9.7's excess, and too many subscribers sat at £20,000 Resolved for the total and the limit share; partly for the share above £12,000 On populations built from HMRC's tables, scale_to_build_year_amounts gives a cash factor of 0.889, cash of £95.58bn (equal to Table 9.4), stocks and shares of £37.20bn, and 29.1% of subscribers at £20,000 (28.8% in 2023-24). 36.9% of cash subscribers are above £12,000, against the impact note's 22%, and that share is still only reported.
Limit reforms raise no revenue Resolved in the docs The changelog, method doc and stage notes now say so.
The Lifetime ISA ownership shift reaches the UC and Pension Credit stages Acknowledged, not measured The docs are corrected; nothing yet computes the effect. The shift now counts ages 18–47, so it adds slightly more holders than at b89d3aab8.
Nothing measures the ISA columns after calibration Deferred The checks the method doc lists for the next full build's national file have no implementation yet. The new static test has a gap (new finding 1).
Unreached targets reported only; no upper bound on holders Declined (ruling of 28 Oct) A saturated shift 91% short still passes. Holders at 4.4 times the target with no shift still pass, now named anchor:unshifted_holders_above_target.
NaN passes the violation counts Resolved A probe gives non_finite_values: 1, and the check fails on any non-zero count.
The synthetic build never blocks Partly resolved A new test runs __call__ end to end, but the synthetic build now always fails this entry (new finding 2).
Counts reported only Declined, detail added The new payer figure counts holders, not people who pay in (new finding 3).
Draw-stability docstring Resolved Reordering rows changes amounts by at most 1.6e-11. Adding one row moves 424 of 1,633 others, and the docstring now says so.
Counter name Resolved Now subscribers_above_their_balance_records.
Holders aged 48–49 Resolved lifetime_isa_paying_ages(2024) == (18, 47), and both specs refuse other ages.
Households with no adult Reasonably declined Still reported.
"Every holder pays in" Resolved Reworded.
Code and tests Mostly resolved The stable-sort grouping keeps row order (the permutation test passes). One single-letter w is left at isa_subscriptions.py:1016. The base-year equality test now gives a non-trivial UC (£401.68 in both layouts).
Base branch Resolved The PR targets main and merges cleanly with 846b48a9c. 699 tests pass on the merged tree, including the contract digest checks.

New findings

  1. test_no_calibration_target_reads_an_isa_fact doesn't check the identifier that targets select by.

    • The test (packages/microcosm-build/tests/engine_free/uk/test_uk_isa_subscriptions.py:524-547) searches three target files for the record-set names hmrc.isa_statistics_2026, hmrc.lisa_statistics_2026 and hmrc.cash_isa_limit_reduction_2026.
    • Targets select facts by source_concept. The ISA facts' concepts are hmrc.isa_subscribers, hmrc.isa_amount_subscribed, hmrc.isa_holders, hmrc.isa_market_value, hmrc.isa_accounts_subscribed, hmrc.cash_isa_subscribers_share and three hmrc.lisa_* concepts. None contains those names.
    • So a target selecting hmrc.isa_amount_subscribed would pass the test, and calibration would then fit weights to figures the stage draws to. This was shown by adding such an entry in memory.
    • No target file names an ISA concept today. Checking the concept prefixes (hmrc.isa_, hmrc.lisa_, hmrc.cash_isa_) as well would close it.
  2. The synthetic CI build now always records uk_stage_isa_subscriptions as failed.

    • Running the synthetic build that test_uk_staging_integration.py runs gives "2024-25 cash subscriptions miss Table 9.4's 2024-25 amount", and the same for stocks and shares. The new target is HMRC's national total, which a small synthetic population can't reach. The old one was the frame's own total times growth.
    • The entry is population_fact_check: true, so the failure is recorded without blocking and CI still passes. But a real limit or age violation there would no longer change the entry's status.
    • Related coverage gaps:
      • The same run selects no cash-only or stocks-and-shares-only subscribers, only about 1,100 both-type records, so it barely exercises the cash path.
      • On the new end-to-end test's fixture, the Lifetime ISA draw saturates (£1.97m of £2.7m), so neither exercises the Lifetime ISA total comparison.
      • The test also hard-codes the check's tolerances rather than reading them from gates.json.
    • Splitting the population-independent rules (limits, ages, holdings, negative and non-finite values, household sums) into an entry without the flag would let the synthetic build block on them again.
  3. lifetime_isa_payer_weight counts holders, not payers (isa_subscriptions.py:1233, read at stage_health.py:4772).

    • It is the weight of holders aged 18–47 whether or not they have room under £20,000.
    • was_lisa already sets that weight to 1,136,000 within 1%, so comparing it with HMRC's accounts proves nothing. In the synthetic build it equals the shift's holders_after exactly.
    • On the test fixture, 9.2% of that weight has no room left and pays £0. Reporting the weight with a positive payment would make the comparison informative.
  4. Holding subscribers at £20,000 while scaling everyone else by 0.889 leaves an empty range just under the limit (scale_to_build_year_amounts, isa_subscriptions.py:1143).

    • On HMRC-table populations, no cash-only subscriber lies between £17,776 and £20,000. On the test fixture (factor 0.644), the largest amount below the limit is £12,820.
    • The £12,000 cash-limit measure is barely affected: £23.1bn of the £25.6bn above £12,000 is from people at the limit. But an overall-limit reform set inside the empty range would reach only people at £20,000.
    • Plausibly, the share at the limit is also understated. HMRC's cash per account rose 27.6% between the years, while everyone below the limit is scaled down 11%.
  5. The remaining gap to the impact note's 22% is mostly a difference of year, not age (docs/uk-isa-subscriptions-1415.md:141-144).

    • Table 9.8 puts 26.4% of cash-only subscribers at 66 or older. For under-65s to sit at 22% while all ages sit at 37%, the over-66s would need about 80% above £12,000, which is implausible.
    • The 22% is for 2022-23 and the model is 2024-25, so that figure can't validate the model either way.

The three focus areas

  1. Build order:
    • isa_subscriptions is still last and writes five person columns.
    • The new end-to-end test asserts that households, weights and earlier person columns pass through unchanged.
    • The extra Lifetime ISA holders from was_lisa still reach uc_reporter_redraw, uc_take_up and pension_credit_take_up as means-test capital. This is documented, but not measured.
  2. Calibration: no target reads an ISA fact today, but the test meant to guarantee that has the gap in new finding 1. Nothing measures the ISA columns at final weights yet.
  3. Checks against bad values:
    • Real builds now block on: non-finite values, totals off Table 9.4's 2024-25 amounts (the 1e-6 tolerance is right, since the solver reaches the target exactly and an unreachable target now fails), and undeclared Lifetime ISA ages.
    • Reported but not blocking, by your rulings: the share above £12,000, the share at the limit, payer counts, unmet subscribers, and holders above the target.
    • The synthetic CI build blocks on none of them (new finding 2).

Tests run

On Python 3.14 with policyengine-uk 2.164.2:

  • 535 engine-free tests pass: ISA, stage checks, was_lisa, the ownership shift, the data contract, end-of-build checks and the country spec.
  • 695 pass in the spec-engine, check-set, coverage and UK graph suites.
  • 8 engine tests pass: the ISA income contract, the UK graph and the uk_spine parity fixture.
  • ruff check and ruff format are clean, ci_test_plan.py verify is ok, and the synthetic build exits 0.

Not run:

  • a real build with licensed data;
  • the size of the UC and Pension Credit effect;
  • the full suites;
  • the old head's synthetic build, so the claim that it used to pass is inferred, not observed.

🤖 Generated with Claude Code

Anthony's second round on microcosm#1421:

- Taper (María's ruling of 29 October 2026): a person whose combined
  amount lies between GBP 18,000 and the overall limit moves on a line from
  their scaled GBP 18,000 up to it. GBP 18,000 is Table 9.7's last band
  floor below the limit, declared as taper_from_gbp and checked against the
  bands. A single factor below one left nobody between about GBP 17,750
  and GBP 20,000. On HMRC-table populations the cash factor is about 0.878,
  with 36.9% of cash subscribers above GBP 12,000, 28.8% at the limit and
  amounts up to about GBP 19,950 below it.
- The gate splits:
  - uk_stage_isa_subscriptions_invariants holds the rules any population
    keeps (non-finite values, limits, ages, holdings, negative amounts,
    household sums). It is no population fact check, so the synthetic
    smoke build blocks on it;
  - uk_stage_isa_subscriptions keeps Table 9.4's totals as a population
    fact check, since a synthetic population cannot reach HMRC's national
    totals.
  The contract mirrors, the spine gate scope and five digests follow.
- The Lifetime ISA receipt reports the paying weight (holders with a
  positive payment) beside the eligible holder weight, and the gate details
  carry the paying weight.
- Tests:
  - the fence test checks the vendored ISA concepts and their prefixes,
    which targets select by, as well as the record sets;
  - the end-to-end test reads both entries' tolerances from gates.json and
    requires every class selected and an unsaturated Lifetime ISA total;
  - the taper is tested both ways.
- The method doc gives the impact note's 22% to its year: Table 9.4's cash
  was GBP 41.6bn in 2022-23 against GBP 95.6bn in 2024-25. It does not
  come from age.
- The last single-letter name goes, and source_stages.json, the coverage
  manifest and the H2 fixture follow.

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

Copy link
Copy Markdown
Collaborator Author

Thanks, Anthony. Commit a4785ef addresses this round, and its push is the first to run CI on this PR.

New findings

  1. Fence test. Fixed. test_no_calibration_target_reads_an_isa_fact now reads the nine concepts from hmrc_isa_statistics_facts.json and checks the contract and both reference surfaces for any JSON string that starts with their prefixes (hmrc.isa_, hmrc.lisa_, hmrc.cash_isa_) or with the record-set ids. It also asserts that every vendored concept carries one of those prefixes. A target selecting hmrc.isa_amount_subscribed now fails it.

  2. The synthetic build. The gate is split, as you suggested.

    • uk_stage_isa_subscriptions_invariants holds the rules any population keeps: non-finite and negative values, the limits, ages, holdings and household sums. It has no population_fact_check, so the synthetic smoke build blocks on it again.
    • uk_stage_isa_subscriptions keeps Table 9.4's totals and stays a population fact check.
    • Real builds block on both, as before.
    • The end-to-end test now:
      • reads both entries' tolerances from gates.json;
      • requires all three classes to be selected;
      • uses a fixture whose Lifetime ISA total is reached, not saturated.

    On the H2 fixture the smoke build now records uk_stage_isa_subscriptions_invariants as passed and would block on a failure. It records uk_stage_isa_subscriptions as failed without blocking, since a synthetic population can't reach £95.6bn. That fixture's small population still fills mostly the both class, which is drawn first, so the end-to-end test is what covers the single-type classes.

  3. The payer weight. Fixed. The receipt now has paying_weight (holders with a positive payment) beside eligible_holder_weight, and the gate details carry the paying weight against HMRC's accounts.

  4. The empty range below £20,000. Ruling (María, 29 October): taper to the limit.

    • Amounts scale by the type's factor up to £18,000, Table 9.7's last band floor below the limit.
    • A person whose combined amount lies between £18,000 and £20,000 moves on a line from their scaled £18,000 up to the limit. That holds for both-type subscribers too.
    • The start is declared (taper_from_gbp) and checked against the bands.
    • On HMRC-table populations: cash factor 0.878, 36.9% of cash subscribers above £12,000, 28.8% at £20,000. The largest amounts below the limit are about £19,950 for cash only and for both types. Order is preserved.
    • The excess over a £12,000 cash limit is £25.9bn (£25.6bn before), and over an £18,000 overall limit £8.79bn (£8.63bn).
    • On the share at the limit: with people growing by accounts (the 28 October ruling), per-person amounts fall below the limit. Flat people would give 34.5% at the limit. The truth lies between, and the method doc reports the flat-people bound.
  5. The 22%. Agreed, and the doc now says so. Table 9.8 puts 26.5% of cash-only subscribers at 66 or over, so age can't explain the gap. Table 9.4's cash was £41.6bn in 2022-23, £69.5bn in 2023-24 and £95.6bn in 2024-25, so the 22% can't validate 2024-25 either way.

Earlier findings

  • The last single-letter name (isa_subscriptions.py:1016) is spelled out.
  • The anchor's reach, the post-calibration checks and the subscription cells remain the pair build's work, under María's rulings.

🤖 Generated with Claude Code

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

@juaristi22 review of a4785ef43

I read the description, both of Anthony's rounds and your replies, and the last commit's changes, and I don't repeat his points. This round covers the head's CI, the new taper code and how this PR meets #1420.

Must

  1. CI is red on the head: engine-free fails on 3.13 and 3.14 (run 40927425900). The one failure is test_committed_exclusion_registers_load (packages/microcosm-build/tests/engine_free/uk/test_uk_weighted_integrity.py:560). The PR adds tax_free_savings_income to uk/input_mass_reviewed_exclusions.json, but the test still expects only dfe_education_spending and property_wealth. I reproduced it locally. Fixing the set alone won't be enough: the new entry's approved_on (2026-10-28), expires_on (2027-04-28) and adjudication (microcosm#1360) also fail the checks at :566-570, so those need the new values too. The comment above the assertion could gain a line for Export versioned additive UK ISA income and migrate calibration and projection together #1360, as it has for UK: model landlord property income on the taxable (net of expenses) concept, not rent received #1106.

Should

  1. Write down the merge order with Budget 2026 slower data fixes, PR C of #1418: electricity at the ONS level, pension income by area #1420. Anthony found four conflicting files at b89d3aab8, all digests or generated files. At the current heads (a4785ef43 and Budget 2026 slower data fixes, PR C of #1418: electricity at the ONS level, pension income by area #1420's 6919f4495) there are 48. The cause is the Chronicle pin: this PR pins 0b721a4 (chronicle#392) and Budget 2026 slower data fixes, PR C of #1418: electricity at the ONS level, pension income by area #1420 pins 53815b2 (chronicle#394), which is 12 commits ahead of 0b721a4 and contains it. So whichever PR lands second should take 53815b2 and regenerate the vendored resources with tools/vendor_uk_ledger_facts.py, not pick a side in each JSON file. Then, as Anthony said, it should recompute the digests from the merged tree. If Budget 2026 slower data fixes, PR C of #1418: electricity at the ONS level, pension income by area #1420 goes second, its touched tests also need running on policyengine-uk 2.164.2, since its description reports them on 2.162.14 only. I can't find a merge order in either description or in UK: data fixes for the Budget 2026 household measures #1418. One line would do.

Nit

  1. Above a factor of about 1.11 the taper stops doing anything, and its line slopes the wrong way (isa_subscriptions.py:1186-1200). The line starts at factor × £18,000. Above 20,000 / 18,000 that start is over the limit, so the line slopes down, and the cap puts everyone above £18,000 / factor at £20,000 regardless. Nothing goes wrong for 2024-25 (cash factor 0.878). But the docstring could say the taper assumes a factor below that bound, or the stage could refuse a larger one, so a later year doesn't hit it without anyone noticing. Below the bound, the taper is continuous at £18,000 and monotone in the factor, so the bisection still holds.

Verdict: close, one must. Item 1 needs to be green before merging. Item 2 is about order and can be settled in the descriptions.

What I ran: a fresh clone at a4785ef43, uv sync --all-packages --locked, then the weighted-integrity, ISA subscriptions, ISA stage health, was_lisa and anchor engine-free files: 97 passed, 1 failed (item 1). git merge-tree of this head with #1420's head. HMRC's September 2026 savings commentary states a £26.1bn (37.5%) rise in cash ISA subscriptions in 2024-25. That implies about £69.6bn in 2023-24 and £95.7bn in 2024-25, which matches the vendored Table 9.4 figures to rounding.

Not verified: a real build, the engine suites, the integration script, the LISA tables' 1,136,000 accounts, and the size of the anchor's effect on UC and Pension Credit (left to the pair build, as you ruled).

🤖 Generated with Claude Code

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Review of a4785ef

I read the body, the thread and Anthony's two rounds, and don't repeat them. Two points are new: one blocks merge, one is about how far this PR gets the Budget's cash ISA measure.

Findings

1. must: engine-free fails on this head, and the cause is this PR.

  • CI run 40927425900, engine-free (3.13) and (3.14), each has one failure: packages/microcosm-build/tests/engine_free/uk/test_uk_weighted_integrity.py::test_committed_exclusion_registers_load ("Extra items in the left set: 'tax_free_savings_income'"). 14,916 tests pass.
  • The PR adds a tax_free_savings_income entry to uk/input_mass_reviewed_exclusions.json. The test still pins the efrs-post-calibration register to {"dfe_education_spending", "property_wealth"} (test_uk_weighted_integrity.py:560).
  • The loop after that assertion will fail on the new entry as well, for three reasons:
    • it allows approved_on only in {2026-10-02, 2026-10-11}, and the new entry has 2026-10-28;
    • it allows expires_on only in {2027-04-02, 2027-04-11}, and the new entry has 2027-04-28;
    • it needs microcosm#1063 or microcosm#1106 in the adjudication, and the new entry cites microcosm#1360.
  • Fix: add the entry to the expected set, widen the three checks to take it, and add a line to the test's history comment for the Export versioned additive UK ISA income and migrate calibration and projection together #1360 ruling.

2. note: the Budget's cash ISA measure still scores £0 on the population after this PR.

  • The body says so for limit reforms: policyengine-uk 2.164.2 reads the new columns into cash_isa_subscriptions_above_limit, and no tax or spending variable reads that.
  • Budget 2026 keeps the £20,000 cash limit in place of SI 2026/1018's £12,000 from 6 April 2027 (OOTLAR 2.5; policyengine-uk#2728). Its cost against the £12,000 counterfactual therefore depends on two things:
    • the 2027-28 cash subscriptions above £12,000;
    • where that money would go, which the engine doesn't model.
  • The subscriptions are 2024-25 amounts held flat. The engine's own docstring (cash_isa_subscriptions.py) says flat amounts understate what a lower limit leaves out in later years, and that "a producer can grow them instead and cap each person at the limit in force". This PR chooses not to.
  • Ask: none for this PR. For the pair build's method doc, record that the 2027-28 cash amounts above £12,000 are 2024-25 levels, so nobody reads the column as a 2027-28 base for the dashboard's cash ISA card.

Checked and found true

  • The Lifetime ISA ages 18 to 47 for 2024-25 are right. Accounts have opened only from 6 April 2017 and only below 40, so the oldest payer in 2024-25 opened at 39 in 2017-18 and is at most 47.
  • policyengine-uk 2.164.2's tax_free_savings_income is adds = ["individual_savings_account_interest_income"] on its main. Under the additive contract, dropping the stored total therefore doesn't leave the engine reading a stale copy, which is the reason the new exclusion entry gives.
  • engine-uk, engine-us, integration-uk, lint and wheels pass at this head.

Not verified: any build; the HMRC Table 9.4 and 9.7 totals; the WAS donor split.

This branch has not been deployed

No deployments
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