Skip to content

SPI donor savings rebased to SPI's 2023-24 bands (savings follow-up to #1418) - #1428

Draft
juaristi22 wants to merge 17 commits into
mainfrom
uk-savings-costing-anchor-1418
Draft

juaristi22 wants to merge 17 commits into
mainfrom
uk-savings-costing-anchor-1418

Conversation

@juaristi22

@juaristi22 juaristi22 commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

The savings follow-up to #1418, part 1: fix the low savings base at its source instead of making the solve stretch weights. This branch starts from PR C's last commit (0b276e571, merged as #1420).

Draft. The national built from b441bb5a0 (on f3cb3ddd3's spine) is certified and shippable, staged locally. The other-income fixes below (4a6dc728f to b82247443), the small traders' pay (d563f4fbc), the tax on savings rows (5c0137fb4) and the take-up pools (6485af4af) need a new spine and national. A spine and national are building from 0c2f5314f, which carries all of them except the tax on savings rows, reverted (below). The spine from f9676fed9 stopped in the new receipts stage, which hadn't declared its source-household read; 0c2f5314f declares it. Staging reaches its repository, so the cut can publish. The spine from d563f4fbc stopped at the UC take-up gate. The spine from 6485af4af passed every gate, but its national stopped at the fit gate on the tax on savings rows. The measurements will come in a comment before review.

The problem

PR C binds SPI Table 3.7's interest and other income by total-income band. Interest moves to the OBR's savings path from 2023-24.

  • The frame's base is low. At design weights it carries about 70 percent of those rows. The solve meets them by stretching the weights of the few households that carry savings, so the totals hit but the column's distribution doesn't.
  • The SPI donors are under-aged. They come from the 2022-23 tape and reach the 2024 frame by each column's engine index:
    • other income by GDP per head, ×1.09 from 2022 to 2024, where SPI's own other income grew ×1.65 in the single year to 2023-24;
    • interest by ONS household interest, ×2.27, where SPI's grew ×2.69 in that year.
  • The top band's split is flipped. On the tape, the £1m-and-over band's other income outweighs its interest, as in the published table. Ageing the two columns on such different indices flips that split.

The PR C national built from 5aba83cd2 stopped at certification on that band. Its figures stay out of this thread, because the band rests on the SPI tape.

The change

  • spi_income.rebase_spi_donor_savings runs before any draw. It scales each donor's interest and other income to HMRC's published 2023-24 Table 3.7 amounts:

    • the factor in each band is the published amount over the tape's FACT-weighted amount, with the tape banded by its own total income;
    • a donor below the first band takes the factor of all bands together;
    • a band the tape carries none of is left as it is and named in the receipt (the production tape carries every band).

    The stage-1 forest and the band-donor resample both read the rebased tape. The receipt records factors only, never counts.

  • _spi_income_uprating_factors then grows the two columns from 2023-24 to the frame's year on the OBR's savings-income growth (October 2026 EFO, Table 4.5), through the new hmrc_uprating.uk_obr_savings_income_level. This is the path the Table 3.7 band targets already take. From the frame's year on, the engine projects both at GDP per head, so both columns are now listed in SPI_HISTORY_INDEX_DIFFERS_FROM_PROJECTION.

  • uk/hmrc_spi_table_3_7_savings_rows.json vendors the 26 published rows (13 bands × 2 columns) from the feed pin. It is declared in the vendoring register and the country package.

  • Docs and changelog: docs/uk-income-anchors-280.md and changelog.d/1418-spi-donor-savings-rebase.changed.md.

The UC take-up gate: 0.055 to 0.06 (second commit)

The first spine built from 667fe532a stopped at uk_stage_uc_take_up. After the take-up redraw, single people's caseload ended 0.0568 of DWP's count further from it than before. PR C's spine was at 0.0523, and the tolerance is 0.055. The cause:

  • The rebased SPI savings feed the UC reporter redraw's investment-income predictor, so a few SPI benefit units change report.
  • That moves the take-up stage's pool merging, which joins adjacent award bands until each pool rests on enough sources.
  • As a result, single people's £1,000–£1,100 band moved from a reachable pool into one whose DWP target exceeds the entitled units.

The reachable pools still realize DWP's counts within 0.005. The tolerance goes to 0.06 (María's ruling of 2026-10-30), as PR A took it from 0.05 to 0.055 for the gilt reshuffle. Pool boundaries that jump with small upstream changes are a follow-up on #1334. The gate's note, the committed-parameter test and the data contract's mirrored digests are updated.

Superseded by 6485af4af, which fixes the pools and returns the gate to 0.055 (below).

The national from f3cb3ddd3: what moved

The spine passed every gate. The national's fit gate blocked on one row. Below £1m, every Table 3.7 interest and other-income band fits within 1% after the solve, and the £1m-and-over other-income row that blocked PR C's national now passes the fit gate.

The share of each group's solved mass sitting on households stretched beyond 3× their design weight, PR C's blocked national against this one:

Group PR C This national
Interest carriers below £100k 54% 41%
Other-income carriers from £100k 29% 10%
Other-income carriers below £100k 66% 63%

The last row is the FRS under-reporting part 2 would address.

At design weights, the rebased frame carries more savings than SPI in the £200k–£1m bands, where its top earners are already over-represented, so the solve now cuts those savers' weights instead. The figures for the £1m-and-over band stay out of this thread, because it rests on the SPI tape.

The £1m-and-over interest hold (third commit)

The one row the fit gate blocked on, Table 3.7's £1m-and-over interest amount, is held as a measured distortion, as the ten HMRC £1m-and-over cells are (María's ruling of 2026-10-30), and is still measured on every evaluation. How the rebase sits in the top bands at design weights is a follow-up.

Why other income is short below £100k, and the fixes (fourth to sixth commits)

A read-only investigation of the national from f3cb3ddd3 found that below £100k the frame carries 0.40 of Table 3.7's other income at design weights. It ranked three causes:

  1. FRS rows carry a different concept (46% of the gap).
  2. SPI copies draw thin, incoherent other income (34%).
  3. Taxpayer coverage (18%), which the solve's taxpayer rows already fix.

By María's choice (2026-10-30), three changes address the first two, plus a fix to part 1's rebase:

  • 4a6dc728f, the rebase on taxpayers (F). Table 3.7 counts taxpayers only, so each band's factor is now computed over the tape's taxpayers (TAXPAYER 1), and non-taxpayers take the all-bands factor. Over all records, the two lowest bands' taxpayers fell short of the published amounts.
  • 092de13b9, FRS savings income on SPI's definitions (A1, A2).
    • NS&I Direct Saver and the other NS&I and bank products move to savings_interest_income, since SPI has counted gross-paid interest as bank interest since 2016-17 (note 4). Gilts stay in other income.
    • Income from an annuity not bought with pension funds or from a trust or covenant (FRS PENTYPE 4 and 5) moves from private pension to other income (note 6). The whole annuity payment is booked, of which only the income element is taxable.
    • Both columns are taxable savings income, so the tax on these rows doesn't move.
  • b82247443, a coherent other-income draw on SPI copies (C). The stage-1 transport locates each copy's draw at its twin's own other income (gilts, trust and annuity income) rather than at an identity uniform that ignored it. The stage spec's analogue map and the coverage manifest follow.

The investigation's frame arithmetic puts other income below £100k at design weights at about 0.66 of its target with these changes, against 0.40 before. The next build measures it.

FRS rows keep their own reported incomes: the investigation's larger lever, an SPI fill of other income on FRS-row taxpayers, is not taken.

The small traders' pay: SPI Table 3.10's employment-income counts (d563f4fbc)

The national from b441bb5a0 costs the Budget's trading allowance measures (Table 6.1 line 15) below PR A's, and both sit below the costing. A read-only investigation found two things:

  • The fall from PR A is PR C's Table 3.10 counts correcting an overshoot in PR A's £2,000–£3,000 band, so it isn't a regression.
  • The frame's small traders have the wrong mix. In Table 3.10, more than half of the traders below £3,000 also have employment income; the frame's mix leans to traders without pay. An employee's side profit is taxed at their marginal rate, so the mix decides what the measures cost.

By María's choice (2026-10-30), hmrc.spi.employment_income.count_by_self_employment_income_band binds Table 3.10's individuals with employment income over the same eight bands from £1 to £10,000 as the self-employment counts:

  • Each person with pay over the year (employment_income_annual, the variable Table 3.6's employment rows read) counts in their own self-employment band, taxpayer or not.
  • Table 3.10's column is wider: income assessed under ITEPA 2003 plus other miscellaneous income (its note 4). Like Table 3.6's employment rows, the binding leaves out miscellaneous income, benefits in kind and the contributory benefits.
  • The counts grow to 2025 as the self-employment counts do. The zero band and the bands from £10,000 are signed out row by row in target_reference_signed_exclusions.json.
  • The target is declared signature-incomplete beside the other Table 3.10 rows. The national references and membership and the national compile-parity receipt are regenerated on the pinned feed (53815b2), with eight new rows.
  • The membership's 13 SPI dividend rows and 8 Table 3.10 self-employment amount rows also take the October EFO engine pins that main's policyengine-uk 2.164.2 lock brought (b89d3aab8). The merge had left them on the 2.162.14 pins. The parity receipt doesn't move for them, and builds compile live from the pins.

The tax on savings rows: the OBR path (5c0137fb4, reverted in f9676fed9)

The three ITL Table 2.6 rows bound HMRC's own 2024-25 and 2025-26 projections as a calendar-2025 window: £8.40bn of tax on savings, ×1.52 on the 2023-24 outturn. Table 3.7's savings income grows only ×1.36 on the OBR's path. Bound together, the rows pinned the tax on savings, and so the savings rates' static yield, about 12% above the base the Budget's costing grows.

By María's choice (2026-10-31, option A):

  • The rows read the 2023-24 outturn alone and grow it by the OBR's savings income growth, the index Table 3.7's savings rows take: £1.61bn, £2.24bn and £3.66bn, £7.51bn in all.
  • This leaves out the leverage that HMRC's projection carries from the frozen Personal Savings Allowance and thresholds.
  • hmrc_itl_savings_rate leaves the source-projection families. Only the three rows move in the references, the membership and the parity receipt.

Reverted (f9676fed9, María's ruling of 2026-10-31). The national from 6485af4af stopped at uk_target_fit, as the leverage caveat predicted:

  • Under the engine's rules, tax on savings grows faster than savings income, because the allowance and thresholds are frozen.
  • So the tax rows grown on the income's path asked for less tax than Table 3.7's savings income gives.
  • The solve cut the interest rows in the basic-rate bands to meet them: the £30,000 band ended 30.7% under target and the £20,000 band 12.0% under.

HMRC's own projection carries that leverage, and b441bb5a0's national met it beside the interest rows. So the savings rates' yield stays pinned to the projection. Its distance from the costing is a difference of basis and of behaviour (ISA shifting), not a fault in the base.

UC take-up pools that cannot meet DWP's count (6485af4af)

The spine from d563f4fbc stopped at uk_stage_uc_take_up. Single people's caseload ended 0.0674 of DWP's count further from it than before the redraw (0.0568 on f3cb3ddd3's spine).

  • The pools whose counts the draw can reach met them.
  • The gap is the pools that cannot meet their count: either DWP's count exceeds every entitled unit, or the reporters alone already exceed it. Those pools kept a clipped rate, and which bands fall there moves with small upstream changes.

By María's ruling (2026-10-31), the stage is fixed instead of the gate widened again:

  • The join. After the source-support pooling, a pool that cannot meet its count joins the adjacent pool with more room for its gap, lowest first and the lower neighbour on a tie. Joining stops when every pool can meet its count or the family type is one pool. So a family type's caseload lands on DWP's wherever its entitled units allow, with claimants drawn across neighbouring award bands where one band falls short. UC results move in later builds.
  • The gate returns to main's 0.055.
  • What follows: the stage spec, the coverage manifest, the H2 fixture and the data contract's digests. New stage tests cover the join.

The trading allowance's base: the self-employed's gross receipts (f3540cbc6)

policyengine-uk applies the trading allowance against self_employment_gross_receipts, and neither the FRS nor the SPI tape records turnover. Without receipts, the engine relieved profits up to the allowance in full and nothing above. So the Budget's raise to £3,000 missed its main base: traders who already claim the £1,000 allowance against larger receipts and gain the larger deduction. No published source gives turnover beside profit. But under current law a claimant's receipts are their profit plus £1,000 exactly, so only who claims is missing.

By María's choice (2026-10-31), a new last spine stage, trading_allowance_receipts, sets each trader's receipts:

  • Who claims. Each trader with a positive profit is flagged as a claimant, drawn on their own person_id: 0.64 for profits up to £4,500, 0.32 from £4,500 to £9,000, none above.
  • Receipts. A claimant's receipts are their profit plus £1,000. Any other trader's are their profit plus £3,000, expenses at the raised allowance; the costing books traders who switch to the allowance as behaviour. A person with no positive profit keeps zero, which the engine reads as unknown.
  • The rates are calibrated to HMRC's policy paper (about 400,000 taxpayers with a lower bill, by about £380 on average) until HMRC publishes claimant counts. An HMRC table of claimants by profit band would replace them.
  • Filing. Every trader with a positive profit is treated as filing, as the bound Table 3.10 counts are of filers. So a profit of £1,000 or less is now taxed where the engine used to relieve it in full.
  • Uprating. The engine uprates receipts with profit, so a claimant's £1,000 grows with the mixed income index, and the raise relieves slightly less than £2,000 more in later years.
  • Wiring:
    • the operation kind in the stage schema;
    • the stage spec, with its mass receipt;
    • the graph, kernel and spine-build wiring;
    • the coverage manifest family and the H2 fixture (51 stages).
    • self_employment_gross_receipts also leaves the register of engine inputs left at their default and joins the export surface's allowed columns, so the data contract's gate digests follow.

Up to date with main, and CI (30 Oct)

  • 9ee6fbde0 merges main into the branch. The main change it brings is PR B (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: ISA subscriptions, policyengine-uk 2.164.2). Conflicts resolved:
    • the FRS account routing, keeping main's ISA interest column and this branch's NS&I routing;
    • the FRS spine and SPI engine tests;
    • the vendoring register, keeping both new resources;
    • the data contract's gate digests, recomputed for the merged gates.json;
    • the coverage manifest, regenerated.
  • 995969f02 leaves the savings rebase off any tape whose weights don't gross to the population.
    • The band factors assume they do. On the H2 fixture's synthetic tape they ran to millions and broke the band donors.
    • A column whose tape holds under a tenth of the published amount is now left as it is, and the receipt says so.
    • The production tape holds about 0.37 of the published interest and 0.6 of the other income.
  • e5ac7a5d8 regenerates the H2 spine fixture for the other-income analogue (C). CI's engine-uk graph test and the integration-uk smoke test refused its stale stage-spec snapshot.
  • 53ad29934 starts a missing self-analogue output at zero before the SPI transport. Since C, the transport reads other_investment_income as each twin's own analogue before the stage-1 draw, so CI's engine test of a frame whose FRS rows carry no other income was refused. That column now gets the stage's zero before the transport, as it did after the draw, and the final values are unchanged. Production FRS frames always write it.
  • 45c2888fa regenerates the H2 spine fixture on policyengine-uk 2.164.2, the lock's pin since the merge of main. e5ac7a5d8 had built it in a local environment still on 2.162.14, which CI's engine-uk graph test caught.

Not in this PR

  • Measured claimant counts for the trading allowance. The claimant rates are calibrated to HMRC's published gainers, not measured. Miscellaneous income's allowance is the engine's (policyengine-uk#2726).
  • FRS under-reporting of savings below £100k (part 2) is not addressed here. The build shows how much of that gap remains.

Checks run locally

All pass. Engine-free tests:

  • the new test_uk_spi_savings_rebase.py;
  • the SPI income, spine, dependency and acceptance-receipt tests;
  • FRS spine, graph, property components, stage health and the HMRC source manifest;
  • Carer's Allowance take-up, ledger-fact vendoring, country spec and donor uprating;
  • the spine parity instrument and the WAS tests that read the H2 fixture;
  • UC take-up and stage health, the population targets and target references, the source stages, the coverage manifest and the data contract;
  • the new test_uk_trading_allowance_receipts.py, the stage rosters, the country spec, the graph and the terminal gates.

The updated engine tests (engine/uk/test_uk_spi_income.py, engine/uk/test_uk_hmrc_uprating.py, engine/uk/test_uk_itl_savings_tax_rates.py, engine/uk/test_uk_release_input_coverage.py) and the H2 acceptance test run in CI.

🤖 Generated with Claude Code

juaristi22 and others added 2 commits October 30, 2026 11:17
The SPI income stage now moves the 2022-23 tape's interest and other
income to HMRC's published 2023-24 Table 3.7 amounts band by band, with
the tape banded by its own total income, before any draw. The OBR's
savings income growth then carries both columns to the frame's year,
the path the Table 3.7 band targets take.

Before, each column moved by its engine index from 2022 to 2024: other
income by GDP per head (x1.09), interest by ONS household interest
(x2.27). SPI's own amounts grew x1.65 and x2.69 in the single year to
2023-24. The frame carried about 70 percent of the savings rows at
design weights, and the solve met them by stretching the few households
that carry them. On the tape, the £1m-and-over band's other income
outweighs its interest, as in the published table; the two indices had
flipped that split.

- uk/hmrc_spi_table_3_7_savings_rows.json vendors the 26 published rows
  from the feed pin, declared in the vendoring register and the country
  package.
- spi_income.rebase_spi_donor_savings scales the prepared tape. A band
  the tape carries none of is left as it is and named in the receipt.
  The forest and the band-donor resample both read the rebased tape.
- _spi_income_uprating_factors grows the two columns from 2023-24 on
  hmrc_uprating.uk_obr_savings_income_level. Both columns now run on the
  projection's GDP per head from the frame's year on
  (SPI_HISTORY_INDEX_DIFFERS_FROM_PROJECTION).
- Tests cover the rebase, the published rows, the OBR factor and the
  stage wiring, and the engine lockstep tests are updated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The first spine built from 667fe53 stopped at uk_stage_uc_take_up.
Single people's caseload ended 0.0568 of DWP's count further from it
than before the redraw, against 0.0523 on PR C's spine and a tolerance
of 0.055.

The rebased SPI savings feed the UC reporter redraw's investment-income
predictor. The few SPI benefit units whose report changes move the take-up
stage's pool merging, which joins adjacent award bands until each pool
rests on enough sources. Single people's £1,000 to £1,100 award band
moved from a reachable pool into one whose DWP target exceeds the
entitled units. The reachable pools still realize DWP's counts within
0.005.

The tolerance goes to 0.06 (María's ruling of 2026-10-30), as PR A took
it from 0.05 to 0.055 for the gilt reshuffle. Pool boundaries that move
with small upstream changes are a follow-up on #1334. The gate's note,
the committed-parameter test and the data contract's mirrored digests
are updated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
juaristi22 and others added 13 commits October 30, 2026 13:15
The national built from f3cb3dd fitted every SPI Table 3.7 interest
and other-income band but one, the £1m-and-over other-income row
included. It missed the £1m-and-over interest amount row outside the
uk_target_fit band.

By María's ruling of 2026-10-30, the row is held out of the objective as
a measured distortion, as the ten HMRC £1m-and-over cells are, and
measured on every evaluation. The register entry carries no figures for
the band. How the rebase sits in the top bands at design weights is a
follow-up.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
SPI Table 3.7 counts taxpayers only, but the rebase divided its amounts
by the tape's amount over all records in each band. In the two lowest
bands non-taxpayers hold much of the tape's savings income, so the
rebased taxpayers reached only part of the published amounts there.

The prepared tape now carries the tape's TAXPAYER flag. Each band's
factor is computed over its taxpayers and applied to them; non-taxpayers
take the all-bands factor, as donors below the first band already do. A
tape without the flag counts every record, and the receipt says which
basis it used.

Option F of the #1428 other-income investigation.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two FRS-row mappings disagreed with SPI Table 3.7, so the frame's other
income below £100k carried the wrong concept.

- NS&I Direct Saver and the other NS&I and bank products move from
  other_investment_income to savings_interest_income. SPI has counted
  all interest paid gross or net as bank interest since 2016-17 (note
  4). Gilts stay in other income, as interest on securities (note 6).
- Income from an annuity not bought with pension funds or from a trust
  or covenant (FRS PENSION.PENTYPE 4 and 5, the ADULT ANYPEN4 and ANYPEN5
  sources) moves from private_pension_income to other_investment_income,
  the other income Table 3.7 counts (note 6). The FRS records the whole
  annuity payment, of which only the income element is taxable.

Both columns are taxable savings income in policyengine-uk, so the
savings tax on these rows does not move. The FRS spine test covers both
routes.

Options A1 and A2 of the #1428 other-income investigation.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Other income was the one investment leaf of the SPI stage-1 transport
with no FRS analogue. Its quantile came from an identity uniform keyed
on the twin, which ignored the twin's holdings. Copies drew large
amounts on modest incomes, and the draw was out of line with the rest of
each copy's income.

The analogue map now names the twin's own other_investment_income:
gilts, trust income and purchased-annuity income, the FRS sources of
SPI's other income after the previous commit. A copy's draw now ranks
with its twin's holdings, as interest, dividends, pensions and property
do. The stage spec's analogue_columns (sources.yaml and
source_stages.json) and the release input coverage manifest that pins
the spec follow, and the spine test's synthetic frame carries the
column, as the FRS spine always writes it.

Option C of the #1428 other-income investigation.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings #1428 up to date with main, including PR B (#1421, the ISA
subscriptions and policyengine-uk 2.164.2).

Conflicts resolved:
- frs_spine: main's ISA interest column, with this branch's NS&I-to-
  deposit routing.
- The FRS spine test: ISA interest apart, plus NS&I as deposit interest
  (5.0 a week of non-ISA interest).
- The SPI engine test: the savings columns on the OBR path.
- The vendoring register: both new resources.
- The coverage manifest: regenerated.
- The data contract: gate digests recomputed for the merged gates.json
  (main's, plus the UC take-up tolerance of 0.06).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The band factors divide published amounts by the tape's FACT-weighted
amounts, so they assume the tape grosses to the taxpayer population.

The H2 spine fixture's synthetic tape doesn't: its factors ran to
millions, lifted its band donors' income out of their bands, and the
fixture's regeneration refused ("no record whose uprated total income
lies in the band from 200000"). CI's acceptance and staging smoke tests
would meet the same refusal.

Now a column whose banded tape amount, over the bands it carries, is
under a tenth of the published amount is left as it is, and the receipt
says so. The production tape holds about 0.37 of the published interest
and 0.6 of the other income. A test covers the skip.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
CI's engine-uk graph test and integration-uk smoke test refused the
SPI income stage: the fixture's stage-spec snapshot still named no
analogue for other_investment_income, against the reviewed map (C in
b822474).

tools/graph_uk_spine_fixture.py regenerates it on the merged tree:
- fixture.json carries the new analogue entry;
- the fixture's derived CGT and property fact sources follow the
  synthetic frame they're built from;
- uk_spine.json's pin follows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
CI's engine test test_spi_stage2_does_not_require_frs_other_investment_income
runs the SPI income stage on a frame whose FRS rows carry no
other_investment_income. The stage fills that output with the
stage-time zero, but only after the stage-1 draw. Since b822474 the
transport reads the column as the twin's own analogue before the draw,
so the stage refused the frame.

Now a stage-1 output that is its own analogue and that the frame doesn't
carry gets the same zero before the transport. Every twin locates as a
non-carrier, and the final values are unchanged. Production FRS frames
always write the column.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
e5ac7a5 built the fixture in a local environment still on
policyengine-uk 2.162.14. The lock has pinned 2.164.2 since the merge of
main (9ee6fbd), so CI's engine-uk graph test found the stamped engine
version differ from the graph it builds. Rebuilt with the environment
synced to the lock: the graph's version stamp moves, and so do the
fixture's synthetic property rental facts, which the engine uprates.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
María's choice of 2026-10-30 (microcosm#1418, the trading allowance
measures). With Table 3.10's self-employment counts alone, the solve can
meet each GBP 1 to GBP 10,000 band with traders who have no pay. In the
table, more than half of the traders below GBP 3,000 also have employment
income, and an employee's side profit is taxed at their marginal rate.

- hmrc.spi.employment_income.count_by_self_employment_income_band counts
  each person with pay over the year (employment_income_annual, which
  Table 3.6's employment rows read) in their own self-employment band,
  taxpayer or not, over the same eight bands as the self-employment
  counts. Table 3.10's column also holds miscellaneous income, benefits
  in kind and the contributory benefits, which the binding leaves out,
  as Table 3.6's does. The counts grow to 2025 with HMRC's projected
  growth of all taxpayers, as the self-employment counts do.
- The zero band and the bands from GBP 10,000 are signed out row by row.
  The target is declared signature-incomplete beside the other Table 3.10
  rows and is unmapped in the registry parity (no uk-data metric).
- The national references and membership and the national compile-parity
  receipt are regenerated on the pinned feed (53815b2): eight new rows.
  The membership's 13 SPI dividend rows and 8 Table 3.10 self-employment
  amount rows also take the October EFO engine pins that main's
  policyengine-uk 2.164.2 lock brought (b89d3aa). The merge left those
  rows on the 2.162.14 pins. The parity receipt doesn't move for them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
María's choice of 2026-10-31 (microcosm#1428). The three Income Tax
liabilities statistics Table 2.6 rows bound HMRC's own 2024-25 and
2025-26 projections as a calendar-2025 window: GBP 8.40bn of tax on
savings, x1.52 on the 2023-24 outturn, against x1.36 for savings income
on the OBR's path. With SPI Table 3.7's savings income already on the
OBR path, those rows pinned the tax on savings, and so the savings
rates' static yield, about 12% above the base the Budget's costing grows.

- The rows now read the 2023-24 outturn alone. They are observed facts,
  so hmrc_itl_savings_rate leaves the source-projection families.
- They grow by the OBR's savings income growth (October 2026 EFO Table
  4.5), x1.3562 to calendar 2025, the index Table 3.7's savings rows
  take: GBP 1.61bn, 2.24bn and 3.66bn, GBP 7.51bn in all.
- Growing the tax on the income's path leaves out the leverage that
  HMRC's projection carries from the frozen Personal Savings Allowance
  and thresholds.
- The national references, membership and compile-parity receipt are
  regenerated on the pinned feed, and only the three rows move. The
  engine test's comments now name calendar 2025's two tax years.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
María's ruling of 2026-10-31 (microcosm#1428): fix the take-up pools
instead of widening the gate again. The spine from d563f4f stopped at
uk_stage_uc_take_up. Single people's caseload ended 0.0674 of DWP's
count further from it than before the redraw: 0.0568 on f3cb3dd's
spine, against this branch's 0.06. The reachable pools met DWP's counts.
The gap was the pools whose DWP count exceeds every entitled unit, which
kept a clipped rate, and which bands land there moves with small
upstream changes.

- After the source-support pooling, the stage joins a pool that cannot
  meet DWP's count to an adjacent pool (_join_unmet_pools). Such a pool's
  undrawn units' expected claims exceed the count, or the count exceeds
  them plus every drawn unit. Lowest first, it joins the neighbour with
  more room for its gap, the lower on a tie. Joining stops when every
  pool can meet its count or the family type is one pool. A family type
  whose count exceeds all its entitled units stays one unreachable pool.
- The stage spec restates the pooling rule (sources.yaml,
  source_stages.json, UC_TAKE_UP_POOLING), and the coverage manifest and
  the H2 fixture follow.
- uk_stage_uc_take_up returns to main's 0.055, undoing this branch's
  0.06. The gate note, the committed-parameter test and the data
  contract's mirrored digests follow.
- New tests: an unmet pool joins the neighbour with more room, or the
  lower one on a tie; an overshooting pool joins one with drawn claims;
  a family type that cannot meet its count is one unreachable pool,
  flagged by the side it misses on.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
María's choice of 2026-10-31 (microcosm#1428). policyengine-uk applies
the trading allowance against self_employment_gross_receipts, which
neither the FRS nor the SPI tape records. Without receipts it relieved
profits up to the allowance in full and nothing above. So the Budget's
raise to GBP 3,000 missed its main base, the traders who already claim
the GBP 1,000 allowance against larger receipts.

- A new last spine stage, trading_allowance_receipts, flags each trader
  with a positive profit as a claimant, drawn on their own person_id:
  0.64 for profits up to GBP 4,500, 0.32 from GBP 4,500 to GBP 9,000,
  none above.
- It stores receipts of profit plus GBP 1,000 for a claimant, exact
  under current law, and profit plus GBP 3,000 for any other trader
  (expenses at the raised allowance; the costing books switchers as
  behaviour). A person with no positive profit keeps zero.
- The rates are calibrated to HMRC's policy paper (about 400,000
  taxpayers with a lower bill, about GBP 380 on average) until HMRC
  publishes claimant counts.
- Every trader with a positive profit is treated as filing, as the
  bound Table 3.10 counts are of filers. So a profit of GBP 1,000 or
  less is now taxed.
- Wiring:
  - the operation kind in the stage schema and the allowed kinds;
  - the stage in sources.yaml and source_stages.json, with its mass
    receipt;
  - the graph reads and cell, the kernel modules, the spine build and
    fixture implementations, and the coverage manifest family.
- self_employment_gross_receipts leaves UK_ENGINE_DEFAULT_INPUTS and
  joins the export surface's allowed columns (terminal_gates and
  gates.json), so the data contract's gate digests follow.
- Tests: the stage's own, and the rosters (51 stages). The H2 fixture
  follows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
juaristi22 and others added 2 commits October 31, 2026 12:32
This reverts commit 5c0137f (María's ruling of 2026-10-31,
microcosm#1428). The national built from 6485af4 stopped at
uk_target_fit. Under the engine's rules, tax on savings grows faster
than savings income: the Personal Savings Allowance and the thresholds
are frozen. So the tax rows grown on the income's path asked for less
tax than SPI Table 3.7's savings income gives. To meet them, the solve
cut the interest rows in the basic-rate bands: the GBP 30,000 band
ended 30.7% under target and the GBP 20,000 band 12.0% under.

HMRC's own 2024-25 and 2025-26 projections carry that leverage, and the
national from b441bb5 met them beside the interest rows. So the
savings rates' yield stays pinned to the projection. Its distance from
the costing is a difference of basis (the projection's leverage) and of
behaviour (ISA shifting), not a fault in the base.

The three rows, their references, membership and parity receipt, and
the two tests are back as at d563f4f. The changelog fragment goes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The spine from f9676fe stopped in trading_allowance_receipts, and so
did CI's graph driver test and the staging smoke test. The stage counts
its receipt's support by household_source_id, but its graph declaration
read only self_employment_income, so the executor didn't hand it that
cell. The stage now declares the read, as regional_home_values and
property_purchases do, and the H2 fixture's graph follows.

The fixture tool's legacy oracle runs every stage on the whole frame, so
it didn't catch this.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

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.

1 participant