Skip to content

Set reliefs and allowances against income for the least tax - #2830

Merged
juaristi22 merged 24 commits into
mainfrom
fix-2106-allowance-order
Nov 10, 2026
Merged

juaristi22 merged 24 commits into
mainfrom
fix-2106-allowance-order

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2106

Reproducer: policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/received_allowances/least_tax_allocation.yaml::Case 2, savings in the starting rate band leave the Personal Allowance to dividends

axiom: TheAxiomFoundation/rulespec-uk#524 queued

What and why

Parliament set the rule in ITA 2007 s. 25(2): "At Steps 2 and 3, deduct the reliefs and allowances in the way which will result in the greatest reduction in the taxpayer's liability to income tax." The model always deducted them from non-savings income first, then savings, then dividends. Under that order it overcharged wherever an allowance set against savings or dividends would save more tax:

2026-27, age 40 origin/main This PR Why
Dividends £20,000 £744.97 £744.97 One kind of income
Savings £4,000 + dividends £20,000 £1,174.97 £744.97 The savings fall in the 0% starting rate band (s. 12), so the allowance saves tax only against dividends
Savings £4,000 + dividends £200,000 + trading loss £1,000 £65,574.21 £65,466.71 The loss relief goes against dividends, not against savings at 0%
Pay £50,000 + savings £10,000 £11,286 £11,232 £270 of the allowance against savings keeps pay at the £37,700 limit, so the £500 savings allowance covers savings that would bear 40%
Pay £50,000 + dividends £20,000 £14,457.25 £14,414.73 The same with the dividend allowance

Both columns come from runs of the model at 6429896 (whose model code 70633f0 keeps) and origin/main 2da212a; the YAML cases pin the right-hand column.

The model overcharged people with savings as well as dividends (second row), and people with only pay and savings (fourth row).

Parliament changed the rule from 2027-28:

  • Before 2027-28, the taxpayer may split the deductions in any way between non-savings, savings and dividend income (s. 25(2)). Parliament taxes property income as non-savings income at the same rates in those years, so the model keeps taking the deductions off the rest of non-savings income first.
  • From 2027-28, the taxpayer deducts reliefs and allowances from income other than property, savings and dividend income first (s. 25(3A), which Parliament inserted by Finance Act 2026 s. 6(6) with effect "for the tax year 2027-28 and subsequent tax years" (s. 6(8))). The taxpayer may then split what remains in any way between property, savings and dividends. The model records the commencement in a new boolean parameter, gov.hmrc.income_tax.reliefs.other_income_first: false until 2027-04-06, true from then.

The model now shares the deductions this way in every year it covers. Before 2027-28, who gains depends on the starting rate limit, the personal savings allowance, the dividend allowance and the rate limits. The model sets the dividend allowance at £5,000 in 2016-17 and 2017-18 and £2,000 from 2018-19 to 2022-23 (dividend_allowance.yaml).

In 2027-28, take property income of £20,000, dividends of £200,000 and a £1,000 trading loss. Setting the relief against property income saves 24% on property income in the basic rate band (22% under FA 2026 s. 7). It also moves £1,000 of dividends down into the 10.75% ordinary rate band, because the dividends start where the property income ends and still end at £219,000. Setting it against dividends leaves that boundary where it is. So the model sets the relief against property income and charges £74,710.21, as origin/main does, against £74,842.71, worked by hand, with the relief against dividends. YAML case 4 works the arithmetic.

Deductions the model shares out

At Steps 2 and 3 the model deducts:

  • trade_loss_relief_against_general_income (s. 64, a Step 2 relief). Parliament made s. 25(2) subject to the priority rule in s. 65(2) to (4) (s. 25(3)). Under that rule the taxpayer's claims follow a set order, but nothing ties the relief to a component, so the model sets it wherever it saves the most tax, after other income from 2027-28.
  • allowances, the sum of:
    • personal_allowance and blind_persons_allowance, the Step 3 allowances (Part 3 Chapter 2);
    • the Step 2 reliefs charitable_investment_gifts (Part 8 Chapter 3), covenanted_payments and other_deductions;
    • pension_contributions_relief, which the model holds on non-savings income (below).

Each part except pension relief can come off any component, and the tax depends only on how much comes off each. So the model shares out their sum, up to total income (s. 25(4) and (5)).

Pension relief stays on non-savings income. The model takes employee contributions as a net pay arrangement and personal contributions as relief at source (pension_contributions_relief, net_pay_pension_relief). Under a net pay arrangement the employer deducts the contributions from employment income (FA 2004 s. 193(2)). Under relief at source the payer keeps basic rate tax on the contribution (s. 192(1)) and, on a claim where higher or additional rate tax is chargeable, Parliament raises the basic and higher rate limits by the contribution (s. 192(4)). Neither gives a Step 2 deduction that the taxpayer can set against savings or dividends; only a net pay excess relieved on a claim (s. 193(4) and (6)) or relief on a claim under s. 194(1) does. The model gives both kinds as a deduction from income, so the search holds min(pension relief, non-savings income) on non-savings income and shares only the rest. YAML cases 14 and 15 pin it: pay £40,000, £10,000 of pension contributions and savings £30,000 in 2026-27 give £11,332, the tax on pay of £30,000 and savings of £30,000. Worked by hand: 17,430 × 20% = 3,486 on the pay; savings from £17,430 to £47,430, £500 at the nil rate, 19,770 × 20% = 3,954 and 9,730 × 40% = 3,892. Every share keeps the savings allowance below the £37,700 limit, so no share lowers that tax. Sharing the pension relief too would charge £11,232. From 2027-28 the hold matters only where the relief exceeds income other than property, savings and dividend income, since the model otherwise deducts it from that income first, as s. 25(3A) requires of the reliefs and allowances in Steps 2 and 3.

How the model finds the least tax

The model shares the deductions between three components: the first (non-savings income before 2027-28, property income after), savings and dividends. Write x for the first component left, w for x plus the savings left, and T for the Step 3 income of the three, which no share changes. Savings sit above non-savings income and dividends above savings (ss. 16 and 16A), so:

  • tax on the first component changes slope where x crosses a rate limit;
  • the starting rate (s. 12) and the savings nil rate (s. 12A) cover savings from q up to max(q, L) + A, where q stands for the non-savings income below the savings, L for the starting rate limit and A for the savings allowance. Their slope changes where q reaches L, where max(q, L) + A crosses a rate limit, and where w reaches the top of that band: on the line w − x = A, or w constant below L;
  • the taxed savings change slope where w crosses a rate limit;
  • the dividend nil rate (s. 13A) covers the lowest £500 of dividends, so dividend tax changes slope where w + £500 crosses a dividend rate limit or reaches T;
  • the limits on the components add x = 0, x = the whole first component, w − x = 0, w − x = all the savings, w = T and w = T less the dividends.

So the Step 5 tax runs continuous and linear on each piece of the feasible polygon that vertical (x constant), horizontal (w constant) and diagonal (w − x constant) lines cut. A linear function takes its least value on a polygon at a corner, so the least tax lies where two of the lines cross. policyengine_uk/utils/income_tax_allocation.py lists every crossing, 173 a person in 2026-27 and 2027-28 from 11 vertical, 10 horizontal and 3 diagonal lines. It projects each onto the feasible polygon and evaluates the Step 5 tax at all of them, for all the people searched at once. For a Marriage Allowance recipient, it takes off the reduction each share wins (below).

Before 2027-28 a reform can add an exclusion E from earned income, such as one that exempts pensions (uc_earnings_deductions.yaml runs one). With property income P, the savings then sit on q = x up to x = P, on q = P up to x = P + E, and on q = x − E beyond. The search adds the lines of each piece: the two boundaries, the rate limits moved by P and E, and the diagonal lines that a fixed q = P gives. That raises the count to 1,034 a person, and only where someone has an exclusion. YAML case 17 pins the result: under that reform, pay £5,000, private pension £10,000 and savings £30,000 give £3,286, against £4,800 under the old order. With the pension and £5,000 of the allowance setting earned income at nil, £22,430 of savings start at nil: £5,000 at the starting rate, £1,000 at the savings nil rate and 16,430 × 20% = 3,286.

The search relies on three things that hold in every modelled year: a starting rate limit at or below the basic rate limit, a starting rate band that shrinks by a pound for each pound of non-savings income (s. 12), and a first UK rate threshold at nil. A reform that sets that threshold above nil leaves savings below it untaxed and adds lines on which x + w stays constant, which the search doesn't list. On 6,520 synthetic people with that threshold at £2,000, a 101 × 101 grid found less tax than the search for 32 in 2026-27 (by up to £181.70) and 38 in 2027-28 (by up to £214.75). The fast path sends everyone to the search under such a reform, and the search still keeps the old order unless a share it lists charges less.

Parliament gives the Marriage Allowance as a tax reduction at Step 6 (ss. 23, 26 and 55B(1)), and since #2828 the model does too (marriage_allowance_tax_reduction). Parliament allows it only to a recipient who pays tax at no rate outside the list in s. 55B(2)(b), which includes the savings nil rate. It also bars a recipient who would pay the dividend upper or additional rate without the dividend nil rate (s. 55B(2)(ba)). The model applies that rate test to non_savings_income_after_allowances and both allocation variables, so the share decides who qualifies. Under s. 25(2) the taxpayer deducts the reliefs and allowances in the way that most reduces their liability, and Parliament fixes liability after the Step 6 reductions (s. 23). So the search minimises the Step 5 tax less the reduction each share wins.

For example, take a married recipient in 2026-27 with pay of £40,000, savings of £9,000 and dividends of £1,570, and a partner with no income. Setting the whole Personal Allowance against pay gives the least Step 5 tax, £7,376.025, but leaves £300 of dividends above the basic rate limit, so the recipient fails the rate test. Setting £2,500 against pay, £8,500 against savings and £1,570 against dividends leaves £37,500 of pay, £500 of savings at the savings nil rate and no dividends. The Step 5 tax comes to £7,500, and income tax, after the £252 reduction, to £7,248 (YAML case 19).

The search finds that share as it finds the least Step 5 tax. Where all the income left after the deductions exceeds the basic rate limit, a recipient with dividends left fails the rate test, so a qualifying share lies on the line w = T. There the nil rates must also cover the savings above the limit. Under current rates that holds only where savings left come to at most A (w − x ≤ A). Under a reform that sets the basic rate limit below L + A, it can hold along the whole of w = T. The search already lists both lines. For the test's limit on non-savings income, it adds one vertical line where non-savings income reaches that limit. On each side of those edges the Step 5 tax less the reduction differs from the Step 5 tax by a constant, so its least value again lies where two lines cross. The new line raises the count to 186 a person in 2026-27 and 2027-28, and to 1,061 with an exclusion from earned income, wherever anyone searched has a reduction at stake.

The model stores its variables in float32, so the search counts a share as meeting the rate test only with room below each limit. The room comes to a tenth of a penny plus a millionth of the largest amount the test compares, or 3.9p at £38,000. Where a share leaves savings at the allowance or no dividends, the search writes in those amounts rather than computing them, so rounding can't push them across the test's limits. It judges the old order by the test's own arithmetic, without that room. So a recipient who already qualifies under the old order moves only for a lower Step 5 tax (YAML case 23). marriage_allowance_reduction_at_stake gives the search the reduction whatever the share: the appropriate percentage of marriage_allowance_if_income_conditions_met, which reads neither the income conditions nor the share. reforms/cps/marriage_tax_reforms.py gives the allowance at any share to people who meet its expanded conditions, and sets their reduction at stake to nil. For them, the search follows the Step 5 tax alone.

We built 2,640 recipients married to partners with no income, on a grid of pay, savings, dividends and property income in London and Scotland in 2026-27 and 2027-28. For each, we forced the old order and an 11 × 11 grid of shares through the model's own variables at 6c91a3c. No recipient pays more income tax than under the old order or under any feasible grid share. Against the old order, 73 pay less, by up to £272, and 8 gain the allowance it denies, while none lose it. Six of the 8 pay more Step 5 tax than some grid share does, to win the allowance, as in the example above. All 73 live in London and gain in 2026-27. From 2027-28 the taxpayer deducts first from pay (s. 25(3A)), and no grid recipient gains that year, though YAML case 22 shows a gain from property income. In these years no Scottish taxpayer can win the allowance through the share. The Scottish limit on non-savings income trails the basic rate limit by more than the savings allowance, so no share meets the test where the income left exceeds the basic rate limit. Below that limit, the old order already leaves the least non-savings income. At 321fb19, whose search left the reduction out, 69 paid less, 2 gained the allowance, and 6 paid up to £212 more than a grid share.

At 5d9ce13, before #2828, the model subtracted the allowance from earned income (#1947), and the search left the allowance out. At that head's model code we built 20,364 married couples a year by hand, 12,277 of them with a recipient. No recipient paid more income tax than under the old order in 2026-27 or 2027-28, and 1,069 and 1,155 recipients paid less, by up to £645. On a merge of 5d9ce13 with f0f576a, from #2828's branch, no couple paid more than under the old order, and 1,119 and 1,200 recipients paid less, again by up to £645. In 2026-27 the merged model gave the allowance to 12 more couples than the old order did, and to none fewer.

Where several shares give the same tax, the model takes the one setting most against the first component, then most against savings: the nearest the least tax allows to the old order. It keeps the old order, to the float32 bit, unless another share saves more than £0.001.

Who skips the search

The search runs only on people three tests leave and on Marriage Allowance recipients whom another share might qualify (below). Each test gives the result the search would give.

  1. One share. With no deduction left to place under s. 25(2), or no income left after the deductions, only one share exists, and the model keeps the old order.
  2. Nothing charged above the first component. The old order sets every shared deduction against the first component, so any other share leaves more of it, and tax on it rises with x wherever the rates rise with income. Where the old order also leaves no savings and no dividends charged above the nil rates, another share can only add tax.
  3. Rates. Moving a pound of the deductions from the first component to savings or dividends raises x by that pound and w by at most a pound. The top of the nil rates moves by at most that pound too: the starting rate band shrinks by at most a pound for each pound of non-savings income (s. 12), and the savings and dividend nil rates cover fixed amounts (ss. 12A and 13A). Write ρn for the rate on the first component just above where the old order leaves it, ρs for the savings rate just above the top of the savings, and σs and σv for the highest savings and dividend rates the moved pounds can reach. Any move raises the tax by at least (ρn − σs) dx + (ρs − σv) dw, with 0 ≤ dw ≤ dx, or ρn dx − σv dw where the old order charges no savings above the nil rates. Where that bound never falls below zero, the old order gives the least tax. Before 2027-28, where a reform adds an exclusion from earned income, the test also needs no property income.

Tests 2 and 3 apply only when every rate scale rises with income from nil; under a reform that makes a scale fall, or sets the first UK threshold above nil, the model sends everyone with something to share to the search. The model also sends to the search each Marriage Allowance recipient whom the old order denies the reduction, where the least non-savings income any share leaves falls within the rate test's limit (_marriage_allowance_in_reach).

For people the tests clear, the search would keep the old order too, since it moves the share only for a saving above £0.001. A property test checks the two paths give the same deductions, bit for bit, on synthetic people with rising and with falling rates, with pension relief among them. Another does the same with Marriage Allowance recipients among them. Two new tests pin the checks that no earlier test separated: a supplied adjusted net income with trade loss relief, where the full search saves £200 and £60 over the old order, and a reform that lifts the Scottish starter band to £9,000, where it saves £30.

Cost

The search adds work only for people it can't skip. On 20,000 synthetic people of the "realistic" mix in test_income_tax_allocation.py, most with pay alone and some with savings, dividends, property income or pension relief, the three tests send 0.925% of them (185 of 20,000) to the search in 2026-27 and 0.635% (127 of 20,000) in 2027-28, at 70633f0. Timings from earlier commits, on a hand-built mix of 20,000 people without a simulation: at 60de330 the deductions took 5.5 ms with the tests against 4.6 s with the search for everyone in 2026-27, and 3.4 ms against 2.5 s in 2027-28. At 5d9ce13, a cold income_tax for one household (pay £50,000 and savings £10,000, 2026-27) took 0.907 to 1.041 s of CPU time in three runs against 0.898 to 1.026 s on origin/main, and the allocation variables took 2.6 to 4.6 ms against 0.35 to 0.45 ms. The pension hold adds one column to the search, and the exclusion lines run only for people with an exclusion. For the Marriage Allowance, the search adds a line, and the model sends it the recipients a share might qualify. Nobody has timed 6429896 or later.

Aggregate impact

At 9f33c78, CI's Aggregate impact report (run 42716260318) compares main at 93b1711e57 with 6f1da93e51, CI's merge of 9f33c78 into that main. CI runs a full microsimulation for each side and year on enhanced_frs_2024_25.h5 (policyengine-uk-data 1.58.1). The change breaks none of the report's checks, one check fails on both sides, and the report flags no move. We estimate that income tax receipts fall, and household net income rises, by £18.6m in 2025-26, £19.6m in 2026-27, £9.5m in 2027-28, £10.7m in 2028-29, £20.4m in 2029-30, £20.6m in 2030-31 and £11.3m in 2031-32. Between 0.9% (2025-26) and 0.3% (2027-28, 2028-29 and 2031-32) of people gain over £1, and 0.0% lose over £1 in every year. Councils spend £1,126 to £1,301 a year less on Council Tax Reduction from 2027-28, and poverty rates don't change in any year.

At 6602ccc, before this branch took in #2828, we estimated that income tax receipts would fall, and household net income would rise, by £18.7m in 2025-26, £19.5m in 2026-27, £9.5m in 2027-28, £10.7m in 2028-29, £20.4m in 2029-30, £20.6m in 2030-31 and £11.3m in 2031-32. Between 0.9% (2025-26) and 0.3% (2027-28, 2028-29 and 2031-32) of people gained over £1, and 0.0% lost over £1 in every year.

CI's Aggregate impact report (run 41535543658) compares main at 0080274d78 with 3b1d34afc6, CI's merge of 6602ccc into main, on enhanced_frs_2024_25.h5 (policyengine-uk-data 1.58.1), a full microsimulation for each side and year. It passes, with 0 checks broken and 1 failing on both sides. It flags two moves, both in 2030-31 and on mean income in each side's own deciles: decile 5 rises £140 and decile 6 falls £114. By base decile, neither decile moves that year.

Readers of the changed variables

Every output variable keeps its name. git grep finds these readers:

  • received_allowances_savings_income and received_allowances_dividend_income now come from the search. Their readers (basic_rate_savings_income, higher_rate_savings_income, add_rate_savings_income, basic_rate_savings_income_pre_starter, savings_starter_rate_income, dividend_income_tax, taxed_dividend_income, received_allowances) don't change.
  • received_allowances_earned_income (allowances against non-savings income beyond trade loss relief) and trade_loss_relief_against_savings_and_dividends now follow from those two. They equal the old order's values wherever the share equals the old order.
  • non_savings_income_after_allowances now comes from the allocation's own arithmetic: where the share keeps the old order, net income less savings, dividends and allowances, as origin/main computes it; elsewhere, in float64, the non-savings income less the deductions the share sets against it. earned_taxable_income adds back the relief set against savings and dividends, because net_income deducts all of it, and never exceeds non_savings_income_after_allowances while savings, dividends and received_allowances_earned_income sit on its exclusions list. Without the add-back, the model would undercount non-savings income wherever the share set relief against savings or dividends while some non-savings income remained (YAML case 16). Without the float64 arithmetic, float32 rounding in net income plus the relief added back left a residue above nil (YAML case 18). taxable_property_income_after_allowances, property_income_tax, non_savings_taxable_income (through earned_taxable_income and taxable_property_income_after_allowances), earned_income_tax and the basic, higher and additional rate earned income variables read them as before.
  • adjusted_net_income and net_income sum totals that no share changes, so neither moves. Nor do the Personal Allowance taper and the savings allowance band (savings_allowance reads net income less allowances).
  • meets_marriage_allowance_income_conditions reads non_savings_income_after_allowances and both allocation variables, so it follows the share, and the search weighs the reduction it decides (above). For Scottish taxpayers, tax_band reads non_savings_income_after_allowances and so follows the share too; no variable reads tax_band.
  • Universal Credit: utils/uc_income_tax.py (uc_income_tax_on_earnings, uc_income_tax_on_trade) takes earnings as the lowest slice of non-savings income, with trade loss relief off the other non-savings income first and allowances off earnings first. It reads the old order's deductions (default_order_deductions), not the share, and takes off the Step 6 reductions that earnings_tax_reductions counts, the Marriage Allowance among them. So the slice equals origin/main's wherever the share leaves those reductions unchanged, and it rises with more savings or dividends only where they cost the person a Step 6 reduction. For example, a recipient with pay of £37,000 and a partner with no income has a slice of £4,634 in 2026-27. With £20,000 of dividends, the recipient fails the rate test, and the slice rises to £4,886. For self-employment income of £37,406 and savings of £14,407 in 2026-27, the share moves the whole Personal Allowance onto the savings and income tax falls from £8,057.20 to £8,016, while uc_income_tax_on_trade stays at £4,967.20 (74fdacf gave £7,481.20). For pay £50,000 and savings £10,000 it stays at £7,486. See the Universal Credit judgment call for why the slice keeps the old order.
  • The legacy means tests read no allocation variable. Income Support, Housing Benefit, Pension Credit and Council Tax Reduction deduct legacy_means_test_deducted_income_tax. It takes the income tax before the winter fuel payment charge and subtracts what the tax on rent, interest and dividends and the other Step 7 charges would come to if nobody had earnings. The model takes the least-tax share in both computations, so the deduction moves by the difference between what the share saves in each; it falls £100 in the child benefit charge case below. The earnings disregards and the Housing Benefit childcare limit read net earnings from housing_benefit/applicable_income/_net_earnings.py. It takes off a pro-rata share of the income tax before the winter fuel payment charge, so net earnings there rise where the share lowers the tax.
  • property_finance_costs_relieved computes adjusted total income (its reference: ITTOIA 2005 s. 274AA(6)) as net income less all the savings and dividend income, charitable_investment_gifts, other_deductions and the personal and blind person's allowances. It doesn't read the share, so it doesn't change (see judgment calls).

Invariants

Invariants 1 to 7 hold for every person with no Marriage Allowance reduction at stake, on the Step 5 tax (income_tax_pre_charges), the only tax the share then changes:

  1. Least tax: no share of the deductions that the taxpayer may make under s. 25 gives less Step 5 tax than the model's.

  2. Never more than the old order: the tax never exceeds what the order non-savings, savings, dividends charges, apart from float32 rounding.

  3. Accounting: the deductions against non-savings income (received_allowances_earned_income plus the relief not set against savings and dividends), savings and dividends add up to min(relief + allowances, income). None falls below zero or exceeds its component, and the relief set against savings and dividends never exceeds what they take.

  4. One kind of income: with one kind of income nothing can move, and the tax and the deductions equal the old order's.

  5. Differential: the search's own Step 5 function (step_5_tax) equals income_tax_pre_charges whenever a test forces a share through the model.

  6. Search exactness: the vertex search's value never exceeds a dense grid's minimum of that function, and comes within the grid's step of it, now also with exclusions from earned income beside property income before 2027-28.

  7. Fast path: for everyone the tests clear, the deductions equal the full search's, bit for bit.
    Invariants 8 to 12 hold for every Marriage Allowance recipient with a reduction at stake, whose share also decides the reduction at Step 6:

  8. Never more than the old order: income tax never exceeds what the order non-savings, savings, dividends charges, apart from float32 rounding.

  9. Least liability: forcing any share through the model's own variables never gives less income tax than the model's share, apart from float32 rounding.

  10. Float32: where the search moves a share and counts it as meeting the rate test, the model's own variables meet the test at that share.

  11. Search exactness: the vertex search returns a feasible share, and no share on a 101 × 101 grid gives a lower Step 5 tax less the reduction, beyond £0.00001. Nor does any share on the line with no dividends left or the line with savings left at the savings allowance.

  12. Fast path: invariant 7 holds with recipients among the people.

Invariants 13 to 15 hold for everyone:

  1. Pension relief: the share sets pension relief against savings or dividends only where it exceeds non-savings income, as the old order does.
  2. Universal Credit: adding savings or dividend income never raises uc_income_tax_on_earnings or uc_income_tax_on_trade for a person whose allowances and Step 6 reductions it leaves unchanged.
  3. Budget 2026 rates: income tax under current law never exceeds income tax under the Budget 2025 savings and property rates plus 2% of the banded savings and taxable property income under those rates, whatever share either takes.

On invariant 13: pay £2,000, personal pension contributions £3,600 and dividends £30,000 in 2026-27 give received_allowances_earned_income of £2,000 and received_allowances_dividend_income of £14,170, which includes the £1,600 of pension relief beyond the pay, and income tax of £1,647.975, at 70633f0 and on origin/main 6c6e229 alike.

Test plan

Ran at 6c91a3c (Python 3.13.9; no survey dataset; a 2 GiB memory cap on each process). 9f33c78 merges in origin/main 93b1711. Since the branch last merged main, main has changed 20 files, on carer's benefits, student loans and Universal Credit conditions, and this PR changes none of them.

  • All 257 YAML cases in the allowances directory pass, all 23 of least_tax_allocation.yaml among them, and so do all 8 in step_6_step_7.yaml and all 9 in legacy_means_tests/income_tax_caused_by_earnings.yaml. All 336 cases pass in the 16 other YAML files that involve the Marriage Allowance, among them property_income_band_base.yaml, gift_aid.yaml, scottish_rates.yaml and the CPS reform's marriage_tax_reforms.yaml.
  • With each test function in its own process, all 31 tests in test_income_tax_allocation.py, both in test_uc_income_tax_properties.py, all 3 in test_marriage_allowance_tax_reduction_properties.py and all 21 in test_income_tax_step_6_step_7_properties.py pass. In test_marriage_allowance_cps_switch.py 45 pass; its two Hypothesis tests over the statutory rate run past the 2 GiB cap, so CI runs them.
  • ruff 0.17.2 format --check, check and check --select F811,F821 pass on the 11 changed Python files, git diff --check reports nothing for either commit, and no line the PR adds to a Python file runs past 88 characters.
  • .github/check_fix_pr.py reports "Fix evidence: PASS" on this title and body, with the changes from origin/main 93b1711 to 9f33c78 and 18 changed executable tests.
  • None of the 2,640 hand-built recipients above breaks invariant 8 or 9.
  • We drew synthetic people in nine years between 2016-17 and 2030-31, about three in four of them with a reduction at stake. For 277,800 of them the search comes within 5.82e-11 of the least Step 5 tax less the reduction on the 101 × 101 grid and the two lines. Weighing the reduction moves 2,032 of their shares. The fast path and the full search give the same deductions, bit for bit, for 2,982,870 people. We also took 820 recipients in each of 2026-27 and 2027-28, in London, Scotland, Wales and the North East. For each, we forced the old order and an 11 × 11 grid of shares through the model's own variables, and none breaks invariants 8, 9 or 10.

Each mutation below makes one change to 6c91a3c's model code:

Change Result
The fast path never sends a recipient to the search (no _marriage_allowance_in_reach) test_recipient_share_gives_the_least_income_tax fails in 2026
The search minimises the Step 5 tax alone least_tax_allocation.yaml fails
No line at the rate test's limit on non-savings income test_vertex_search_weighs_the_marriage_allowance fails in 2027
The search judges the old order with the room test_search_keeps_a_recipient_the_test_passes_exactly_at_the_limit fails: the search sets £10,700 of the allowance against savings, not nil
No reform override on the reduction at stake test_the_share_weighs_the_allowance_only_where_the_share_decides_it fails, with £252 at stake rather than nil
The old order never meets the rate test least_tax_allocation.yaml fails
The search weighs the reduction twice test_search_keeps_a_recipient_the_test_passes_exactly_at_the_limit fails
The rate test ignores dividends least_tax_allocation.yaml fails
The rate test ignores savings meets_marriage_allowance_income_conditions.yaml fails
No room below the limits, or no exact setting of savings and dividends (all of it, the dividends alone, or the savings alone) All four survive. On 144,000 recipients we drew at the limits, no one of them, and not all four together, broke an invariant; only removing the room changed shares, moving 2 more people and counting 7 more shares as meeting the test
The in-reach check ignores other income below the first component Survives: equivalent. Where s. 25(3A) leaves other income after the deductions, nothing remains to share, so the fast path already keeps the old order

Ran at 056a674 (uv sync --frozen --extra dev; no survey dataset; HUGGING_FACE_TOKEN and POLICYENGINE_UK_DEFAULT_DATASET unset):

  • test_trading_loss_properties.py and test_statutory_paternity_pay_income_tax_properties.py: all 20 pass with -n 4, and all 20 without xdist.
  • test_income_tax_allocation.py and test_uc_income_tax_properties.py: all 24 pass.
  • The income tax YAML directory: all 490 cases pass. least_tax_allocation.yaml: all 18 pass. trade_loss_relief_against_general_income.yaml: all 22 pass.
  • policyengine_uk/tests/code_health with -n 4: all 3,089 tests pass.
  • .github/check_fix_pr.py on this title and body, with the branch's changes from its merge base with origin/main 6c6e229 (2da212a): "Fix evidence: PASS", with 15 changed executable tests.
  • uvx ruff@0.17.2 format --check . ("2167 files already formatted"), uvx ruff@0.17.2 check . and uvx ruff@0.17.2 check --select F811,F821 policyengine_uk pass, and git diff --check 70633f089..HEAD reports nothing.
  • income_tax_allocation.py changes only in a docstring and a comment: its AST, with docstrings blanked, matches 70633f0's.
  • Mutation checks on a scratch copy of the package, each run on test_relief_never_taxes_more_than_income_cut_in_the_old_order without xdist and then restored:
    • The old two-sided assertion for everyone: fails on pinned person 1 (pay £5,000, savings £10,000, dividends £30,000, loss £20,000), with income tax of £637.47 against £1,282.47 for the twin.
    • net_income subtracting trade_loss_relief_against_general_income twice, with the relief itself unchanged: the one-sided bound passes for everyone, and the check for people with no savings or dividend income fails on pinned person 3 (pay £300,000, loss £200,000), with £53,703 against £87,453. With that check removed, the test passes under the mutant.

At 3b54d53: income_tax_allocation.py differs from 056a674 only in one comment, and its AST, with docstrings blanked, matches 056a674's, so the runs above hold there. At that head CI passes all seven Python shards, YAML, Lint, Fix evidence and Aggregate impact, uvx ruff@0.17.2 format --check ., check . and check --select F811,F821 policyengine_uk pass, and .github/check_fix_pr.py gives "Fix evidence: PASS" with 15 changed executable tests.

At 6602ccc: income_tax_allocation.py differs from 3b54d53 only in its module docstring and that comment, and its AST, with docstrings blanked, matches 3b54d53's, so the runs above hold for this head. At this head CI passes all seven Python shards, YAML, Lint, Fix evidence and Aggregate impact.

Ran at 70633f0 (uv sync --frozen --extra dev; no survey dataset):

  • uvx ruff@0.17.2 format --check ., uvx ruff@0.17.2 check . and uvx ruff@0.17.2 check --select F811,F821 policyengine_uk pass, and so do uvx ruff@0.17.0 format and check on the changed Python files and git diff --check. No line the branch adds to a Python file runs past 88 characters.
  • policyengine_uk/tests/code_health: all 3,089 tests pass.
  • The income tax YAML directory: all 490 cases pass, all 18 of least_tax_allocation.yaml among them.
  • test_income_tax_allocation.py: all 22 pass. test_uc_income_tax_properties.py: both pass.
  • .github/check_fix_pr.py on this title and body, with the branch's changes against origin/main: "Fix evidence: PASS", with 13 changed executable tests.
  • Witnesses, at 70633f0 and on origin/main 6c6e229:
    • pay £20,000, personal pension contributions £5,000 and savings £5,000 (2026-27): income tax £772 on both; the same with employee (net pay) contributions: £772 on both (see the pension judgment call);
    • the invariant 13 witness above: £1,647.975 on both;
    • pay £50,000 and dividends £20,000 in 2027-28: all £12,570 of the allowance against pay and income tax of £14,457.25 on both; in 2026-27 the share sets £270 against dividends and charges £14,414.725 here, against £14,457.25 on origin/main.
  • On 6,520 synthetic people (synthetic_people, seeds 100 to 119, 326 each) with uk[0].threshold at £2,000, vertex_search_and_grid finds less tax on the grid than from the search for 32 people in 2026-27 (by up to £181.70) and 38 in 2027-28 (by up to £214.75), and for no one below the grid minimum less two steps. Seeds 0 to 3 give 6 of 1,304 by up to £91.86.
  • The fast-path share on the "realistic" mix (seed 2106, 20,000 people): 185 to the search in 2026-27 (0.925%) and 127 in 2027-28 (0.635%).

Ran at def18d3, whose model code 70633f0 keeps (70633f0 changes only docstrings, comments and one variable's documentation):

  • policyengine_uk/tests/code_health: all 3,089 tests pass.
  • The income tax YAML directory: all 490 cases pass.
  • test_income_tax_allocation.py (22) and test_uc_income_tax_properties.py (2): all 24 pass.
  • uc_earnings_deductions.yaml (15) and integration/income.yaml (8): all 23 pass.

Ran on the working tree as it stood at 6429896 or one step before it (uv sync --extra dev, Python 3.13.18, 4 vCPU; no survey dataset). The earlier tree lacked only YAML case 18 and the cap that holds pension relief within allowances, which can't bind in the model because allowances includes the relief; def18d3 keeps 6429896's code and renames one line of shard_durations.toml. The runs at def18d3 above repeat the ones that touch case 18 or the allocation tests:

  • The income tax YAML directory (policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax) passes 489 cases, integration/income.yaml 8, the Universal Credit income directory (with uc_earnings_deductions.yaml) 174, and uc_trading_loss.yaml, uc_company_analogous_business.yaml and the benefit cap directory 150.
  • test_income_tax_allocation.py: 22 tests pass, including 7 new: the dense-grid search with exclusions in 2018-19, 2026-27 and 2027-28, two supplied adjusted net income cases, the Scottish starter band reform and the pension hold.
  • test_uc_income_tax_properties.py (new): both tests pass, including the Hypothesis property with the self-employment witness as an explicit example in 2025-26 and 2026-27.
  • test_budget_2026_savings_property_rates_properties.py: all 8 tests pass with -n 4 and without, including the new bound (invariant 15).
  • test_scottish_top_rate.py 10, test_savings_starting_rate_indexation.py 26 and test_property_income_rate_reforms.py 55 pass.
  • test_gift_aid.py, test_property_income_band_base_properties.py, test_income_tax_step_6_step_7_properties.py, test_adjusted_net_income_pension_relief.py, test_income_concepts_properties.py, test_uc_company_owner_properties.py and test_pension_annual_allowance_properties.py: all 63 tests pass.
  • A differential against origin/main 2da212a on 3,000 random single adults a year in London and Scotland, 2025-26 to 2028-29 (12,000 rows), with pay, self-employment, property income, savings, dividends, employee and personal pension contributions, trading losses and Gift Aid:
    • income_tax_pre_charges falls by more than 1p plus a millionth of the tax in 725 rows, by up to £645, and rises beyond that in none (float32 rounding explains the largest rise, £0.0078).
    • uc_income_tax_on_earnings and uc_income_tax_on_trade move by more than 1p in no row. At 74fdacf the two slices rose 319 times over the 12,000 rows, counting each slice separately, by up to £4,930.98, and fell in none.
    • adjusted_net_income moves in no row.
    • Against 74fdacf, the pension hold raises the tax by more than 1p in 51 rows, by up to £108: in 2025-26 and 2026-27 by up to £100 for employees and people with relief at source, and in 2027-28 and 2028-29 for Scottish taxpayers with property income, savings and relief at source but no other income, where 74fdacf set the relief against savings at 44% rather than property income at 42%. In each of those rows the tax equals or falls below origin/main's.
  • Float32 residue, on 6,000 hand-built people whose trading loss exceeds their trading income, at scales up to £50 million: of the 2,373 whom origin/main leaves with no non-savings income, 74fdacf left 607 above nil (up to £4.00) and 6429896 leaves 10 (up to £0.0078, rounding in the moved share); earned_taxable_income went from 63 above nil to none.
  • A scratch merge with tax_band compares a Scottish taxpayer's total income with the Scottish thresholds; the Scottish rates apply to non-savings, non-dividend income only #2350, checked for a dependency cycle (see Merge notes).

Revert and mutant checks, each on a copy of the package as it stood at 6429896 or one step before it (the earlier copies lacked only the cap that holds pension relief within allowances):

Change Result
Pension relief shared again Cases 14 and 15 fail (received_allowances_earned_income 2,300, not 22,570), and so does test_pension_relief_stays_on_non_savings_income
earned_taxable_income add-back removed Case 16 fails (earned_taxable_income 30,000, not 37,700)
Exclusion lines removed Case 17 fails (received_allowances_earned_income 7,300, not 5,000), and test_vertex_search_equals_dense_grid_with_exclusions fails in 2018 and 2026
UC slice reading the share Both tests in test_uc_income_tax_properties.py fail (the slice rises by £2,514)
non_savings_income_after_allowances from the add-back sum, with no cap on earned_taxable_income Case 18 fails (£0.0078125, not 0)
F6_old_order_check_dropped Both test_fast_path_follows_a_supplied_net_income cases fail
F11_all_used_threshold_wide test_fast_path_follows_a_supplied_net_income[all-savings-used] fails
F8_uk_rates_for_scots test_fast_path_reads_the_scottish_starter_rate fails
F10_all_used_without_old_order_check Survives: equivalent. With Step 3 income at or below £0.000001, every share's Step 5 tax falls below the £0.001 tolerance, so the search keeps the old order whatever the old order sets
F1_shared_within_first_dropped Survives, as expected: in exact arithmetic the old order's in_first already implies shared <= first, so the check matters only for float32 residues and negative components
F1 and F6 together Both supplied adjusted net income cases and test_fast_path_equals_full_search fail

The 74fdacf model code gives £11,232 for cases 14 and 15 and fails both. Under the pension-exempting reform, the search at 74fdacf missed the least tax that a 101 × 101 grid finds for 40 of 5,216 synthetic people with exclusions, in eight years from 2016-17 to 2028-29, by up to £415.14; at 6429896 it misses it for none of 15,648 in the same years.

Ran at 74fdacf, before these changes:

  • test_budget_2026_savings_property_rates_properties.py: all 7 tests pass with -n 4 and without, and the two failing tests pass with the seeds 108342745077855236797573561117658719055 and 245902028163439803096709191647468822378.
  • The same file at 5d9ce13, run against 74fdacf's model code with -n 4 and without the rUK property test: 3 pass and 2 fail, test_savings_tax_rises_by_two_points_in_every_country and test_scottish_property_tax_ignores_the_property_rates, as at 5d9ce13. Both failing people pay Scottish income tax and have property income of about £54,000 and savings; see below.
  • 4,000 random people in 2027-28: 46 break the same-election identities, and the share moves for all 46; none breaks invariant 15.

Earlier runs, at the commits named (each later commit to 74fdacf changed only tests, comments or the Marriage Allowance exclusion, as the commit messages say):

  • 5d9ce13: test_property_income_rate_reforms.py (55) and test_savings_starting_rate_indexation.py (26) passed; 18 other YAML files passed, 271 cases, every other file in the income tax directory with a case of two or more people.
  • 5d9ce13: the mutants A_rate_limit_lines_dropped, B_s25_3A_ignored, C_tie_break_reversed and G1_marriage_allowance_read each failed one of the 13 cases then in least_tax_allocation.yaml; F2, F3, F4, F5, F8b, F9 and F12 failed test_fast_path_equals_full_search, and F7 failed test_fast_path_equals_full_search_when_rates_fall[2026].
  • e500393: test_gift_aid.py (12), test_income_tax_step_6_step_7_properties.py (10), test_property_income_band_base_properties.py (5), test_adjusted_net_income_pension_relief.py (17) and test_income_concepts_properties.py (2) passed.
  • origin/main 2da212a: cases 2, 3, 5, 7 and 8 of the first 11 cases of least_tax_allocation.yaml failed and the other 6 passed.

CI at 9f33c78 (pull_request run 42716260469): Python shards 1 to 7 pass 13,159 tests (61 skipped, 6 xfailed), and Test / YAML passes 6,459. The Python shards run the tests that need survey data on the pinned enhanced_frs_2024_25.h5 (policyengine-uk-data 1.58.1).

  • YAML tests whose expected values come from the legislation, worked by hand, in policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/received_allowances/least_tax_allocation.yaml: both rows of the issue's table; savings £4,000, dividends £200,000 and a £1,000 trading loss; property £20,000, dividends £200,000 and a £1,000 trading loss in 2027-28; two Scottish taxpayers; pay and savings, and pay and dividends, across the basic rate limit; pay, nil-rate savings and dividends across it; one income type (savings only; property only in 2027-28); two Marriage Allowance recipients who already qualify under the old order, one whose taxable income ends at the basic rate limit, three whose share wins the allowance (at a higher Step 5 tax, by clearing dividends and stopping savings at the nil rate, and from property income in 2027-28) and one whose pay alone exceeds the limit (cases 13 and 19 to 23); net pay and relief at source pension relief kept on non-savings income; trading loss relief set against savings with the pay left in earned income; a reform that exempts pensions; trading loss relief past the non-savings income.
  • Lint and format checks pass at 9f33c78 in CI, and on the 11 changed Python files at 6c91a3c with ruff 0.17.2.
  • Changelog fragment changelog.d/2106.fixed.md.

The Budget 2026 property tests and the share

Four tests in test_budget_2026_savings_property_rates_properties.py compare everyone under the Budget 2025 and Budget 2026 rates on the assumption that each person keeps the same allowance order. The least-tax search can move the allowance when the rates change, and at 5d9ce13 two of those tests failed on people it moved. Minimised to one person: a Scottish taxpayer with property income of £54,000 and savings of £13,000 in 2027-28. Under the Budget 2025 rates the allowance saves 42% against either income (Scottish higher rate on the property income, the UK higher savings rate on the savings), so the model keeps it on the property income. Under the Budget 2026 rates savings pay 44% while the property income still pays the Scottish 42%, so the model sets £12,500 of the allowance against savings, leaving £500 within the savings allowance, and £70 against the property income. Income tax stays at £15,904.89, against £16,154.89 had the allowance stayed on property income: the move saves 2% of £12,500. Property tax rises by 42% of £12,500, so the identity "Scottish property tax doesn't move" fails for this person, and so does "savings tax rises by 2% of banded savings". The search gives the least tax under s. 25(2); the tests' assumption was wrong. same_choices limits the identities to people whose election and allowance order stay the same, a pinned test (property £58,000, savings £24,000) checks the move, and invariant 15 checks every person, moved or not.

Existing tests whose expected values move, and why

Each of these expected values moves because the model charges less tax under the least-tax share. Each case's comment works the arithmetic.

  • reliefs/trade_loss_relief_against_general_income.yaml, "Relief takes non-savings income first, then savings, then dividends", becomes "Relief and allowances leave savings that the starting rate and savings allowance cover". Savings take 4,000 (from 10,000) and dividends 23,570 (from 17,570), since savings up to £6,000 bear 0% (ss. 12 and 12A). Income tax falls from 1,282.48 to 637.475.
  • integration/income.yaml, "Savings slice of basic rate band" (2020-21): £2,500 of the allowance against pay and £10,000 against savings keep the pay at the £37,500 limit. basic_rate_savings_income_pre_starter goes from 10,000 to 0, basic_rate_savings_income from 9,500 to 0, higher_rate_savings_income from 5,000 to 4,500 and taxed_savings_income from 14,500 to 4,500. The tax falls by £100, to £9,300. Setting £500 more against the savings leaves it at £9,300; setting £500 more against the pay charges £9,400. received_allowances_savings_income: 10_000 joins the outputs.
  • integration/income.yaml, "Personal allowance caps savings starter rate before dividends.", becomes "Personal allowance goes to dividends rather than savings at the starting rate." Savings take 0 (from 2,570) and dividends 2,570. basic_rate_savings_income_pre_starter and savings_starter_rate_income go from 2,430 to 5,000 and taxed_dividend_income from 4,500 to 1,930. Income tax falls from 483.75 to 207.475.
  • gift_aid.yaml, "Gift Aid extends UK limits for dividends above the basic-rate limit": earned_taxable_income goes from [37,430, 37,430] to [37,700, 38,700], dividend_income_tax from [3,396.25, 3,203.75] to [3,299.725, 2,942.225] and income_tax from [10,882.25, 10,689.75] to [10,839.725, 10,682.225].
  • gift_aid.yaml, "Gift Aid extends UK limits across savings and dividend slices": savings_income_tax goes from [460, 300] to [120, 0], dividend_income_tax from [1,608.75, 1,433.75] to [1,608.75, 1,001] and income_tax from [9,368.75, 9,033.75] to [9,268.75, 8,941].
  • gift_aid.yaml, "Gift Aid raises the basic rate limit for savings above property income": savings_income_tax goes from [5,646, 5,846] to [3,292, 3,692]; the income tax of each falls by £100.
  • test_gift_aid.py::test_zero_gift_preserves_baseline_outputs_bit_for_bit: the fourth rest-of-UK row (pay £30,000, savings £12,000, dividends £18,000, property £9,000) sets £1,300 against pay and £11,270 against savings. earned_taxable_income and basic_rate_earned_income go from 17,430 to 28,700, received_allowances_earned_income from 12,570 to 1,300, received_allowances_savings_income from 0 to 11,270, basic_rate_savings_income from 10,770 to 0, higher_rate_savings_income from 730 to 230, earned_income_tax from 3,486 to 5,740, savings_income_tax from 2,446 to 92 and income_tax from 13,988.25 to 13,888.25. Its Scottish twin doesn't change, because pay above £31,092 would bear 42%.
  • The received_allowances_savings_income.yaml and received_allowances_dividend_income.yaml unit cases fed received_allowances_earned_income and received_allowances_savings_income in as inputs, which the new formulas don't read. They now take the incomes as inputs. Every expected value stays the same except two dividend cases, which move from 0 to 500: the old order would leave £500 of dividends taxed at 8.75%, while £500 more allowance against dividends keeps the dividends left within the £500 nil rate (s. 13A). One received_allowances_earned_income.yaml case replaces an inconsistent net_income input with taxable_employment_income; its expected value doesn't change.
  • property_income_band_base.yaml, "Case 2, savings sit above property income" (2026-27, property income £40,000 and savings £20,000): savings_income_tax goes from 5,846 to 3,692, and income_tax (7,540 + 3,692) joins the outputs. The model sets £2,300 of the Personal Allowance against the property income, which leaves it at the £37,700 basic rate limit, and the other £10,270 against savings, so the £500 savings allowance (s. 12B(3)) covers savings that would bear 40%. Income tax falls by £100.
  • test_property_income_band_base_properties.py:
    • The higher-rate check asserted savings tax above £3,900 for that person; it now pins savings tax of £3,692, income tax of £11,232 and non-savings taxable income of £37,700.
    • The accounting invariant subtracted savings and dividends in full; it now subtracts the savings and dividends left after the allowances set against them.
    • The monotonic invariant tested savings and dividend tax; it now tests income tax, because more property income can lower savings tax. A new test takes property income from £30,000 to £40,000 with savings of £20,000: savings tax falls from £3,800 to £3,692 while income tax rises from £7,286 to £11,232.
    • The file now loads the model once through the cloned_uk_tax_benefit_system fixture, as 76 other test files do.
  • test_budget_2026_savings_property_rates_properties.py: same_choices replaces same_election (above), and two tests join: the pinned Scottish move (property £58,000 and savings £24,000 in 2027-28: savings tax falls from £9,870 to £4,809.20, property tax rises by £5,279.40 and income tax rises by £218.60, against £470 had the allowance stayed on property income) and the bound for everyone (invariant 15).
  • test_trading_loss_properties.py: test_relief_taxes_like_income_cut_in_section_25_order becomes test_relief_never_taxes_more_than_income_cut_in_the_old_order, and its key in shard_durations.toml follows. It asserted that a person's income tax before the winter fuel payment charge with a loss equals, within 50p and float32 rounding, the tax of a twin with no loss whose income the relief cuts in the order non-savings, savings, dividends. That cut, with the allowances set against it in the same order, gives the old order's share, which the model always weighs and keeps unless another share saves more than £0.001, and the Personal Allowance taper reads the same adjusted net income for both. So the tax with the loss can now only match the twin's or fall below it. Pinned person 1 (pay £5,000, savings £10,000, dividends £30,000, loss £20,000) pays £637.47 against the twin's £1,282.47, the figures of the trade_loss_relief_against_general_income.yaml bullet above. The test now bounds the tax with the loss by the twin's for everyone, and keeps the equality for people with no savings or dividend income, for whom every share equals the cut; pinned person 3 (pay £300,000, loss £200,000) runs that check.
  • test_statutory_paternity_pay_income_tax_properties.py::test_a_pound_of_statutory_paternity_pay_can_halve_the_savings_allowance: the rise in income tax before the winter fuel payment charge from a pound of statutory paternity pay on wages of £48,270 and savings of £2,000 (2026-27) goes from £100.40 to £100.20. With the pound, the model sets £1,500 of the Personal Allowance against savings and £11,070 against pay, so pay of £37,201 bears 20% and the £500 of savings left falls within the £500 savings allowance: income tax goes from £7,340 to £7,440.20, against £7,440.40 under the old order. The last pound of Step 3 income would bear 40% but for the savings nil rate, so it still counts as higher-rate income (ITA 2007 s. 12B(8)(b)(ii)), and the allowance stays at £500 (s. 12B(3)).
  • legacy_means_tests/income_tax_caused_by_earnings.yaml, "The High Income Child Benefit Charge the pay brings in is deducted" (2026-27, pay £40,000 and interest £30,000): the model sets £2,300 of the Personal Allowance against the pay, which leaves it at the £37,700 limit, and £10,270 against the interest. The £500 savings allowance then covers interest that would bear 40%. Income tax before the winter fuel payment charge falls from £16,035 to £15,935, legacy_means_test_income_tax rises from £5,486 to £7,540, and legacy_means_test_deducted_income_tax falls from £13,749 to £13,649. Without the pay, the tax stays at £2,286.

Merge notes

Judgment calls

  • Pension relief: the model gives net pay and relief at source both as a deduction from income. Under a net pay arrangement the employer deducts the contribution from employment income (FA 2004 s. 193(2)), so holding it on non-savings income follows the law. Under relief at source Parliament deducts nothing from income: the payer keeps basic rate tax on the contribution (s. 192(1)) and, on a claim where higher or additional rate tax is chargeable, the basic and higher rate limits rise by the contribution (s. 192(4)). Deducting it from the bottom of non-savings income gives the same tax in cases 14 and 15. It doesn't at the starting rate for savings, whose limit Parliament leaves alone in s. 192(4): pay £20,000, personal contributions £5,000 and savings £5,000 in 2026-27 cost £772 in the model, here and on origin/main, against £1,286 under the law (£2,286 less the £1,000 the payer keeps). Where the relief exceeds non-savings income, the model deducts the rest from savings and dividends, as origin/main does. A net pay excess relieved on a claim (s. 193(4) and (6)) or relief on a claim under s. 194(1) could go against savings or dividends (s. 24(1)(a)); the model doesn't separate those, so it holds them too.
  • Adjusted total income: property_finance_costs_relieved still deducts all the savings and dividend income, whatever share the search takes. At Step 2 of ITTOIA 2005 s. 274AA(6), Parliament excludes only "so much of [net income] as is" savings or dividend income, so relief that the share sets against savings or dividends should raise adjusted total income under the law. The model's gap predates this PR, and this PR leaves it.
  • The s. 25(3A) parameter: a boolean parameter records its commencement, so a reform can switch it off or on. No input variable sets it.
  • The objective: the search minimises the Step 5 tax less the Marriage Allowance reduction a share wins. capped_mcad, property_finance_cost_relief, pension_tax_allowance, marriage_allowance_tax_reduction and income_tax_reductions read income_tax_pre_charges only to cap the Step 6 reductions at the tax left, and no reduction except the Marriage Allowance reads a share variable. income_tax_before_winter_fuel_payment_charge charges max(0, Step 5 tax − reductions) + the Step 7 charges, which never rises when the objective falls, so the least objective gives the least liability. Where the other reductions already take the tax to nil, the search can still move the share, with no change in liability.
  • Ties: where shares give the same tax, the model takes the one nearest the old order.
  • Universal Credit: the earnings slice keeps the order non-savings, savings, dividends rather than the share, so it rises with more savings or dividends only where they cost the person a Step 6 reduction. DWP deducts income tax paid "in respect of the employment" (reg. 55(5)(b) of the Universal Credit Regulations 2013) and, for the self-employed, "in respect of any trade" (reg. 57(2) step 3). Neither says how to split a person's tax between earnings and other income, so the model chooses the order, the one PAYE coding follows for employees.
  • A first UK threshold above nil: the search assumes the first threshold sits at nil, as in every modelled year (above).
  • The savings allowance under the share: with a pound of Step 3 income above the basic rate limit that the share leaves at the savings nil rate, Parliament still counts it as higher-rate income (s. 12B(8)(b)(ii): "income on which income tax would be charged at the higher rate, or default higher rate, but for section 12A"), so the allowance comes to £500 under s. 12B(3), as savings_allowance computes from net income less allowances against the basic rate limit, which no share changes.

🤖 Generated with Claude Code

MaxGhenis and others added 16 commits October 31, 2026 22:20
…gives the least tax

ITA 2007 s. 25(2) deducts Step 2 reliefs and Step 3 allowances in the way
that gives the greatest reduction in liability, and from 2027-28 s. 25(3A)
deducts them from income other than property, savings and dividend income
first. The model set them against non-savings, savings and dividend income
in that fixed order. utils/income_tax_allocation.py finds the least-tax
share per person by evaluating the Step 5 tax at every crossing of the lines
where its slope changes.

Fixes #2106.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the least-tax share moves

Base non-savings income on net_income plus trade loss relief, as the default
order does, so a supplied adjusted net income or a reform that sets net
income is followed. Add a 2027-28 case where s. 25(3A) keeps the allowance
against pay. Update the gift aid and integration cases, and the gift aid
snapshot, whose tax falls under s. 25(2), with the arithmetic.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The search now runs only on people that three tests leave: one share
exists (nothing to share, or no income left after the deductions); the
old order sets every shared deduction against the first component and
leaves no savings or dividends charged above the nil rates; or the rate
on the first component just above where the old order leaves it covers
the highest savings and dividend rates the moved deductions can reach.
Each test gives the result the search would, and a property test checks
that on synthetic people, with rising and with falling rates. The search
evaluates its candidates only on the people left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fast path looked up each rate by the last threshold passed, which
goes wrong once a Gift Aid gift lifts the UK basic and higher rate limits
above the £10 million extra bracket: a person whose income sits below the
lifted basic rate limit read as paying the additional rate, so the fast
path could keep the old order where moving deductions saves tax. The fast
path now sums the rate steps the income has passed, the slope of the tax
the search evaluates, and requires every step to be non-negative. The
property test gains that person.

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

Case 12 sets £270 of the allowance against dividends above the basic rate
limit, with £300 of savings between the pay and the dividends inside the
savings allowance. The search finds that share only through the line where
the pay reaches the basic rate limit. The changelog fragment now names the
model as the actor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Property income of £40,000 and savings of £20,000 in 2026-27: under ITA 2007
s. 25(2) the model sets £10,270 of the Personal Allowance against savings,
which keeps the property income at the basic rate limit and puts the £500
savings allowance against savings taxed at 40%. Savings tax falls from
£5,846 to £3,692 and income tax from £11,332 to £11,232.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under ITA 2007 s. 25(2) the model can set allowances against savings or
dividends while property income is left, so:

- the accounting invariant subtracts the savings and dividends left after
  the allowances set against them;
- the monotonic invariant tests income tax, since more property income can
  lower savings tax: property income of £30,000 to £40,000 with savings of
  £20,000 takes savings tax from £3,800 to £3,692 while income tax rises
  from £7,286 to £11,232, which a new test pins;
- the higher-rate check pins savings tax of £3,692, income tax of £11,232
  and non-savings taxable income of £37,700 for property income of £40,000
  and savings of £20,000.

The file now loads the model once through cloned_uk_tax_benefit_system,
as other test files do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The notes now say the model sets the allowances under ITA 2007 s. 25(2),
rather than the section or the allowance doing it. Comments and
docstrings only; the code and the YAML data don't change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ITA 2007 s. 55B(1) gives the Marriage Allowance as a tax reduction, which
Step 6 of s. 23 deducts (s. 26 lists Chapter 3A), not as a deduction at
Step 3. The search now leaves marriage_allowance out of the earned income
exclusions it reads, so the s. 55B(2)(b) rate test can read the share
without a cycle. Case 13 of least_tax_allocation.yaml fails when the search
reads the allowance again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments only: the module docstring, the IGNORED_EXCLUSIONS note and case
13's working.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace passive constructions in the branch's comments, two YAML case
names, a test module docstring and the documentation of
non_savings_income_after_allowances ("is taxed at 20%" becomes "bears
20%", "is left" becomes "remains"). No test input, expected value or
formula changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Scottish taxpayer's property income stays on the Scottish rates while the
Budget 2026 savings rates rise, so the least-tax order can move an allowance
onto savings. Property tax and savings tax then both move, so the identities
in test_budget_2026_savings_property_rates_properties.py hold only for people
whose allowance order the rates leave unchanged. Pin that person with a worked
case, and add them to the fast-path test's fixed people.

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

Pension relief under net pay (FA 2004 s. 193(2)) and relief at source
(s. 192(4)) is no Step 2 deduction the taxpayer can set against savings or
dividends, so the least-tax search now holds min(pension relief, non-savings
income) on non-savings income and shares only the rest.

The income tax that Universal Credit deducts from earnings reads the old
order's deductions, so more savings or dividends never raise it.

Before 2027-28 the search adds the lines that an exclusion from earned
income creates beside property income. non_savings_income_after_allowances
comes from the allocation's own arithmetic, and earned_taxable_income never
exceeds it, so float32 rounding in the relief added back leaves no residue.

New tests: YAML cases for pension relief, the earned income add-back, a
pension-exempting reform and the float32 residue; property tests for the UC
slice, the search with exclusions and the Budget 2026 rate bound; tests
that separate the fast path's supplied-net-income and Scottish starter rate
checks. Docstrings name the actor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6JoK8gQvt3JZzVN18g
The branch renamed test_more_property_income_never_lowers_savings_or_dividend_tax
to test_more_property_income_never_lowers_income_tax, and the checked-in
durations still named the old test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6JoK8gQvt3JZzVN18g
Quote what FA 2004 s. 192(1) and (4) give for relief at source in the
allocation's docstring and comment, YAML case 15 and the pension test.
Give the first-threshold sample's counts, bound the float32 residue at
under a penny, name the model's choice of PAYE coding for the Universal
Credit slice, and note that non_savings_taxable_income reads
non_savings_income_after_allowances through its two parts. No formula
changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N2tyHPRRwcYXiPudqKR4bf
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Aggregate impact on certified data: pass

Base 93b1711e57 against head 6f1da93e51, both on enhanced_frs_2024_25.h5 (policyengine-uk-data 1.58.1, sha256 056e80fd8e91), fiscal years 2025-26 to 2031-32. Each year is a full microsimulation run on each side.

Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 0 (thresholds in report_spec.yaml; a flag does not fail the job).

Workflow run and full report

Notes

  • In the years a figure from fewer than 10 records is suppressed (spending.disabled_students_allowance), every total containing it, and the decile incomes, are rebuilt from their other components, so no published total includes it. Those totals are marked †. A change is withheld where the two sides leave out different figures and can't be lined up.

Moves flagged for review

None.

Receipts by tax head

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Income tax -£18.6m -£19.6m -£9.5m -£10.7m -£20.4m -£20.6m -£11.3m

Spending by benefit and service

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Pension Credit 0 -£0 0 -£0 0 -£3 -£1
Housing Benefit 0 0 -£0 0 0 -£0 0
Council Tax Reduction -£0 -£0 -£1,126 -£1,167 -£1,211 -£1,256 -£1,301
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed

Caseloads

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed

Government balance, each component counted once

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Government receipts, each component once -£18.6m -£19.6m -£9.5m -£10.7m -£20.4m -£20.6m -£11.3m
Government spending -£0 † -£1 † -£1,126 † -£1,167 † -£1,211 † -£1,259 † -£1,301 †
Government balance, each receipt once -£18.6m † -£19.6m † -£9.5m † -£10.7m † -£20.4m † -£20.6m † -£11.3m †

Model totals

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
gov_tax -£18.6m -£19.6m -£9.5m -£10.7m -£20.4m -£20.6m -£11.3m
household_tax -£18.6m -£19.6m -£9.5m -£10.7m -£20.4m -£20.6m -£11.3m
pre_budget_change_household_tax -£18.6m -£19.6m -£9.5m -£10.7m -£20.4m -£20.6m -£11.3m
gov_spending -£0 † -£1 † -£1,126 † -£1,167 † -£1,211 † -£1,259 † -£1,301 †
household_benefits -£0 † -£1 † -£1,126 † -£1,167 † -£1,211 † -£1,259 † -£1,301 †
gov_balance -£18.6m † -£19.6m † -£9.5m † -£10.7m † -£20.4m † -£20.6m † -£11.3m †

Household income totals

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Household net income +£18.6m † +£19.6m † +£9.5m † +£10.7m † +£20.4m † +£20.6m † +£11.3m †
Household net income (HBAI definition) +£18.1m +£19.0m +£9.0m +£10.2m +£19.9m +£20.2m +£11.0m

Inequality

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Gini coefficient, equivalised HBAI income before housing costs +0.0000 +0.0000 +0.0000 +0.0000 +0.0000 +0.0000 +0.0000

Mean household net income in each side's own deciles

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Decile 1: mean household_net_income per household +£0.15 † +£0.18 † +£0.16 † +£0.15 † -£0.40 † +£0.13 † +£0.10 †
Decile 2: mean household_net_income per household +£0.04 † +£0.05 † +£0.05 † +£0.05 † -£1.17 † +£0.04 † +£0.04 †
Decile 3: mean household_net_income per household +£0.03 † +£0.03 † +£0.02 † +£0.02 † +£0.02 † +£0.02 † +£0.02 †
Decile 4: mean household_net_income per household +£0.04 † +£0.15 † +£0.03 † +£0.02 † +£0.01 † +£0.01 † +£0.01 †
Decile 5: mean household_net_income per household +£0.08 † -£0.37 † +£0.06 † +£0.13 † +£0.14 † +£0.12 † +£0.12 †
Decile 6: mean household_net_income per household +£0.05 † +£0.01 † +£0.00 † +£0.00 † +£0.00 † -£0.21 † +£0.02 †
Decile 7: mean household_net_income per household +£0.33 † +£0.82 † +£0.43 † +£0.36 † +£0.38 † +£0.41 † +£0.43 †
Decile 8: mean household_net_income per household +£1.42 † +£3.88 † +£1.40 † +£1.38 † +£1.36 † +£1.36 † +£1.43 †
Decile 9: mean household_net_income per household +£1.32 † -£1.20 † +£0.64 † +£0.81 † +£0.87 † -£5.03 † +£0.03 †
Decile 10: mean household_net_income per household +£2.59 † +£1.89 † +£0.37 † +£0.61 † +£3.46 † -£98 † +£1.34 †

Standard reform costings (change in gov_balance)

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise basic rate by 1pp +£3.3m +£2.8m +£567 -£226 -£1,104 +£403 +£653
Raise higher rate by 1pp -£480 +£415 -£193 +£2,107 -£430 -£886 -£377
Raise personal allowance by ~800GBP/year -£3.0m -£0.2m -£0.3m -£0.1m +£1.3m -£0.3m -£1.6m
Raise child benefit by 25GBP/week per additional child -£0 -£17 -£16 -£31 +£16 +£18 +£20
Reduce Universal Credit taper rate to 20% -£3 +£137 -£10 +£31 -£2 -£8 +£7
Raise Class 1 main employee NICs rate to 10% +£227 -£292 -£313 -£184 -£251 +£290 +£391
Raise VAT standard rate by 2pp -£28 -£41 -£500 +£1,009 +£648 -£775 -£929
Raise additional rate by 3pp -£193 +£1,346 +£29 -£823 -£214 -£1,312 -£142
Raise the Universal Credit standard allowance by 10% -£1 -£1 +£1 +£82 -£10 -£3 +£1
Raise the State Pension by 10% -£0.6m +£0.5m +£0.1m +£0.3m +£1.0m +£1.3m +£1.1m

Standard reforms: change in each reform's target

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise basic rate by 1pp: change in income_tax +£3.7m +£2.8m +£156 +£13 -£199 +£58 -£53
Raise higher rate by 1pp: change in income_tax -£149 +£517 -£3 -£21 -£253 -£243 -£286
Raise personal allowance by ~800GBP/year: change in income_tax -£3.0m -£0.2m -£0.3m -£0.1m +£1.3m -£0.3m -£1.6m
Reduce Universal Credit taper rate to 20%: change in universal_credit 0 +£0 0 +£0 0 0 0
Raise additional rate by 3pp: change in income_tax -£155 +£469 +£1 -£1 +£0 -£293 -£55

Household net income by decile

Mean change in household net income per household, and as a share of the decile's base income; households stay in the base run's deciles.

Base decile 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Decile 1 0 0 0 0 0 0 0
Decile 2 0 0 0 0 0 0 0
Decile 3 0 0 0 0 0 0 0
Decile 4 0 0 0 0 fewer than 10 households change fewer than 10 households change fewer than 10 households change
Decile 5 0 0 0 0 0 0 0
Decile 6 0 0 fewer than 10 households change fewer than 10 households change 0 0 0
Decile 7 0 +£1 (+0.0%) 0 0 0 0 0
Decile 8 +£1 (+0.0%) +£2 (+0.0%) +£1 (+0.0%) +£1 (+0.0%) +£1 (+0.0%) +£1 (+0.0%) +£1 (+0.0%)
Decile 9 +£1 (+0.0%) +£2 (+0.0%) +£1 (+0.0%) +£1 (+0.0%) +£1 (+0.0%) +£1 (+0.0%) 0
Decile 10 +£3 (+0.0%) +£2 (+0.0%) 0 +£1 (+0.0%) +£3 (+0.0%) +£3 (+0.0%) +£1 (+0.0%)
all +£1 (+0.0%) +£1 (+0.0%) 0 0 +£1 (+0.0%) +£1 (+0.0%) 0
Year People gaining over £1 People losing over £1
2025-26 0.9% 0.0%
2026-27 0.7% 0.0%
2027-28 0.3% 0.0%
2028-29 0.3% 0.0%
2029-30 0.4% 0.0%
2030-31 0.4% 0.0%
2031-32 0.3% 0.0%

No change in any year: population; poverty rates; people in poverty.

Checks failing on both base and head (1)
  • VAT excluding private school fees is never negative (sign:vat): 2025-26: 76 units below zero, a weighted £-52,314,332 in total; 2026-27: 76 units below zero, a weighted £-54,150,722 in total; 2027-28: 76 units below zero, a weighted £-56,043,426 in total
Counted-once check

The check raised each of 92 components by £1 per unit in 2026-27 and read every head of its totals household by household: its own head must rise by exactly £1, no other tax head may move, no other benefit may rise, and the model's own total must move by what its heads moved. A response that is not finite breaks every rule. The check measures a head that breaks a rule twice more: once holding every other head at its baseline value, and once holding every other head at its value after the first £1 while the component rises again. The head fails where either measurement still shows the response. The check cannot hold a head at a value that is not finite. In each unit where another head is not finite after the first £1, it holds that head at its baseline value, and the second measurement judges no household the unit belongs to. There a head fails wherever it first broke a rule, or wherever the check could not confirm whether its response contains the component. 0 failed and the check skipped 0.

  • widowed_parents_allowance: pension_credit responds only through other heads in fewer than 10 households (other heads that moved there: carers_allowance; gov_spending; not a failure)
  • widowed_parents_allowance: pension_credit responds only through other heads in fewer than 10 households (other heads that moved there: carers_allowance; household_benefits; not a failure)
Provenance
Side Commit Runtime Environment hash Partition hash Time
base main 93b1711e57 uk 2.177.3 (files e08a1c0481f8), core 3.34.1, py 3.13.18 aarch64 e3a6bc10c49f 93bd0b3e0f8d 2619.96s
head #2830 merged into main 6f1da93e51 uk 2.177.3 (files 63c31e0bd96f), core 3.34.1, py 3.13.18 aarch64 e3a6bc10c49f 93bd0b3e0f8d 2816.66s

Dataset: hf://policyengine/policyengine-uk-data-private/enhanced_frs_2024_25.h5@2526edbbb687a3b54dbb11272e5389e4b62d2ac2, sha256 056e80fd8e9151d78f432a25cde863340baee81304c6216a37fb575ce8563784 (verified on both sides), policyengine-uk-data 1.58.1.

Receipts by tax head: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Final charge on cash interest in non-cash ISAs £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Lifetime ISA withdrawal charge £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Income tax £294.44bn £315.20bn £335.79bn £351.25bn £366.92bn £385.31bn £399.96bn
National Insurance (employee and self-employed) £51.70bn £54.14bn £56.13bn £57.73bn £59.76bn £61.64bn £63.51bn
National Insurance (employer) £148.71bn £155.37bn £160.88bn £165.09bn £172.06bn £177.36bn £182.57bn
Employer NI change borne by capital £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Employer NI change borne by consumers £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Capital gains tax £12.60bn £13.15bn £13.60bn £14.11bn £14.67bn £15.24bn £15.80bn
Student loan repayments £7.82bn £8.13bn £8.39bn £8.58bn £8.79bn £9.04bn £9.29bn
VAT excluding private school fees £242.22bn £249.47bn £258.67bn £264.86bn £270.95bn £277.04bn £283.21bn
Change in VAT from its baseline £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
VAT on private school fees £1.95bn £2.01bn £2.09bn £2.14bn £2.20bn £2.26bn £2.31bn
Change in private school VAT from its baseline £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Fuel duties £24.71bn £24.13bn £26.00bn £26.83bn £26.51bn £25.78bn £24.63bn
Alcohol duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Tobacco duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Vehicle excise duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Betting and gaming duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
TV licence fee £4.26bn £4.40bn £4.41bn £4.42bn £4.43bn £4.44bn £4.45bn
Council tax £49.21bn £52.16bn £54.89bn £57.95bn £61.16bn £64.47bn £67.91bn
High value council tax surcharge £0.00bn £0.00bn £0.00bn £0.48bn £0.49bn £0.50bn £0.51bn
Northern Ireland domestic rates £0.55bn £0.58bn £0.60bn £0.63bn £0.66bn £0.69bn £0.72bn
Business rates £34.68bn £36.20bn £38.00bn £39.46bn £42.27bn £43.25bn £44.29bn
Stamp duty land tax on the household's own purchases £6.88bn £7.32bn £7.66bn £8.06bn £8.47bn £8.91bn £9.35bn
Stamp duty land tax paid by companies £4.72bn £4.57bn £5.29bn £6.03bn £6.37bn £6.92bn £7.45bn
Land transaction tax (Wales) £0.07bn £0.08bn £0.08bn £0.09bn £0.10bn £0.10bn £0.11bn
Land and buildings transaction tax (Scotland) £0.31bn £0.32bn £0.33bn £0.35bn £0.36bn £0.38bn £0.40bn
Unspecified tax change borne by capital £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Unspecified tax change borne by consumers £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Wealth tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Wealth tax excluding the main residence £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Land value tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Carbon tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Spending by benefit and service: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Lifetime ISA bonus £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
State Pension £121.68bn £125.92bn £126.72bn £129.48bn £132.98bn £136.65bn £140.41bn
Pension Credit £6.21bn £6.46bn £6.68bn £6.82bn £6.89bn £7.07bn £7.22bn
Winter Fuel Payment £1.86bn £1.85bn £1.79bn £1.77bn £1.78bn £1.78bn £1.78bn
Pension Age Winter Heating Payment (Scotland) £0.19bn £0.20bn £0.20bn £0.21bn £0.21bn £0.22bn £0.22bn
Universal Credit £75.70bn £81.81bn £84.29bn £86.34bn £87.51bn £89.06bn £89.70bn
Housing Benefit £8.57bn £8.52bn £8.85bn £9.06bn £9.29bn £9.57bn £9.84bn
Council Tax Reduction £2.19bn £2.32bn £2.44bn £2.58bn £2.72bn £2.87bn £3.02bn
Employment and Support Allowance (income-related) £0.79bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Employment and Support Allowance (contribution-based) £5.20bn £5.37bn £5.56bn £5.69bn £5.82bn £5.95bn £6.09bn
Jobseeker's Allowance (income-based) £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Jobseeker's Allowance (contribution-based) £0.29bn £0.30bn £0.31bn £0.31bn £0.32bn £0.33bn £0.34bn
Income Support £0.11bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Working Tax Credit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Child Tax Credit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Incapacity Benefit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Personal Independence Payment £26.95bn £28.03bn £29.05bn £30.09bn £30.75bn £31.51bn £32.21bn
Disability Living Allowance £7.49bn £7.79bn £8.07bn £8.36bn £8.55bn £8.76bn £8.95bn
Attendance Allowance £8.68bn £9.03bn £9.36bn £9.69bn £9.90bn £10.15bn £10.38bn
Carer's Allowance £4.48bn £4.66bn £4.83bn £5.00bn £5.11bn £5.27bn £5.42bn
Carer Support Payment (Scotland) £0.31bn £0.32bn £0.33bn £0.34bn £0.35bn £0.36bn £0.37bn
Scottish Carer Supplement £0.00bn £0.04bn £0.04bn £0.05bn £0.05bn £0.05bn £0.05bn
Severe Disablement Allowance £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn
Industrial Injuries Disablement Benefit £0.50bn £0.52bn £0.54bn £0.55bn £0.56bn £0.58bn £0.59bn
Armed Forces Compensation Scheme £0.61bn £0.63bn £0.65bn £0.66bn £0.68bn £0.69bn £0.71bn
War Widow's or Widower's Pension £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Child Benefit £16.19bn £16.82bn £17.39bn £18.00bn £18.39bn £18.84bn £19.25bn
Scottish Child Payment £0.49bn £0.52bn £0.53bn £0.55bn £0.56bn £0.57bn £0.56bn
Two Child Limit Payment (Scotland, counterfactual) £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Healthy Start vouchers £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn
Maternity Allowance £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Statutory Maternity Pay £3.30bn £3.41bn £3.54bn £3.62bn £3.70bn £3.78bn £3.87bn
Statutory Paternity Pay £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Statutory Sick Pay £1.12bn £1.16bn £1.20bn £1.23bn £1.26bn £1.29bn £1.32bn
Sure Start Maternity Grant £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn
Bereavement Support Payment £0.67bn £0.69bn £0.71bn £0.73bn £0.75bn £0.76bn £0.78bn
Widowed Parent's Allowance £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Tax-Free Childcare £0.56bn £0.57bn £0.57bn £0.58bn £0.60bn £0.61bn £0.59bn
Extended childcare entitlement £2.99bn £3.50bn £3.59bn £3.71bn £3.78bn £3.86bn £3.93bn
Universal childcare entitlement £2.06bn £2.22bn £2.29bn £2.37bn £2.43bn £2.48bn £2.53bn
Targeted childcare entitlement £0.50bn £0.51bn £0.52bn £0.54bn £0.55bn £0.57bn £0.56bn
Childcare Offer for Wales £0.26bn £0.28bn £0.28bn £0.29bn £0.30bn £0.31bn £0.32bn
Care to Learn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Childcare Grant £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Parents' Learning Allowance £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn
Adult Dependants' Grant £0.06bn £0.06bn £0.04bn £0.04bn £0.04bn £0.04bn £0.04bn
Travel Grant £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed
16 to 19 Bursary Fund £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
NHS spending £203.89bn £204.30bn £204.56bn £204.91bn £205.34bn £205.85bn £206.35bn
State education spending £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn
Transport subsidy spending £25.05bn £25.10bn £25.14bn £25.18bn £25.23bn £25.30bn £25.36bn
Cost-of-living support payment £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Energy Bills Rebate £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Household Energy Payment £0.00bn £2.11bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Energy Price Guarantee subsidy £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Basic income £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Disability basic income £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Other public spending budget change £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Caseloads: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Lifetime ISA bonus 0k 0k 0k 0k 0k 0k 0k
State Pension 11,264k 11,145k 10,819k 10,700k 10,723k 10,750k 10,776k
Pension Credit 1,489k 1,484k 1,476k 1,454k 1,452k 1,457k 1,466k
Winter Fuel Payment 7,844k 7,783k 7,473k 7,375k 7,391k 7,409k 7,427k
Pension Age Winter Heating Payment (Scotland) 803k 802k 800k 797k 799k 801k 802k
Universal Credit 6,343k 6,419k 6,408k 6,369k 6,364k 6,343k 6,295k
Housing Benefit 1,249k 1,182k 1,181k 1,165k 1,164k 1,166k 1,168k
Council Tax Reduction 3,326k 3,344k 3,351k 3,364k 3,376k 3,388k 3,395k
Employment and Support Allowance (income-related) 71k 0k 0k 0k 0k 0k 0k
Employment and Support Allowance (contribution-based) 719k 720k 721k 722k 724k 726k 728k
Jobseeker's Allowance (income-based) 0k 0k 0k 0k 0k 0k 0k
Jobseeker's Allowance (contribution-based) 67k 67k 67k 67k 67k 68k 68k
Income Support 25k 0k 0k 0k 0k 0k 0k
Working Tax Credit 0k 0k 0k 0k 0k 0k 0k
Child Tax Credit 0k 0k 0k 0k 0k 0k 0k
Incapacity Benefit 0k 0k 0k 0k 0k 0k 0k
Personal Independence Payment 3,567k 3,575k 3,579k 3,585k 3,593k 3,602k 3,611k
Disability Living Allowance 1,291k 1,294k 1,296k 1,298k 1,300k 1,304k 1,307k
Attendance Allowance 1,695k 1,698k 1,700k 1,703k 1,707k 1,711k 1,715k
Carer's Allowance 1,034k 1,037k 1,038k 1,040k 1,042k 1,060k 1,062k
Carer Support Payment (Scotland) 71k 71k 71k 72k 72k 72k 72k
Scottish Carer Supplement 0k 71k 71k 72k 72k 72k 72k
Severe Disablement Allowance 4k 4k 4k 4k 4k 4k 4k
Industrial Injuries Disablement Benefit 137k 137k 137k 138k 138k 138k 139k
Armed Forces Compensation Scheme 93k 93k 94k 94k 94k 94k 94k
War Widow's or Widower's Pension 0k 0k 0k 0k 0k 0k 0k
Child Benefit 8,123k 8,120k 8,120k 8,130k 8,145k 8,161k 8,178k
Scottish Child Payment 351k 352k 352k 353k 350k 348k 337k
Two Child Limit Payment (Scotland, counterfactual) 0k 0k 0k 0k 0k 0k 0k
Healthy Start vouchers 200k 200k 200k 201k 201k 201k 202k
Maternity Allowance 0k 0k 0k 0k 0k 0k 0k
Statutory Maternity Pay 333k 333k 334k 334k 335k 336k 337k
Statutory Paternity Pay 0k 0k 0k 0k 0k 0k 0k
Statutory Sick Pay 181k 182k 182k 182k 182k 183k 183k
Sure Start Maternity Grant 26k 26k 26k 26k 26k 26k 26k
Bereavement Support Payment 98k 98k 98k 98k 98k 99k 99k
Widowed Parent's Allowance 0k 0k 0k 0k 0k 0k 0k
Tax-Free Childcare 1,043k 1,027k 1,014k 1,009k 1,011k 1,009k 993k
Extended childcare entitlement 795k 800k 796k 797k 798k 798k 792k
Universal childcare entitlement 1,071k 1,072k 1,072k 1,073k 1,076k 1,079k 1,074k
Targeted childcare entitlement 165k 156k 154k 154k 155k 155k 150k
Childcare Offer for Wales 39k 39k 39k 39k 39k 39k 39k
Care to Learn 0k 0k 0k 0k 0k 0k 0k
Childcare Grant 0k 0k 0k 0k 0k 0k 0k
Parents' Learning Allowance 98k 98k 98k 99k 99k 99k 99k
Adult Dependants' Grant 18k 18k 12k 12k 12k 12k 12k
Travel Grant 0k 0k 0k 0k 0k 0k 0k
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed
16 to 19 Bursary Fund 0k 0k 0k 0k 0k 0k 0k
Cost-of-living support payment 0k 0k 0k 0k 0k 0k 0k
Energy Bills Rebate 0k 0k 0k 0k 0k 0k 0k
Household Energy Payment 0k 21,065k 0k 0k 0k 0k 0k
Energy Price Guarantee subsidy 0k 0k 0k 0k 0k 0k 0k
Basic income 0k 0k 0k 0k 0k 0k 0k
Disability basic income 0k 0k 0k 0k 0k 0k 0k
Government balance, each component counted once: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Government receipts, each component once £884.82bn £927.24bn £972.81bn £1,008.06bn £1,046.18bn £1,083.32bn £1,116.48bn
Government spending £529.29bn † £545.32bn † £550.39bn † £559.10bn † £566.74bn † £575.46bn † £583.06bn †
Government balance, each receipt once £355.53bn † £381.92bn † £422.42bn † £448.96bn † £479.45bn † £507.87bn † £533.42bn †
Benefits in household_benefits but not in gov_spending £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Model totals: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
gov_tax £884.82bn £927.24bn £972.81bn £1,008.06bn £1,046.18bn £1,083.32bn £1,116.48bn
household_tax £491.95bn £520.39bn £551.18bn £575.97bn £600.96bn £626.65bn £648.38bn
pre_budget_change_household_tax £484.12bn £512.26bn £542.79bn £566.92bn £591.68bn £617.12bn £638.58bn
gov_spending £529.29bn † £545.32bn † £550.39bn † £559.10bn † £566.74bn † £575.46bn † £583.06bn †
household_benefits £529.29bn † £545.32bn † £550.39bn † £559.10bn † £566.74bn † £575.46bn † £583.06bn †
gov_balance £355.53bn † £381.92bn † £422.42bn † £448.96bn † £479.45bn † £507.87bn † £533.42bn †
Household income totals: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Household market income £1,637.96bn £1,702.77bn £1,757.35bn £1,806.41bn £1,855.97bn £1,912.43bn £1,970.44bn
Household net income £1,636.62bn † £1,687.50bn † £1,715.11bn † £1,747.12bn † £1,778.27bn † £1,816.57bn † £1,859.18bn †
Household net income (HBAI definition) £1,449.50bn £1,499.51bn £1,530.52bn £1,564.80bn £1,597.47bn £1,635.14bn £1,676.11bn
Population: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
People 69,245k 69,383k 69,473k 69,592k 69,738k 69,912k 70,080k
Benefit units 35,110k 35,181k 35,226k 35,286k 35,360k 35,449k 35,534k
Households 31,046k 31,108k 31,149k 31,201k 31,267k 31,345k 31,420k
Poverty rates: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Absolute poverty, before housing costs: everyone 16.2% 15.4% 16.3% 16.2% 16.4% 16.4% 16.5%
Absolute poverty, before housing costs: children 24.6% 21.9% 23.1% 22.7% 22.9% 23.0% 23.2%
Absolute poverty, before housing costs: working-age adults 14.0% 13.7% 14.6% 14.6% 14.7% 14.7% 14.9%
Absolute poverty, before housing costs: pensioners 13.7% 13.4% 14.0% 13.9% 14.0% 14.0% 13.9%
Absolute poverty, after housing costs: everyone 21.5% 21.0% 22.0% 22.5% 22.8% 23.0% 23.4%
Absolute poverty, after housing costs: children 33.1% 31.2% 32.1% 32.4% 32.8% 33.2% 34.0%
Absolute poverty, after housing costs: working-age adults 19.9% 19.8% 20.8% 21.5% 21.8% 22.0% 22.2%
Absolute poverty, after housing costs: pensioners 12.7% 12.4% 13.2% 13.2% 13.3% 13.4% 13.6%
Relative poverty, before housing costs: everyone 16.9% 15.9% 16.3% 16.2% 16.3% 16.2% 16.5%
Relative poverty, before housing costs: children 25.8% 22.6% 23.1% 22.7% 22.8% 22.6% 23.2%
Relative poverty, before housing costs: working-age adults 14.4% 14.0% 14.6% 14.6% 14.7% 14.6% 14.9%
Relative poverty, before housing costs: pensioners 14.5% 14.1% 14.0% 13.8% 13.8% 13.9% 13.9%
Relative poverty, after housing costs: everyone 21.4% 20.9% 21.2% 21.3% 21.3% 21.5% 21.7%
Relative poverty, after housing costs: children 32.9% 31.0% 31.1% 31.0% 31.1% 31.2% 31.7%
Relative poverty, after housing costs: working-age adults 19.8% 19.7% 20.2% 20.4% 20.5% 20.6% 20.8%
Relative poverty, after housing costs: pensioners 12.5% 12.4% 12.2% 11.8% 11.7% 11.8% 12.1%
People in poverty: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Absolute poverty, before housing costs: everyone 11,251k 10,708k 11,337k 11,302k 11,412k 11,452k 11,588k
Absolute poverty, before housing costs: children 3,677k 3,272k 3,456k 3,407k 3,443k 3,465k 3,511k
Absolute poverty, before housing costs: working-age adults 5,950k 5,860k 6,279k 6,333k 6,390k 6,402k 6,493k
Absolute poverty, before housing costs: pensioners 1,624k 1,576k 1,602k 1,562k 1,579k 1,585k 1,584k
Absolute poverty, after housing costs: everyone 14,871k 14,567k 15,264k 15,691k 15,911k 16,096k 16,374k
Absolute poverty, after housing costs: children 4,941k 4,671k 4,815k 4,873k 4,932k 5,015k 5,146k
Absolute poverty, after housing costs: working-age adults 8,426k 8,436k 8,944k 9,325k 9,475k 9,560k 9,684k
Absolute poverty, after housing costs: pensioners 1,504k 1,460k 1,505k 1,493k 1,505k 1,520k 1,545k
Relative poverty, before housing costs: everyone 11,698k 11,036k 11,323k 11,301k 11,352k 11,354k 11,588k
Relative poverty, before housing costs: children 3,849k 3,391k 3,456k 3,407k 3,429k 3,416k 3,511k
Relative poverty, before housing costs: working-age adults 6,131k 5,993k 6,272k 6,333k 6,359k 6,362k 6,493k
Relative poverty, before housing costs: pensioners 1,719k 1,652k 1,594k 1,561k 1,565k 1,576k 1,584k
Relative poverty, after housing costs: everyone 14,802k 14,520k 14,761k 14,796k 14,871k 15,007k 15,219k
Relative poverty, after housing costs: children 4,916k 4,649k 4,663k 4,653k 4,675k 4,703k 4,798k
Relative poverty, after housing costs: working-age adults 8,397k 8,415k 8,702k 8,816k 8,873k 8,962k 9,052k
Relative poverty, after housing costs: pensioners 1,488k 1,455k 1,395k 1,326k 1,324k 1,342k 1,369k
Inequality: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Gini coefficient, equivalised HBAI income before housing costs 0.3077 0.3049 0.3055 0.3054 0.3059 0.3064 0.3080
Mean household net income in each side's own deciles: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Decile 1: mean household_net_income per household £17,748 † £17,980 † £17,927 † £18,181 † £18,317 † £18,537 † £18,742 †
Decile 2: mean household_net_income per household £28,048 † £29,038 † £28,757 † £28,999 † £29,260 † £29,678 † £30,174 †
Decile 3: mean household_net_income per household £34,220 † £35,425 † £36,066 † £36,547 † £37,156 † £37,773 † £38,102 †
Decile 4: mean household_net_income per household £38,926 † £40,769 † £41,716 † £42,763 † £43,387 † £44,167 † £44,499 †
Decile 5: mean household_net_income per household £43,346 † £45,603 † £46,054 † £47,001 † £47,756 † £48,627 † £50,309 †
Decile 6: mean household_net_income per household £47,936 † £49,179 † £50,883 † £52,036 † £52,926 † £53,947 † £54,723 †
Decile 7: mean household_net_income per household £55,669 † £56,647 † £57,489 † £57,670 † £58,788 † £59,703 † £61,102 †
Decile 8: mean household_net_income per household £68,786 † £70,811 † £71,870 † £73,483 † £73,890 † £75,325 † £77,181 †
Decile 9: mean household_net_income per household £78,891 † £81,011 † £80,860 † £82,646 † £85,464 † £86,940 † £89,508 †
Decile 10: mean household_net_income per household £116,864 † £120,162 † £124,545 † £126,420 † £125,756 † £128,269 † £130,694 †
Standard reform costings (change in gov_balance): head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise basic rate by 1pp £6.41bn £6.66bn £6.63bn £6.85bn £7.03bn £7.25bn £7.47bn
Raise higher rate by 1pp £3.61bn £3.87bn £4.00bn £4.22bn £4.44bn £4.71bn £4.83bn
Raise personal allowance by ~800GBP/year £-3.82bn £-3.93bn £-4.01bn £-4.05bn £-4.11bn £-4.24bn £-1.95bn
Raise child benefit by 25GBP/week per additional child £-1.99bn £-1.75bn £-1.55bn £-1.38bn £-1.30bn £-1.18bn £-1.07bn
Reduce Universal Credit taper rate to 20% £-20.83bn £-21.38bn £-21.81bn £-22.02bn £-22.37bn £-22.77bn £-22.96bn
Raise Class 1 main employee NICs rate to 10% £10.89bn £11.30bn £11.71bn £12.01bn £12.31bn £12.62bn £13.00bn
Raise VAT standard rate by 2pp £24.17bn £24.97bn £25.86bn £26.46bn £27.07bn £27.68bn £28.31bn
Raise additional rate by 3pp £2.90bn £3.15bn £3.22bn £3.41bn £3.61bn £3.85bn £4.14bn
Raise the Universal Credit standard allowance by 10% £-3.06bn £-3.23bn £-3.30bn £-3.40bn £-3.48bn £-3.53bn £-3.55bn
Raise the State Pension by 10% £-7.76bn £-7.99bn £-8.22bn £-8.44bn £-8.79bn £-8.52bn £-8.86bn
Standard reforms: change in each reform's target: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise basic rate by 1pp: change in income_tax £6.55bn £6.86bn £6.79bn £7.00bn £7.20bn £7.44bn £7.66bn
Raise higher rate by 1pp: change in income_tax £3.61bn £3.92bn £4.01bn £4.23bn £4.45bn £4.72bn £4.84bn
Raise personal allowance by ~800GBP/year: change in income_tax £-3.94bn £-4.07bn £-4.14bn £-4.18bn £-4.22bn £-4.40bn £-1.76bn
Raise child benefit by 25GBP/week per additional child: change in child_benefit £2.33bn £2.14bn £1.94bn £1.76bn £1.65bn £1.52bn £1.40bn
Reduce Universal Credit taper rate to 20%: change in universal_credit £20.79bn £21.36bn £21.84bn £22.07bn £22.39bn £22.80bn £23.01bn
Raise Class 1 main employee NICs rate to 10%: change in national_insurance £11.15bn £11.63bn £12.00bn £12.29bn £12.61bn £12.94bn £13.31bn
Raise VAT standard rate by 2pp: change in vat £23.98bn £24.77bn £25.66bn £26.24bn £26.85bn £27.46bn £28.07bn
Raise additional rate by 3pp: change in income_tax £2.90bn £3.15bn £3.22bn £3.42bn £3.62bn £3.86bn £4.12bn
Raise the Universal Credit standard allowance by 10%: change in universal_credit £3.06bn £3.22bn £3.32bn £3.41bn £3.48bn £3.54bn £3.55bn
Raise the State Pension by 10%: change in state_pension £10.68bn £11.17bn £11.28bn £11.60bn £12.03bn £12.46bn £12.96bn

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Ready for hub review

Verdict: REQUEST_CHANGES at 70633f0897957186ff9128aac91eb98ea9d7c5ec.

The text-only delta closes all nine P3 findings from the previous round. Every quotation I checked against the law in the brief matches word for word, and both of the author's judgment calls are right. The new 2027-28 case is right by hand and in a run. No formula changed. But CI on this head is red. Two existing tests, which pass on main, fail on this branch because of the least-tax share itself. Neither the body nor any earlier round lists them. Each test pins the old order. Each fix is a one-line change to a test, with no change to the model. I ran both fixes below on a scratch copy and they pass.

How I worked: detached worktrees of the head and of main at 6c6e229a5, each set up with uv sync --frozen --extra dev, with HUGGING_FACE_TOKEN and POLICYENGINE_UK_DEFAULT_DATASET unset.

P1

P1-1. CI is red: two tests still pin the old order. The CI run 41494574912 ran on refs/pull/2830/merge (a4a9b32e9, which merges 70633f0 into 6c6e229). Its results: Test / Python 3/6 "1 failed, 1972 passed", Test / Python 4/6 "1 failed, 1842 passed", and Test / Result failure. I reproduced both on the head alone:

uv run pytest policyengine_uk/tests/test_trading_loss_properties.py \
  policyengine_uk/tests/test_statutory_paternity_pay_income_tax_properties.py -n 4
head 70633f089: 2 failed, 18 passed
main 6c6e229a5: 20 passed

Neither file has changed since the merge base 2da212abc, on the branch or on main. So the share causes both failures, and def18d3 fails them too, since it has the same model code.

(a) test_trading_loss_properties.py:381, test_relief_taxes_like_income_cut_in_section_25_order. The test says the tax with a loss equals the tax of a twin whose income is cut by the relief in the order non-savings, savings, dividends. Under the share, the relief can do better than that cut. Pinned person 1 (pay £5,000, savings £10,000, dividends £30,000, loss £20,000) fails with 637.4749755859375 against 1282.4749755859375. Those are the figures that this PR's own change to trade_loss_relief_against_general_income.yaml works (1,282.48 → 637.475). The relief is a feasible share for the twin's cut income too, and the PA taper reads adjusted net income, which is the same for both. So the least-tax result can only be at or below the twin's. Replacements:

  • :395: assert abs(a[tax][i] - b[tax][i]) <= 0.5 + tolerance( → assert a[tax][i] - b[tax][i] <= 0.5 + tolerance(
  • Rename the test to test_relief_never_taxes_more_than_income_cut_in_the_old_order, and rename its key in shard_durations.toml:453 to match.
  • reduced docstring (:313-314): "The person's income cut by the relief in the order non-savings, savings, dividends, computed outside the model. The least-tax share (ITA 2007 s. 25(2)) can only charge as much tax as this cut, or less."
  • Module docstring item 2 (:15-21): "2. Differential: a person's Income Tax before the winter fuel payment charge with a loss never exceeds the same tax of the same person without the loss and with their income cut by the relief, taken from non-savings income first, then savings, then dividends. The model sets the relief where it gives the least tax (s. 25(2)), and that cut is one of the shares it weighs. The charge itself can differ between the two: it tests total income, which the relief, at Step 2, does not lower but the cut does."

With the one-character assertion change, the file passes (on a scratch copy, then restored).

(b) test_statutory_paternity_pay_income_tax_properties.py:452, test_a_pound_of_statutory_paternity_pay_can_halve_the_savings_allowance. It expects a rise of £100.40 and gets 100.2001953125. I worked it by hand for 2026-27, with pay £48,270 and savings £2,000:

  • LOW: 35,700 × 20% + 1,000 × 20% = £7,340.
  • HIGH, with £1 more pay: the savings allowance falls to £500, as the model computes it. In the old order the pay is 35,701 × 20% = 7,140.20. The savings then take £500 at nil, 1,499 × 20% = 299.80 and 1 × 40% = 0.40, for £7,440.40.
  • The share instead moves £1,500 of the allowance onto savings. The pay of 37,201 × 20% = 7,440.20 then bears all the tax: the £500 of savings left sits at nil from £37,201 to £37,701, so nothing bears 40%. That gives £7,440.20, and the rise is £100.20.

Model runs give the same figures: on head, HIGH is income_tax_pre_charges 7,440.2002, with received_allowances_savings_income 1,500 and received_allowances_earned_income 11,070. On main it is 7,440.4004, with everything against earned income. Replacements:

  • :453: - 100.4) → - 100.2)
  • Comment :447-448: "Under the order non-savings, savings, dividends, savings tax goes from 20% of 1,000 = £200 to 20% of 1,499 plus 40% of 1 = £300.20. Setting £1,500 of the Personal Allowance against savings instead leaves the last pound within the £500 savings allowance. Pay of 37,201 then bears 20%, so income tax rises from £7,340 to £7,440.20, not £7,440.40 (ITA 2007 s. 25(2))."

With that change the file passes (on a scratch copy, then restored). One caveat: this rests on the model's savings_allowance, which stays at £500 under any share. Nobody quoted the s. 12B test of higher rate income, so I can't say whether the law's allowance stays at £500 when no savings bear 40%.

Body: add both tests to "Existing tests whose expected values move, and why", with the reasons above. Replace "Not run: the whole test suite, which CI runs on this PR, and any test that needs a survey dataset." with the CI result once it is green.

P3

P3-1. The pension paragraph's last sentence reads against its own claim (body, "Pension relief stays on non-savings income"). The paragraph says neither arrangement gives a Step 2 deduction. It then ends "since the taxpayer must otherwise deduct it from that income first (s. 25(3A))", but s. 25(3A) reaches only "the reliefs and allowances in Steps 2 and 3". Replace with: "…since the model otherwise deducts it from that income first, as s. 25(3A) requires of the reliefs and allowances in Steps 2 and 3."

P3-2. Optional: Scottish limits (income_tax_allocation.py:24-29, :225-229). For a Scottish taxpayer's non-savings income, the limits that rise are the Scottish ones, under s. 192(4A): "the Scottish basic rate limit, and any other Scottish rate limit … above the Scottish basic rate limit". The equivalence still holds there. The starter band, which (4A) leaves alone, sits below the inserted basic-rate width, and the payer keeps the Scottish basic rate on the contribution where HMRC notify the administrator (s. 192(1A)(a)). The docstring would be complete with "(s. 192(4), and for the Scottish rate limits s. 192(4A))" after "by the contribution".

P3-3. Axiom packet, the pending note on the 2027-28 case. The packet's own blocker section lists s. 8 among the sections already in the corpus at 2026-06-11-uk-income-tax-rates. So "once it holds s. 8 for 2027-28" can read as already met. Suggest: "until the corpus holds the s. 8 rates, the basic rate limit and the Personal Allowance in force for 2027-28, mark this expected value as pending on the statute's rates."

Previous round, finding by finding

Finding Status Evidence
P3-1 relief at source Closed Docstring :22-32, comment :225-229, YAML case 15 and test docstring :716-722 now cite s. 192(1) and the claim condition in s. 192(4). I reran the witness (pay £20,000, personal contributions £5,000, savings £5,000, 2026-27): income_tax 772.0 on head and on main, and 772.0 with employee contributions on both. By hand: 2,430 × 20% = 486; savings take 2,570 at the starting rate, then £1,000 at nil and 1,430 × 20% = 286, for £772. Under the law: 7,430 × 20% + 4,000 × 20% = 2,286, less £1,000, so £1,286 ✓.
P3-2 invariant 8 Closed Rerun on head and main: received_allowances_earned_income 2,000, received_allowances_dividend_income 14,170, income_tax 1,647.975.
P3-3 Universal Credit Closed The judgment call and the Readers bullet match the replacement. uc_income_tax.py:63-67 and test_uc_income_tax_properties.py:4-10 follow it. The "Reg. 55(5)(b) deducts…" comment at uc_income_tax.py:24 is in the merge base 2da212abc in that form, as the author says.
P3-4 first threshold Closed (text) The docstring :80-85 and the body carry the figures that the author and the previous round each got from seeds 100 to 119. I didn't rerun the 6,520-person sample.
P3-5 residue docstring Closed :677-679 as given.
P3-6 merge notes Closed git merge-tree --write-tree --name-only 70633f089 <sha>: e5b16d14b exit 1, 9aa5ee4c9 exit 1, each with only non_savings_income_after_allowances.py; 6c6e229a5 exit 0. The branch SHAs match the body. On the author's divergence, see below.
P3-7 Axiom packet Closed See "Quotations". The Completion boundary now keeps only s. 16A's commencement and FA 2026 s. 7.
P3-8 companion cases Closed The case texts are unchanged from the round that worked them, and least_tax_allocation.yaml passes 18/18.
P3-9 voice and cost Closed The three replacements are applied. I didn't rerun the 0.925% and 0.635% counts.

The author's two judgment calls

  • "Higher or additional rate" for s. 192(4): right. The subsection reads "If (apart from this section) income tax at the higher rate or the additional rate ... is chargeable…". The YAML case 15 comment keeps only "on a claim". That is accurate for its person, whose £70,000 of income bears higher rate tax.
  • non_savings_taxable_income as an indirect reader: right. Its formula (non_savings_taxable_income.py:33-36) is earned_taxable_income + taxable_property_income_after_allowances. git grep '"non_savings_income_after_allowances"' outside the tests finds only earned_taxable_income.py:39, taxable_property_income_after_allowances.py:32 and property_income_tax.py:50,94. "reads it through the last two" names exactly those two. The previous round's suggestion to list it as a direct reader would have been false.

The new 2027-28 case

By hand, at the model's 2027-28 parameters (I read them: PA 12,570, UK thresholds [0, 37,700, 125,140], dividend rates [10.75%, 35.75%, 39.35%]). Pay £50,000 and dividends £20,000:

  • s. 25(3A) takes all £12,570 off the pay, which leaves 37,430 × 20% = 7,486.
  • The dividends run from £37,430 to £57,430. The £500 nil band covers £37,430 to £37,930, which takes in the last £270 of the basic rate band. The other 19,500 lie above £37,700 at 35.75%, which gives 6,971.25.
  • Step 5 tax: £14,457.25.

Run: income_tax_pre_charges 14,457.25 and received_allowances_earned_income 12,570, received_allowances_dividend_income 0, on head and on main. In 2026-27 the head gives 14,414.7246 with £270 against dividends, and main gives 14,457.25.

Quotations

Checked word for word against the hub's fetched text in the brief:

  • ITA 2007 s. 16A(3) and (4): match, curly apostrophes included.
  • FA 2004 s. 192(1): matches, apart from the stray space before the full stop that the fetched text has. The author dropped it and said so.
  • FA 2004 s. 192(4): matches, with its "..." elision.
  • FA 2004 s. 193(2), (4)(a) and (b) and (6), and s. 194(1): match.
  • ITA 2007 s. 25(1) to (6): match. Subsection (3) is abridged with "[...]", and the opening item and the closing general clause sit where the full text puts them.

Each cited subsection says what the text claims:

  • s. 193(2) is a deduction "from the employment income".
  • s. 193(6) and s. 194(1) are deductions "in calculating the net income … (see Step 2…)", so they are Step 2 reliefs.
  • s. 192(1) has the payer "deduct and retain" tax "at the relevant rate", which s. 192(1A) makes the basic rate, or the Scottish or Welsh basic rate where HMRC notify the administrator. The author's "basic rate tax" note covers this.
  • s. 16A(4) puts property income "immediately before the savings income", which matches the packet's "immediately below".

Checked and sound

  • No formula changed. git diff --stat def18d3a3..70633f089 shows 6 files, +49/−31. I read every hunk. All are docstrings, comments, one YAML comment and one documentation string. An AST comparison of the five Python files, with docstrings and documentation blanked, gives SAME for all five.
  • Tests at head:
    • test_income_tax_allocation.py and test_uc_income_tax_properties.py: 24 passed.
    • Income tax YAML directory: 490 passed.
    • least_tax_allocation.yaml: 18 passed.
    • uvx ruff@0.17.2 format --check . ("2167 files already formatted") and ruff check . ("All checks passed!").
    • git diff --check def18d3a3..HEAD: clean.
  • The live PR title and body are the hub's PR_TITLE.txt and PR_BODY.md. I spot-checked the replaced sentences from P3-2, P3-3, P3-4, P3-6 and P3-9 for the exact text.
  • CI on 70633f0 (the merge with 6c6e229): Lint, Fix evidence, Changelog fragments, Documentation build, smoke imports on 3.11 to 3.14, Test / YAML, Python shards 1, 2, 5 and 6, and Aggregate impact all pass. Shards 3 and 4 fail (P1-1).

What I couldn't check

  • The 6,520-person first-threshold sample and the 20,000-person fast-path counts (not rerun).
  • The resolved Fix Scottish tax bands and test Marriage Allowance rates directly #2828 merge suites at this head (551 and 87 tests, 24 couples).
  • The whole suite locally. I relied on CI for it, and ran only the two failing files locally.
  • Any provision nobody quoted:
    • ITA 2007 ss. 8, 12, 12B (whether the law's savings allowance stays at £500 in P1-1(b)), 26(1)(a), 55B and 55C;
    • FA 2026 ss. 6 and 7;
    • UC Regulations 2013 regs. 55 and 57;
    • the commencement of s. 16A.
  • Whether rulespec-uk#524 as filed matches AXIOM_ISSUE.md. That repository is outside this session's access, so I reviewed the brief's copy.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Picking this up for the UK hub. Reply here if you're mid-change and I'll stop.


Generated by Claude Code

The trading loss differential now bounds the tax with a loss by the tax on
income cut in the old order, and keeps the equality where the person has no
savings or dividend income. The paternity pay test expects the £100.20 rise
that setting £1,500 of the Personal Allowance against savings gives. The
allocation docs cite FA 2004 s. 192(4A) for the Scottish rate limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6KkfvQBfSHTvh7rcen
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

PR_TITLE.txt

Set reliefs and allowances against income for the least tax

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

PR_BODY.md

Fixes #2106

Reproducer: `policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/received_allowances/least_tax_allocation.yaml::Case 2, savings in the starting rate band leave the Personal Allowance to dividends`

axiom: https://github.com/TheAxiomFoundation/rulespec-uk/issues/524 queued

## What and why

Parliament set the rule in ITA 2007 s. 25(2): "At Steps 2 and 3, deduct the reliefs and allowances in the way which will result in the greatest reduction in the taxpayer's liability to income tax." The model always deducted them from non-savings income first, then savings, then dividends. Under that order it overcharged wherever an allowance set against savings or dividends would save more tax:

| 2026-27, age 40 | `origin/main` | This PR | Why |
|---|---|---|---|
| Dividends £20,000 | £744.97 | £744.97 | One kind of income |
| Savings £4,000 + dividends £20,000 | £1,174.97 | £744.97 | The savings fall in the 0% starting rate band (s. 12), so the allowance saves tax only against dividends |
| Savings £4,000 + dividends £200,000 + trading loss £1,000 | £65,574.21 | £65,466.71 | The loss relief goes against dividends, not against savings at 0% |
| Pay £50,000 + savings £10,000 | £11,286 | £11,232 | £270 of the allowance against savings keeps pay at the £37,700 limit, so the £500 savings allowance covers savings that would bear 40% |
| Pay £50,000 + dividends £20,000 | £14,457.25 | £14,414.73 | The same with the dividend allowance |

Both columns come from runs of the model at 6429896f9 (whose model code 70633f089 keeps) and `origin/main` 2da212abc; the YAML cases pin the right-hand column.

The model overcharged people with savings as well as dividends (second row), and people with only pay and savings (fourth row).

Parliament changed the rule from 2027-28:

- **Before 2027-28,** the taxpayer may split the deductions in any way between non-savings, savings and dividend income (s. 25(2)). Parliament taxes property income as non-savings income at the same rates in those years, so the model keeps taking the deductions off the rest of non-savings income first.
- **From 2027-28,** the taxpayer deducts reliefs and allowances from income other than property, savings and dividend income first (s. 25(3A), which Parliament inserted by Finance Act 2026 s. 6(6) with effect "for the tax year 2027-28 and subsequent tax years" (s. 6(8))). The taxpayer may then split what remains in any way between property, savings and dividends. The model records the commencement in a new boolean parameter, `gov.hmrc.income_tax.reliefs.other_income_first`: false until 2027-04-06, true from then.

The model now shares the deductions this way in every year it covers. Before 2027-28, who gains depends on the starting rate limit, the personal savings allowance, the dividend allowance and the rate limits. The model sets the dividend allowance at £5,000 in 2016-17 and 2017-18 and £2,000 from 2018-19 to 2022-23 (`dividend_allowance.yaml`).

In 2027-28, take property income of £20,000, dividends of £200,000 and a £1,000 trading loss. Setting the relief against property income saves 24% on property income in the basic rate band (22% under FA 2026 s. 7). It also moves £1,000 of dividends down into the 10.75% ordinary rate band, because the dividends start where the property income ends and still end at £219,000. Setting it against dividends leaves that boundary where it is. So the model sets the relief against property income and charges £74,710.21, as `origin/main` does, against £74,842.71, worked by hand, with the relief against dividends. YAML case 4 works the arithmetic.

### Deductions the model shares out

At Steps 2 and 3 the model deducts:

- **`trade_loss_relief_against_general_income`** (s. 64, a Step 2 relief). Parliament made s. 25(2) subject to the priority rule in s. 65(2) to (4) (s. 25(3)). Under that rule the taxpayer's claims follow a set order, but nothing ties the relief to a component, so the model sets it wherever it saves the most tax, after other income from 2027-28.
- **`allowances`,** the sum of:
  - `personal_allowance` and `blind_persons_allowance`, the Step 3 allowances (Part 3 Chapter 2);
  - the Step 2 reliefs `charitable_investment_gifts` (Part 8 Chapter 3), `covenanted_payments` and `other_deductions`;
  - `pension_contributions_relief`, which the model holds on non-savings income (below).

Each part except pension relief can come off any component, and the tax depends only on how much comes off each. So the model shares out their sum, up to total income (s. 25(4) and (5)).

**Pension relief stays on non-savings income.** The model takes employee contributions as a net pay arrangement and personal contributions as relief at source (`pension_contributions_relief`, `net_pay_pension_relief`). Under a net pay arrangement the employer deducts the contributions from employment income (FA 2004 s. 193(2)). Under relief at source the payer keeps basic rate tax on the contribution (s. 192(1)) and, on a claim where higher or additional rate tax is chargeable, Parliament raises the basic and higher rate limits by the contribution (s. 192(4)). Neither gives a Step 2 deduction that the taxpayer can set against savings or dividends; only a net pay excess relieved on a claim (s. 193(4) and (6)) or relief on a claim under s. 194(1) does. The model gives both kinds as a deduction from income, so the search holds min(pension relief, non-savings income) on non-savings income and shares only the rest. YAML cases 14 and 15 pin it: pay £40,000, £10,000 of pension contributions and savings £30,000 in 2026-27 give £11,332, the tax on pay of £30,000 and savings of £30,000. Worked by hand: 17,430 × 20% = 3,486 on the pay; savings from £17,430 to £47,430, £500 at the nil rate, 19,770 × 20% = 3,954 and 9,730 × 40% = 3,892. Every share keeps the savings allowance below the £37,700 limit, so no share lowers that tax. Sharing the pension relief too would charge £11,232. From 2027-28 the hold matters only where the relief exceeds income other than property, savings and dividend income, since the model otherwise deducts it from that income first, as s. 25(3A) requires of the reliefs and allowances in Steps 2 and 3.

### How the model finds the least tax

The model shares the deductions between three components: the first (non-savings income before 2027-28, property income after), savings and dividends. Write x for the first component left, w for x plus the savings left, and T for the Step 3 income of the three, which no share changes. Savings sit above non-savings income and dividends above savings (ss. 16 and 16A), so:

- tax on the first component changes slope where x crosses a rate limit;
- the starting rate (s. 12) and the savings nil rate (s. 12A) cover savings from q up to max(q, L) + A, where q stands for the non-savings income below the savings, L for the starting rate limit and A for the savings allowance. Their slope changes where q reaches L, where max(q, L) + A crosses a rate limit, and where w reaches the top of that band: on the line w − x = A, or w constant below L;
- the taxed savings change slope where w crosses a rate limit;
- the dividend nil rate (s. 13A) covers the lowest £500 of dividends, so dividend tax changes slope where w + £500 crosses a dividend rate limit or reaches T;
- the limits on the components add x = 0, x = the whole first component, w − x = 0, w − x = all the savings, w = T and w = T less the dividends.

So the Step 5 tax runs continuous and linear on each piece of the feasible polygon that vertical (x constant), horizontal (w constant) and diagonal (w − x constant) lines cut. A linear function takes its least value on a polygon at a corner, so the least tax lies where two of the lines cross. `policyengine_uk/utils/income_tax_allocation.py` lists every crossing, 173 a person in 2026-27 and 2027-28 from 11 vertical, 10 horizontal and 3 diagonal lines, projects each onto the feasible polygon and evaluates the Step 5 tax at all of them, for all the people searched at once.

Before 2027-28 a reform can add an exclusion E from earned income, such as one that exempts pensions (`uc_earnings_deductions.yaml` runs one). With property income P, the savings then sit on q = x up to x = P, on q = P up to x = P + E, and on q = x − E beyond. The search adds the lines of each piece: the two boundaries, the rate limits moved by P and E, and the diagonal lines that a fixed q = P gives. That raises the count to 1,034 a person, and only where someone has an exclusion. YAML case 17 pins the result: under that reform, pay £5,000, private pension £10,000 and savings £30,000 give £3,286, against £4,800 under the old order. With the pension and £5,000 of the allowance setting earned income at nil, £22,430 of savings start at nil: £5,000 at the starting rate, £1,000 at the savings nil rate and 16,430 × 20% = 3,286.

The search relies on three things that hold in every modelled year: a starting rate limit at or below the basic rate limit, a starting rate band that shrinks by a pound for each pound of non-savings income (s. 12), and a first UK rate threshold at nil. A reform that sets that threshold above nil leaves savings below it untaxed and adds lines on which x + w stays constant, which the search doesn't list. On 6,520 synthetic people with that threshold at £2,000, a 101 × 101 grid found less tax than the search for 32 in 2026-27 (by up to £181.70) and 38 in 2027-28 (by up to £214.75). The fast path sends everyone to the search under such a reform, and the search still keeps the old order unless a share it lists charges less.

The search leaves the Marriage Allowance out. Parliament gives it as a tax reduction at Step 6 (ss. 26 and 55B(1)), not as an allowance at Step 3. The model still subtracts the allowance from earned income (#1947) and so moves a recipient's savings and dividends down by up to £1,260; for a recipient, the search's Step 5 tax can then differ from the model's. At 5d9ce1343's model code we built 20,364 married couples a year by hand: 14,364 on a grid of pay, savings, dividends and property income in England and Scotland, and 6,000 drawn at random. 12,277 of them include a recipient. No recipient paid more income tax than under the old order, in 2026-27 or 2027-28, and 1,069 and 1,155 recipients paid less, by up to £645. That grid has no pension contributions and no exclusion beyond the Marriage Allowance, so the pension hold and the exclusion lines leave its people unchanged; it hasn't been rerun at 6429896f9.

Until #1963 lands, a search that read the model's subtraction would charge 1,190 of those recipients less in 2026-27, by up to £252 (£1,260 at 20%), and none less in 2027-28 (5d9ce1343). In YAML case 13, the model sets the Personal Allowance against all £5,000 of the recipient's pay, so it has no earned income left to subtract the Marriage Allowance from; with the Personal Allowance set against savings instead, it would subtract £1,260 from the pay. Once #1963 lands, the model gives the allowance as the s. 55B tax reduction, and the search's tax and the model's agree for recipients too.

Where several shares give the same tax, the model takes the one setting most against the first component, then most against savings: the nearest the least tax allows to the old order. It keeps the old order, to the float32 bit, unless another share saves more than £0.001.

### Who skips the search

The search runs only on people three tests leave, and on nobody when the tests clear everyone. Each test gives the result the search would give.

1. **One share.** With no deduction left to place under s. 25(2), or no income left after the deductions, only one share exists, and the model keeps the old order.
2. **Nothing charged above the first component.** The old order sets every shared deduction against the first component, so any other share leaves more of it, and tax on it rises with x wherever the rates rise with income. Where the old order also leaves no savings and no dividends charged above the nil rates, another share can only add tax.
3. **Rates.** Moving a pound of the deductions from the first component to savings or dividends raises x by that pound and w by at most a pound. The top of the nil rates moves by at most that pound too: the starting rate band shrinks by at most a pound for each pound of non-savings income (s. 12), and the savings and dividend nil rates cover fixed amounts (ss. 12A and 13A). Write ρn for the rate on the first component just above where the old order leaves it, ρs for the savings rate just above the top of the savings, and σs and σv for the highest savings and dividend rates the moved pounds can reach. Any move raises the tax by at least (ρn − σs) dx + (ρs − σv) dw, with 0 ≤ dw ≤ dx, or ρn dx − σv dw where the old order charges no savings above the nil rates. Where that bound never falls below zero, the old order gives the least tax. Before 2027-28, where a reform adds an exclusion from earned income, the test also needs no property income.

Tests 2 and 3 apply only when every rate scale rises with income from nil; under a reform that makes a scale fall, or sets the first UK threshold above nil, the model sends everyone with something to share to the search.

For people the tests clear, the search would keep the old order too, since it moves the share only for a saving above £0.001. A property test checks the two paths give the same deductions, bit for bit, on synthetic people with rising and with falling rates, now with pension relief among them. Two new tests pin the checks that no earlier test separated: a supplied adjusted net income with trade loss relief, where the full search saves £200 and £60 over the old order, and a reform that lifts the Scottish starter band to £9,000, where it saves £30.

### Cost

The search adds work only for people it can't skip. On 20,000 synthetic people of the "realistic" mix in `test_income_tax_allocation.py`, most with pay alone and some with savings, dividends, property income or pension relief, the three tests send 0.925% of them (185 of 20,000) to the search in 2026-27 and 0.635% (127 of 20,000) in 2027-28, at 70633f089. Timings from earlier commits, on a hand-built mix of 20,000 people without a simulation: at 60de3303b the deductions took 5.5 ms with the tests against 4.6 s with the search for everyone in 2026-27, and 3.4 ms against 2.5 s in 2027-28. At 5d9ce1343, a cold `income_tax` for one household (pay £50,000 and savings £10,000, 2026-27) took 0.907 to 1.041 s of CPU time in three runs against 0.898 to 1.026 s on `origin/main`, and the allocation variables took 2.6 to 4.6 ms against 0.35 to 0.45 ms. The pension hold adds one column to the search, and the exclusion lines run only for people with an exclusion; nobody has timed 6429896f9 or later.

### Readers of the changed variables

Every output variable keeps its name. `git grep` finds these readers:

- **`received_allowances_savings_income` and `received_allowances_dividend_income`** now come from the search. Their readers (`basic_rate_savings_income`, `higher_rate_savings_income`, `add_rate_savings_income`, `basic_rate_savings_income_pre_starter`, `savings_starter_rate_income`, `dividend_income_tax`, `taxed_dividend_income`, `received_allowances`) don't change.
- **`received_allowances_earned_income`** (allowances against non-savings income beyond trade loss relief) and **`trade_loss_relief_against_savings_and_dividends`** now follow from those two. They equal the old order's values wherever the share equals the old order.
- **`non_savings_income_after_allowances`** now comes from the allocation's own arithmetic: where the share keeps the old order, net income less savings, dividends and allowances, as `origin/main` computes it; elsewhere, in float64, the non-savings income less the deductions the share sets against it. **`earned_taxable_income`** adds back the relief set against savings and dividends, because `net_income` deducts all of it, and never exceeds `non_savings_income_after_allowances` while savings, dividends and `received_allowances_earned_income` sit on its exclusions list. Without the add-back, the model would undercount non-savings income wherever the share set relief against savings or dividends while some non-savings income remained (YAML case 16). Without the float64 arithmetic, float32 rounding in net income plus the relief added back left a residue above nil (YAML case 18). `taxable_property_income_after_allowances`, `property_income_tax`, `non_savings_taxable_income` (through `earned_taxable_income` and `taxable_property_income_after_allowances`), `earned_income_tax` and the basic, higher and additional rate earned income variables read them as before.
- **`adjusted_net_income`** and **`net_income`** sum totals that no share changes, so neither moves. Nor do the Personal Allowance taper, the savings allowance band (`savings_allowance` reads net income less allowances), `tax_band` or the Marriage Allowance conditions.
- **Universal Credit:** `utils/uc_income_tax.py` (`uc_income_tax_on_earnings`, `uc_income_tax_on_trade`) takes earnings as the lowest slice of non-savings income, with trade loss relief off the other non-savings income first and allowances off earnings first. It reads the old order's deductions (`default_order_deductions`), not the share, so the slice equals `origin/main`'s and adding savings or dividends never raises it. For self-employment income of £37,406 and savings of £14,407 in 2026-27, the share moves the whole Personal Allowance onto the savings and income tax falls from £8,057.20 to £8,016, while `uc_income_tax_on_trade` stays at £4,967.20 (74fdacf1d gave £7,481.20). For pay £50,000 and savings £10,000 it stays at £7,486. See the Universal Credit judgment call for why the slice keeps the old order.
- **Housing Benefit and Council Tax Reduction** read no allocation variable. `housing_benefit/applicable_income/_net_earnings.py` takes income tax on earnings as `income_tax_before_winter_fuel_payment_charge` times the earnings share of income, a pro-rata share rather than a slice. `housing_benefit_applicable_income.py` and `council_tax_reduction_applicable_income.py` deduct that total. So all three deduct less income tax where the share lowers it.
- **`property_finance_costs_relieved`** computes adjusted total income (its reference: ITTOIA 2005 s. 274AA(6)) as net income less all the savings and dividend income, `charitable_investment_gifts`, `other_deductions` and the personal and blind person's allowances. It doesn't read the share, so it doesn't change (see judgment calls).

## Invariants

For every person, on the Step 5 tax (`income_tax_pre_charges`), the only tax the share changes:

1. **Least tax:** no share of the deductions that the taxpayer may make under s. 25 gives less Step 5 tax than the model's (exact for people without Marriage Allowance; see above).
2. **Never more than the old order:** the tax never exceeds what the order non-savings, savings, dividends charges, apart from float32 rounding. For Marriage Allowance recipients, the hand-built couples above test it on income tax.
3. **Accounting:** the deductions against non-savings income (`received_allowances_earned_income` plus the relief not set against savings and dividends), savings and dividends add up to min(relief + allowances, income). None falls below zero or exceeds its component, and the relief set against savings and dividends never exceeds what they take.
4. **One kind of income:** with one kind of income nothing can move, and the tax and the deductions equal the old order's.
5. **Differential:** for people without Marriage Allowance, the search's own Step 5 function (`step_5_tax`) equals `income_tax_pre_charges` for every share forced through the model.
6. **Search exactness:** the vertex search's value never exceeds a dense grid's minimum of that function, and comes within the grid's step of it, now also with exclusions from earned income beside property income before 2027-28.
7. **Fast path:** for everyone the tests clear, the deductions equal the full search's, bit for bit.
8. **Pension relief:** the share sets pension relief against savings or dividends only where it exceeds non-savings income, as the old order does.
9. **Universal Credit:** adding savings or dividend income never raises `uc_income_tax_on_earnings` or `uc_income_tax_on_trade` for a person whose allowances it leaves unchanged.
10. **Budget 2026 rates:** income tax under current law never exceeds income tax under the Budget 2025 savings and property rates plus 2% of the banded savings and taxable property income under those rates, whatever share either takes.

On invariant 8: pay £2,000, personal pension contributions £3,600 and dividends £30,000 in 2026-27 give `received_allowances_earned_income` of £2,000 and `received_allowances_dividend_income` of £14,170, which includes the £1,600 of pension relief beyond the pay, and income tax of £1,647.975, at 70633f089 and on `origin/main` 6c6e229a5 alike.

## Test plan

**Ran at 056a674b9348c6656de2f64fb218eed6c374f938** (`uv sync --frozen --extra dev`; no survey dataset; `HUGGING_FACE_TOKEN` and `POLICYENGINE_UK_DEFAULT_DATASET` unset):

- `test_trading_loss_properties.py` and `test_statutory_paternity_pay_income_tax_properties.py`: all 20 pass with `-n 4`, and all 20 without xdist.
- `test_income_tax_allocation.py` and `test_uc_income_tax_properties.py`: all 24 pass.
- The income tax YAML directory: all 490 cases pass. `least_tax_allocation.yaml`: all 18 pass. `trade_loss_relief_against_general_income.yaml`: all 22 pass.
- `policyengine_uk/tests/code_health` with `-n 4`: all 3,089 tests pass.
- `.github/check_fix_pr.py` on this title and body, with the branch's changes from its merge base with `origin/main` 6c6e229a5 (2da212abc): "Fix evidence: PASS", with 15 changed executable tests.
- `uvx ruff@0.17.2 format --check .` ("2167 files already formatted"), `uvx ruff@0.17.2 check .` and `uvx ruff@0.17.2 check --select F811,F821 policyengine_uk` pass, and `git diff --check 70633f089..HEAD` is clean.
- `income_tax_allocation.py` changes only in a docstring and a comment: its AST, with docstrings blanked, is the same as at 70633f089.
- Mutation checks on a scratch copy of the package, each run on `test_relief_never_taxes_more_than_income_cut_in_the_old_order` without xdist and then restored:
  - The old two-sided assertion for everyone: fails on pinned person 1 (pay £5,000, savings £10,000, dividends £30,000, loss £20,000), with income tax of £637.47 against £1,282.47 for the twin.
  - `net_income` subtracting `trade_loss_relief_against_general_income` twice, with the relief itself unchanged: the one-sided bound passes for everyone, and the check for people with no savings or dividend income fails on pinned person 3 (pay £300,000, loss £200,000), with £53,703 against £87,453. With that check removed, the test passes under the mutant.

**Ran at 70633f089** (`uv sync --frozen --extra dev`; no survey dataset):

- `uvx ruff@0.17.2 format --check .`, `uvx ruff@0.17.2 check .` and `uvx ruff@0.17.2 check --select F811,F821 policyengine_uk` pass, and so do `uvx ruff@0.17.0 format` and `check` on the changed Python files and `git diff --check`. No line the branch adds to a Python file runs past 88 characters.
- `policyengine_uk/tests/code_health`: all 3,089 tests pass.
- The income tax YAML directory: all 490 cases pass, all 18 of `least_tax_allocation.yaml` among them.
- `test_income_tax_allocation.py`: all 22 pass. `test_uc_income_tax_properties.py`: both pass.
- `.github/check_fix_pr.py` on this title and body, with the branch's changes against `origin/main`: "Fix evidence: PASS", with 13 changed executable tests.
- Witnesses, at 70633f089 and on `origin/main` 6c6e229a5:
  - pay £20,000, personal pension contributions £5,000 and savings £5,000 (2026-27): income tax £772 on both; the same with employee (net pay) contributions: £772 on both (see the pension judgment call);
  - the invariant 8 witness above: £1,647.975 on both;
  - pay £50,000 and dividends £20,000 in 2027-28: all £12,570 of the allowance against pay and income tax of £14,457.25 on both; in 2026-27 the share sets £270 against dividends and charges £14,414.725 here, against £14,457.25 on `origin/main`.
- On 6,520 synthetic people (`synthetic_people`, seeds 100 to 119, 326 each) with `uk[0].threshold` at £2,000, `vertex_search_and_grid` finds less tax on the grid than from the search for 32 people in 2026-27 (by up to £181.70) and 38 in 2027-28 (by up to £214.75), and for no one below the grid minimum less two steps. Seeds 0 to 3 give 6 of 1,304 by up to £91.86.
- The fast-path share on the "realistic" mix (seed 2106, 20,000 people): 185 to the search in 2026-27 (0.925%) and 127 in 2027-28 (0.635%).

**Ran at def18d3a3,** whose model code 70633f089 keeps (70633f089 changes only docstrings, comments and one variable's documentation):

- `policyengine_uk/tests/code_health`: all 3,089 tests pass.
- The income tax YAML directory: all 490 cases pass.
- `test_income_tax_allocation.py` (22) and `test_uc_income_tax_properties.py` (2): all 24 pass.
- `uc_earnings_deductions.yaml` (15) and `integration/income.yaml` (8): all 23 pass.

**Ran on the working tree as it stood at 6429896f9 or one step before it** (`uv sync --extra dev`, Python 3.13.18, 4 vCPU; no survey dataset). The earlier tree lacked only YAML case 18 and the cap that holds pension relief within `allowances`, which can't bind in the model because `allowances` includes the relief; def18d3a3 keeps 6429896f9's code and renames one line of `shard_durations.toml`. The runs at def18d3a3 above repeat the ones that touch case 18 or the allocation tests:

- The income tax YAML directory (`policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax`) passes 489 cases, `integration/income.yaml` 8, the Universal Credit income directory (with `uc_earnings_deductions.yaml`) 174, and `uc_trading_loss.yaml`, `uc_company_analogous_business.yaml` and the benefit cap directory 150.
- `test_income_tax_allocation.py`: 22 tests pass, including 7 new: the dense-grid search with exclusions in 2018-19, 2026-27 and 2027-28, two supplied adjusted net income cases, the Scottish starter band reform and the pension hold.
- `test_uc_income_tax_properties.py` (new): both tests pass, including the Hypothesis property with the self-employment witness as an explicit example in 2025-26 and 2026-27.
- `test_budget_2026_savings_property_rates_properties.py`: all 8 tests pass with `-n 4` and without, including the new bound (invariant 10).
- `test_scottish_top_rate.py` 10, `test_savings_starting_rate_indexation.py` 26 and `test_property_income_rate_reforms.py` 55 pass.
- `test_gift_aid.py`, `test_property_income_band_base_properties.py`, `test_income_tax_step_6_step_7_properties.py`, `test_adjusted_net_income_pension_relief.py`, `test_income_concepts_properties.py`, `test_uc_company_owner_properties.py` and `test_pension_annual_allowance_properties.py`: all 63 tests pass.
- A differential against `origin/main` 2da212abc on 3,000 random single adults a year in London and Scotland, 2025-26 to 2028-29 (12,000 rows), with pay, self-employment, property income, savings, dividends, employee and personal pension contributions, trading losses and Gift Aid:
  - `income_tax_pre_charges` falls by more than 1p plus a millionth of the tax in 725 rows, by up to £645, and rises beyond that in none (the largest rise, £0.0078, is float32 rounding).
  - `uc_income_tax_on_earnings` and `uc_income_tax_on_trade` move by more than 1p in no row. At 74fdacf1d the two slices rose 319 times over the 12,000 rows, counting each slice separately, by up to £4,930.98, and fell in none.
  - `adjusted_net_income` moves in no row.
  - Against 74fdacf1d, the pension hold raises the tax by more than 1p in 51 rows, by up to £108: in 2025-26 and 2026-27 by up to £100 for employees and people with relief at source, and in 2027-28 and 2028-29 for Scottish taxpayers with property income, savings and relief at source but no other income, where 74fdacf1d set the relief against savings at 44% rather than property income at 42%. In each of those rows the tax equals or falls below `origin/main`'s.
- Float32 residue, on 6,000 hand-built people whose trading loss exceeds their trading income, at scales up to £50 million: of the 2,373 whom `origin/main` leaves with no non-savings income, 74fdacf1d left 607 above nil (up to £4.00) and 6429896f9 leaves 10 (up to £0.0078, rounding in the moved share); `earned_taxable_income` went from 63 above nil to none.
- A scratch merge with #2350, checked for a dependency cycle (see Merge notes).

**Revert and mutant checks,** each on a copy of the package as it stood at 6429896f9 or one step before it (the earlier copies lacked only the cap that holds pension relief within `allowances`):

| Change | Result |
|---|---|
| Pension relief shared again | Cases 14 and 15 fail (`received_allowances_earned_income` 2,300, not 22,570), and so does `test_pension_relief_stays_on_non_savings_income` |
| `earned_taxable_income` add-back removed | Case 16 fails (`earned_taxable_income` 30,000, not 37,700) |
| Exclusion lines removed | Case 17 fails (`received_allowances_earned_income` 7,300, not 5,000), and `test_vertex_search_equals_dense_grid_with_exclusions` fails in 2018 and 2026 |
| UC slice reading the share | Both tests in `test_uc_income_tax_properties.py` fail (the slice rises by £2,514) |
| `non_savings_income_after_allowances` from the add-back sum, with no cap on `earned_taxable_income` | Case 18 fails (£0.0078125, not 0) |
| `F6_old_order_check_dropped` | Both `test_fast_path_follows_a_supplied_net_income` cases fail |
| `F11_all_used_threshold_wide` | `test_fast_path_follows_a_supplied_net_income[all-savings-used]` fails |
| `F8_uk_rates_for_scots` | `test_fast_path_reads_the_scottish_starter_rate` fails |
| `F10_all_used_without_old_order_check` | Survives: equivalent. With Step 3 income at or below £0.000001, every share's Step 5 tax falls below the £0.001 tolerance, so the search keeps the old order whatever the old order sets |
| `F1_shared_within_first_dropped` | Survives, as expected: in exact arithmetic the old order's `in_first` already implies `shared <= first`, so the check matters only for float32 residues and negative components |
| `F1` and `F6` together | Both supplied adjusted net income cases and `test_fast_path_equals_full_search` fail |

The 74fdacf1d model code gives £11,232 for cases 14 and 15 and fails both. Under the pension-exempting reform, the search at 74fdacf1d missed the least tax that a 101 × 101 grid finds for 40 of 5,216 synthetic people with exclusions, in eight years from 2016-17 to 2028-29, by up to £415.14; at 6429896f9 it misses it for none of 15,648 in the same years.

**Ran at 74fdacf1d,** before these changes:

- `test_budget_2026_savings_property_rates_properties.py`: all 7 tests pass with `-n 4` and without, and the two failing tests pass with the seeds `108342745077855236797573561117658719055` and `245902028163439803096709191647468822378`.
- The same file at 5d9ce1343, run against 74fdacf1d's model code with `-n 4` and without the rUK property test: 3 pass and 2 fail, `test_savings_tax_rises_by_two_points_in_every_country` and `test_scottish_property_tax_ignores_the_property_rates`, as at 5d9ce1343. Both failing people are Scottish taxpayers with property income of about £54,000 and savings; see below.
- 4,000 random people in 2027-28: 46 break the same-election identities, and the share moves for all 46; none breaks invariant 10.

**Earlier runs,** at the commits named (each later commit to 74fdacf1d changed only tests, comments or the Marriage Allowance exclusion, as the commit messages say):

- 5d9ce1343: `test_property_income_rate_reforms.py` (55) and `test_savings_starting_rate_indexation.py` (26) passed; 18 other YAML files passed, 271 cases, every other file in the income tax directory with a case of two or more people.
- 5d9ce1343: the mutants `A_rate_limit_lines_dropped`, `B_s25_3A_ignored`, `C_tie_break_reversed` and `G1_marriage_allowance_read` each failed one of the 13 cases then in `least_tax_allocation.yaml`; `F2`, `F3`, `F4`, `F5`, `F8b`, `F9` and `F12` failed `test_fast_path_equals_full_search`, and `F7` failed `test_fast_path_equals_full_search_when_rates_fall[2026]`.
- e5003937c: `test_gift_aid.py` (12), `test_income_tax_step_6_step_7_properties.py` (10), `test_property_income_band_base_properties.py` (5), `test_adjusted_net_income_pension_relief.py` (17) and `test_income_concepts_properties.py` (2) passed.
- `origin/main` 2da212abc: cases 2, 3, 5, 7 and 8 of the first 11 cases of `least_tax_allocation.yaml` failed and the other 6 passed.

**Not run:** the whole test suite, which CI runs on this PR, and any test that needs a survey dataset.

- [x] YAML tests whose expected values come from the legislation, worked by hand, in `policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/received_allowances/least_tax_allocation.yaml`: both rows of the issue's table; savings £4,000, dividends £200,000 and a £1,000 trading loss; property £20,000, dividends £200,000 and a £1,000 trading loss in 2027-28; two Scottish taxpayers; pay and savings, and pay and dividends, across the basic rate limit; pay, nil-rate savings and dividends across it; one income type (savings only; property only in 2027-28); a Marriage Allowance recipient whose share leaves the allowance out; net pay and relief at source pension relief kept on non-savings income; trading loss relief set against savings with the pay left in earned income; a reform that exempts pensions; trading loss relief past the non-savings income.
- [x] Lint and format checks pass at 056a674b9348c6656de2f64fb218eed6c374f938.
- [x] Changelog fragment `changelog.d/2106.fixed.md`.

### The Budget 2026 property tests and the share

Four tests in `test_budget_2026_savings_property_rates_properties.py` compare everyone under the Budget 2025 and Budget 2026 rates on the assumption that each person keeps the same allowance order. The least-tax search can move the allowance when the rates change, and at 5d9ce1343 two of those tests failed on people it moved. Minimised to one person: a Scottish taxpayer with property income of £54,000 and savings of £13,000 in 2027-28. Under the Budget 2025 rates the allowance saves 42% against either income (Scottish higher rate on the property income, the UK higher savings rate on the savings), so the model keeps it on the property income. Under the Budget 2026 rates savings pay 44% while the property income still pays the Scottish 42%, so the model sets £12,500 of the allowance against savings, leaving £500 within the savings allowance, and £70 against the property income. Income tax stays at £15,904.89, against £16,154.89 had the allowance stayed on property income: the move saves 2% of £12,500. Property tax rises by 42% of £12,500, so the identity "Scottish property tax doesn't move" fails for this person, and so does "savings tax rises by 2% of banded savings". The search gives the least tax under s. 25(2); the tests' assumption was wrong. `same_choices` limits the identities to people whose election and allowance order stay the same, a pinned test (property £58,000, savings £24,000) checks the move, and invariant 10 checks every person, moved or not.

### Existing tests whose expected values move, and why

Each of these expected values moves because the model charges less tax under the least-tax share. Each case's comment works the arithmetic.

- `reliefs/trade_loss_relief_against_general_income.yaml`, "Relief takes non-savings income first, then savings, then dividends", becomes "Relief and allowances leave savings that the starting rate and savings allowance cover". Savings take 4,000 (from 10,000) and dividends 23,570 (from 17,570), since savings up to £6,000 bear 0% (ss. 12 and 12A). Income tax falls from 1,282.48 to 637.475.
- `integration/income.yaml`, "Savings slice of basic rate band" (2020-21): £2,500 of the allowance against pay and £10,000 against savings keep the pay at the £37,500 limit. `basic_rate_savings_income_pre_starter` goes from 10,000 to 0, `basic_rate_savings_income` from 9,500 to 0, `higher_rate_savings_income` from 5,000 to 4,500 and `taxed_savings_income` from 14,500 to 4,500. The tax falls by £100, to £9,300. Setting £500 more against the savings leaves it at £9,300; setting £500 more against the pay charges £9,400. `received_allowances_savings_income: 10_000` joins the outputs.
- `integration/income.yaml`, "Personal allowance caps savings starter rate before dividends.", becomes "Personal allowance goes to dividends rather than savings at the starting rate." Savings take 0 (from 2,570) and dividends 2,570. `basic_rate_savings_income_pre_starter` and `savings_starter_rate_income` go from 2,430 to 5,000 and `taxed_dividend_income` from 4,500 to 1,930. Income tax falls from 483.75 to 207.475.
- `gift_aid.yaml`, "Gift Aid extends UK limits for dividends above the basic-rate limit": `earned_taxable_income` goes from [37,430, 37,430] to [37,700, 38,700], `dividend_income_tax` from [3,396.25, 3,203.75] to [3,299.725, 2,942.225] and `income_tax` from [10,882.25, 10,689.75] to [10,839.725, 10,682.225].
- `gift_aid.yaml`, "Gift Aid extends UK limits across savings and dividend slices": `savings_income_tax` goes from [460, 300] to [120, 0], `dividend_income_tax` from [1,608.75, 1,433.75] to [1,608.75, 1,001] and `income_tax` from [9,368.75, 9,033.75] to [9,268.75, 8,941].
- `gift_aid.yaml`, "Gift Aid raises the basic rate limit for savings above property income": `savings_income_tax` goes from [5,646, 5,846] to [3,292, 3,692]; the income tax of each falls by £100.
- `test_gift_aid.py::test_zero_gift_preserves_baseline_outputs_bit_for_bit`: the fourth rest-of-UK row (pay £30,000, savings £12,000, dividends £18,000, property £9,000) sets £1,300 against pay and £11,270 against savings. `earned_taxable_income` and `basic_rate_earned_income` go from 17,430 to 28,700, `received_allowances_earned_income` from 12,570 to 1,300, `received_allowances_savings_income` from 0 to 11,270, `basic_rate_savings_income` from 10,770 to 0, `higher_rate_savings_income` from 730 to 230, `earned_income_tax` from 3,486 to 5,740, `savings_income_tax` from 2,446 to 92 and `income_tax` from 13,988.25 to 13,888.25. Its Scottish twin doesn't change, because pay above £31,092 would bear 42%.
- The `received_allowances_savings_income.yaml` and `received_allowances_dividend_income.yaml` unit cases fed `received_allowances_earned_income` and `received_allowances_savings_income` in as inputs, which the new formulas don't read. They now take the incomes as inputs. Every expected value stays the same except two dividend cases, which move from 0 to 500: the old order would leave £500 of dividends taxed at 8.75%, while £500 more allowance against dividends keeps the dividends left within the £500 nil rate (s. 13A). One `received_allowances_earned_income.yaml` case replaces an inconsistent `net_income` input with `taxable_employment_income`; its expected value doesn't change.
- `property_income_band_base.yaml`, "Case 2, savings sit above property income" (2026-27, property income £40,000 and savings £20,000): `savings_income_tax` goes from 5,846 to 3,692, and `income_tax` (7,540 + 3,692) joins the outputs. The model sets £2,300 of the Personal Allowance against the property income, which leaves it at the £37,700 basic rate limit, and the other £10,270 against savings, so the £500 savings allowance (s. 12B(3)) covers savings that would bear 40%. Income tax falls by £100.
- `test_property_income_band_base_properties.py`:
  - The higher-rate check asserted savings tax above £3,900 for that person; it now pins savings tax of £3,692, income tax of £11,232 and non-savings taxable income of £37,700.
  - The accounting invariant subtracted savings and dividends in full; it now subtracts the savings and dividends left after the allowances set against them.
  - The monotonic invariant tested savings and dividend tax; it now tests income tax, because more property income can lower savings tax. A new test takes property income from £30,000 to £40,000 with savings of £20,000: savings tax falls from £3,800 to £3,692 while income tax rises from £7,286 to £11,232.
  - The file now loads the model once through the `cloned_uk_tax_benefit_system` fixture, as 76 other test files do.
- `test_budget_2026_savings_property_rates_properties.py`: `same_choices` replaces `same_election` (above), and two tests join: the pinned Scottish move (property £58,000 and savings £24,000 in 2027-28: savings tax falls from £9,870 to £4,809.20, property tax rises by £5,279.40 and income tax rises by £218.60, against £470 had the allowance stayed on property income) and the bound for everyone (invariant 10).
- `test_trading_loss_properties.py`: `test_relief_taxes_like_income_cut_in_section_25_order` becomes `test_relief_never_taxes_more_than_income_cut_in_the_old_order`, and its key in `shard_durations.toml` follows. It asserted that a person's income tax before the winter fuel payment charge with a loss equals, within 50p and float32 rounding, the tax of a twin with no loss whose income the relief cuts in the order non-savings, savings, dividends. That cut, with the allowances set against it in the same order, is the old order's share, which the model always weighs and keeps unless another share saves more than £0.001, and the Personal Allowance taper reads the same adjusted net income for both. So the tax with the loss can now only match the twin's or fall below it. Pinned person 1 (pay £5,000, savings £10,000, dividends £30,000, loss £20,000) pays £637.47 against the twin's £1,282.47, the figures of the `trade_loss_relief_against_general_income.yaml` bullet above. The test now bounds the tax with the loss by the twin's for everyone, and keeps the equality for people with no savings or dividend income, for whom every share is the cut; pinned person 3 (pay £300,000, loss £200,000) runs that check.
- `test_statutory_paternity_pay_income_tax_properties.py::test_a_pound_of_statutory_paternity_pay_can_halve_the_savings_allowance`: the rise in income tax before the winter fuel payment charge from a pound of statutory paternity pay on wages of £48,270 and savings of £2,000 (2026-27) goes from £100.40 to £100.20. With the pound, the model sets £1,500 of the Personal Allowance against savings and £11,070 against pay, so pay of £37,201 bears 20% and the £500 of savings left falls within the £500 savings allowance: income tax goes from £7,340 to £7,440.20, against £7,440.40 under the old order. The last pound of Step 3 income would bear 40% but for the savings nil rate, so it still counts as higher-rate income (ITA 2007 s. 12B(8)(b)(ii)), and the allowance stays at £500 (s. 12B(3)).

### Merge notes

`git merge-tree --write-tree --name-only` with explicit SHAs, from 056a674b9348c6656de2f64fb218eed6c374f938:

- **#2350** (`fix-2350-scottish-tax-band` at e5b16d14bbf9456945986565798902b720d222f3, draft #2828): exit 1, one conflicted file, `non_savings_income_after_allowances.py`, in its documentation only; `test_gift_aid.py` merges cleanly. Take this branch's file and end its documentation with "property_income_tax, taxable_property_income_after_allowances, earned_taxable_income, the Marriage Allowance rate test and, for Scottish taxpayers, tax_band read it, and non_savings_taxable_income reads it through taxable_property_income_after_allowances and earned_taxable_income." #2350 has `liable_only_at_marriage_allowance_rates` read `non_savings_income_after_allowances`, `received_allowances_savings_income` and `received_allowances_dividend_income`, and all three follow the share here. The search leaves the Marriage Allowance out, so no cycle runs through the allowance: on the resolved merge of def18d3a3 and e5b16d14b, the income tax YAML directory and `finance/tax/income_tax/scottish_rates.yaml` pass (551), `test_tax_band_scottish_properties.py`, `test_gift_aid.py`, `test_marriage_allowance_cps_switch.py`, `test_removed_marriage_allowance_eligible_bands_parameter.py` and `test_income_tax_allocation.py` pass (87), and `income_tax`, `marriage_allowance` and `tax_band` compute with no `CycleError` for 24 married couples (6 profiles, including a recipient with pay of £50,270 and £1 of savings and a couple with net pay pension relief, a trading loss and savings, each in London and Scotland in 2026-27 and 2027-28). 70633f089 and 056a674b9 change no formula against def18d3a3, and nobody has rerun that merge at either. The hand-built couples above ran on a merge of 5d9ce1343 and f0f576a44: no couple paid more than under the old order in either year, 1,119 recipients paid less in 2026-27 and 1,200 in 2027-28, by up to £645, and in 2026-27 the model gave the allowance to 12 more couples than under the old order, and to none fewer.
- **#2783** (branch `fix-2783-scottish-rates` at 4bae9d1dfe2416c88cd753d9ad4c17df127a20ea, which carries #2350 at e5b16d14b; no pull request yet): exit 1, the same single conflicted file, in the same documentation.
- **`origin/main`** at 6c6e229a51fbd804dab2062dac1f94af04c609c2: exit 0, no conflict.
- **#1963** (at 69c919f60) removes `marriage_allowance` from `earned_taxable_income_exclusions` and gives the allowance as a Step 6 reduction. Nobody has run a merge with it. A static read finds no cycle, because #1963 computes the election in branch simulations, and the search already leaves the allowance out, so its share doesn't change when #1963 lands. Its own `liable_only_at_marriage_allowance_rates` reads `non_savings_taxable_income` and both allocation variables. The search minimises the Step 5 tax, so check after both land that it still gives the least liability where the share changes whether the reduction applies.
- **#2661** (trading losses carried forward, at bcf0fb7cd) changes how `trade_loss_relief_against_general_income` works out the relief; the search reads it as a total. Git merged 74fdacf1d and #2661 cleanly; not rerun at 6429896f9 or later.

### Judgment calls

- **Pension relief:** the model gives net pay and relief at source both as a deduction from income. Under a net pay arrangement the employer deducts the contribution from employment income (FA 2004 s. 193(2)), so holding it on non-savings income follows the law. Under relief at source Parliament deducts nothing from income: the payer keeps basic rate tax on the contribution (s. 192(1)) and, on a claim where higher or additional rate tax is chargeable, the basic and higher rate limits rise by the contribution (s. 192(4)). Deducting it from the bottom of non-savings income gives the same tax in cases 14 and 15. It doesn't at the starting rate for savings, whose limit s. 192(4) leaves alone: pay £20,000, personal contributions £5,000 and savings £5,000 in 2026-27 cost £772 in the model, here and on `origin/main`, against £1,286 under the law (£2,286 less the £1,000 the payer keeps). Where the relief exceeds non-savings income, the model deducts the rest from savings and dividends, as `origin/main` does. A net pay excess relieved on a claim (s. 193(4) and (6)) or relief on a claim under s. 194(1) could go against savings or dividends (s. 24(1)(a)); the model doesn't separate those, so it holds them too.
- **Adjusted total income:** `property_finance_costs_relieved` still deducts all the savings and dividend income, whatever share the search takes. ITTOIA 2005 s. 274AA(6) Step 2 excludes only "so much of [net income] as is" savings or dividend income, so relief that the share sets against savings or dividends should raise adjusted total income under the law. The model's gap predates this PR, and this PR leaves it.
- **The s. 25(3A) parameter:** a boolean parameter records its commencement, so a reform can switch it off or on. No input variable sets it.
- **Step 5 tax:** the search minimises the Step 5 tax. `capped_mcad`, `property_finance_cost_relief`, `pension_tax_allowance` and `income_tax_reductions` read `income_tax_pre_charges` only to cap the Step 6 reductions at the tax left. `income_tax_before_winter_fuel_payment_charge` charges max(0, Step 5 tax − reductions) + the Step 7 charges, which never rises when the Step 5 tax falls, so the least Step 5 tax gives the least liability.
- **Ties:** where shares give the same tax, the model takes the one nearest the old order.
- **Universal Credit:** the earnings slice keeps the order non-savings, savings, dividends rather than the share, so more savings or dividends never raise it. DWP deducts income tax paid "in respect of the employment" (reg. 55(5)(b) of the Universal Credit Regulations 2013) and, for the self-employed, "in respect of any trade" (reg. 57(2) step 3). Neither says how to split a person's tax between earnings and other income, so the order is the model's choice, the one PAYE coding follows for employees.
- **A first UK threshold above nil:** the search assumes the first threshold sits at nil, as in every modelled year (above).
- **The savings allowance under the share:** with a pound of Step 3 income above the basic rate limit that the share leaves at the savings nil rate, s. 12B(8)(b)(ii) still counts it as higher-rate income ("income on which income tax would be charged at the higher rate, or default higher rate, but for section 12A"), so the allowance is £500 under s. 12B(3), as `savings_allowance` computes from net income less allowances against the basic rate limit, which no share changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

AXIOM_ISSUE.md

Title: Encode ITA 2007 s. 25(2) and (3A): deduct reliefs and allowances in the way that gives the least tax (PE parity: policyengine-uk#2106)

Labels: pe-parity

This signed-encoder request covers ITA 2007 s. 25, under which the taxpayer sets the Step 2 reliefs and Step 3 allowances against the components of income, and the s. 25(3A) order that Parliament added by Finance Act 2026 s. 6(6) from 2027-28. PolicyEngine-UK encodes both for PolicyEngine/policyengine-uk#2106 (`received_allowances_earned_income`, `received_allowances_savings_income`, `received_allowances_dividend_income`, `trade_loss_relief_against_savings_and_dividends`, `policyengine_uk/utils/income_tax_allocation.py`). It leaves out the Marriage Allowance, which Parliament gives as a Step 6 tax reduction (s. 55B(1)) and rulespec-uk already encodes in `uk/statutes/ukpga/2007/3/55B.yaml`, and the priority rules that s. 25(3) lists for particular loss reliefs.

Pension relief stays out of the share too. PolicyEngine-UK takes employee contributions as a net pay arrangement, which the employer deducts from employment income at Step 1 (FA 2004 s. 193(2)), and personal contributions as relief at source, under which the payer keeps basic rate tax on the contribution (FA 2004 s. 192(1)) and, on a claim where higher or additional rate tax is chargeable, Parliament raises the basic and higher rate limits by the contribution (s. 192(4)). Neither gives a Step 2 deduction the taxpayer can set against savings or dividend income, so PolicyEngine-UK keeps that relief on non-savings income and shares only the rest.

## Pinned target and execution blocker

Proposed label: `pe-parity`. OpenAI-billed signed runs need approval, and none exists for this work yet, so nobody should invoke the encoder or write the module by hand.

The signed encoder needs both provisions in rulespec-uk's pinned corpus first. At rulespec-uk main `0497350d74f9d0f7272b88e08019e66366d3be58` (2026-10-29, still main on 2026-11-01):

- `data/corpus/sources/uk/statute/2026-06-11-uk-income-tax-rates/ukpga/2007/3/` holds sections 8, 12A, 12B, 13A and 16 only;
- `.../ukpga/2026/11/` holds sections 1, 2, 3, 9 and 10;
- `uk/statutes/ukpga/2007/3/` holds modules for sections 6, 8, 10, 11D, 12, 12A, 12B, 13, 13A, 16, 23, 35, 55B and 58, and none for section 25.

| Proposed module path | Proposed corpus selector / operative legal identifier |
|---|---|
| `uk/statutes/ukpga/2007/3/25.yaml` (new) | `uk/statute/ukpga/2007/3/25`; s. 25(1) to (6), current text, with (3A) |
| (commencement, read by `25.yaml`) | `uk/statute/ukpga/2026/11/6`; s. 6(6) inserts s. 25(3A), and s. 6(8) gives it effect for 2027-28 and later years |
| `uk/statutes/income_tax/individual/savings_dividend_oracle_pipeline.yaml` (existing) | its allowance order (`uk_svdv_non_savings_income_after_allowance`, `uk_svdv_allowance_remaining_after_non_savings`) sets the Personal Allowance "against the lowest slice first: non-savings, then" savings, then dividends, and should read `25.yaml` instead |

## Operative source

Official sources, read from legislation.gov.uk on 2026-10-31 and 2026-11-01 (ITA 2007 in the revised version that legislation.gov.uk dates from 2026-09-16; FA 2026 as enacted), and on 2026-11-02 (ITA 2007 ss. 12A, 12B and 16A and FA 2004 ss. 192 to 194, current version):

- https://www.legislation.gov.uk/ukpga/2007/3/section/25
- https://www.legislation.gov.uk/ukpga/2007/3/section/23
- https://www.legislation.gov.uk/ukpga/2007/3/section/55B
- https://www.legislation.gov.uk/ukpga/2007/3/section/8 (read 2026-11-01)
- https://www.legislation.gov.uk/ukpga/2026/11/section/6/enacted
- https://www.legislation.gov.uk/ukpga/2007/3/section/16A (read 2026-11-02)
- https://www.legislation.gov.uk/ukpga/2007/3/section/12A and https://www.legislation.gov.uk/ukpga/2007/3/section/12B (read 2026-11-02)
- https://www.legislation.gov.uk/ukpga/2004/12/section/192, https://www.legislation.gov.uk/ukpga/2004/12/section/193 and https://www.legislation.gov.uk/ukpga/2004/12/section/194 (read 2026-11-02)

ITA 2007 s. 25:

> (1) This section supplements the provisions about reliefs and allowances in Steps 2 and 3 of the calculation in section 23.
>
> (2) At Steps 2 and 3, deduct the reliefs and allowances in the way which will result in the greatest reduction in the taxpayer's liability to income tax.
>
> (3) Subsection (2) is subject to— section 65(2) to (4) (priority rule in relation to trade loss relief against general income), [...] and any other provision of the Income Tax Acts under which reliefs or allowances deductible at Step 2 or 3 are not permitted to be deducted from particular components of income or are required to be deducted from particular components of income or in a different order.
>
> (3A) Subsection (2) is also subject to a requirement that the reliefs and allowances in Steps 2 and 3 must be deducted from components of income other than property income, savings income or dividend income (so far as it would otherwise be possible to do so) before they are deducted from property income, savings income or dividend income.
>
> (4) A relief or allowance may be deducted at Step 2 or 3 only so far as there is sufficient income from which to deduct it.
>
> (5) In deciding whether there is sufficient income from which to deduct a relief or allowance, reliefs and allowances already deducted at Step 2 or 3 must be taken into account.
>
> (6) Nothing in Step 2 or 3 is to be read as permitting a relief or allowance to be deducted more than once.

ITA 2007 s. 23, Steps 5 and 6: "Add together the amounts of tax calculated at Step 4." "Deduct from the amount of tax calculated at Step 5 any tax reductions to which the taxpayer is entitled for the tax year under a provision listed in relation to the taxpayer in section 26." Section 26(1)(a) lists "Chapter 3A of Part 3 of this Act (transferable tax allowance for married couples and civil partners)", and s. 55B(1) reads: "An individual is entitled to a tax reduction for a tax year of the appropriate percentage of the transferable amount if the conditions in subsection (2) are met."

Finance Act 2026 s. 6(8): "The amendments made by this section and that Schedule have effect for the tax year 2027-28 and subsequent tax years."

ITA 2007 s. 16A (treatment of property income in hierarchy of total income):

> (3) If a person has property income but no dividend income or savings income, the property income is treated as the highest part of the person’s total income.
>
> (4) If a person—
> (a) has property income, and
> (b) dividend income or savings income (or both dividend income and savings income), the property income is treated as the part of the person’s total income immediately before the savings income or, if the person does not have savings income, immediately before the dividend income.

ITA 2007 s. 12A (savings income charged at the savings nil rate):

> (3) If £X is less than or equal to £A, income tax is charged at the savings nil rate (rather than the basic, higher or additional rate or the default basic, default higher or default additional rate) on the excess.

ITA 2007 s. 12B (individual's entitlement to a savings allowance):

> (3) If—
> (a) any of the individual's income for the year is higher-rate income, and
> (b) none of the individual's income for the year is additional-rate income, the individual's savings allowance for the year is £500.
>
> (4) If none of the individual's income for the year is higher-rate income, the individual's savings allowance for the year is £1,000.
>
> (8) For the purposes of this section—
> [...]
> (b) each of the following is “higher-rate income”—
> (i) income on which income tax is charged at the higher rate, property higher rate, default higher rate or dividend upper rate,
> (ii) income on which income tax would be charged at the higher rate, or default higher rate, but for section 12A (income charged at savings nil rate),

FA 2004 s. 192 (relief at source):

> (1) Where an individual is entitled to be given relief in accordance with this section in respect of the payment of a contribution under a pension scheme, the individual or other person by whom the contribution is paid is entitled, on making the payment, to deduct and retain out of it a sum equal to income tax on the contribution at the relevant rate.
>
> (4) If (apart from this section) income tax at the higher rate or the additional rate ... is chargeable in respect of any part of the individual's total income for the tax year, on the making of a claim the basic rate limit and the higher rate limit for the tax year in the individual's case are increased by the amount of the contribution.

FA 2004 s. 193 (relief under net pay arrangements):

> (2) The amount of the contribution is to be allowed to be deducted by the sponsoring employer from the employment income from the individual’s employment with the employer for the tax year in which the payment is made.
>
> (4) A claim for excess relief may be made if—
> (a) the amount of the contributions paid by an individual under one or more relevant net pay pension schemes in a tax year exceeds the employment income from the individual’s employment or employments with the sponsoring employer or employers for the tax year, or
> (b) it is not possible for the sponsoring employer or employers for any other reason to deduct the whole amount of the contribution from the individual’s employment income.
>
> (6) On the making of the claim for excess relief the amount of the excess may be deducted in calculating the net income of the individual for the tax year (see Step 2 of the calculation in section 23 of ITA 2007).

FA 2004 s. 194 (relief on making of claim):

> (1) Where an individual is entitled to be given relief in accordance with this section in respect of the payment of a contribution, on the making of a claim the amount of the contribution may be deducted in calculating the net income of the individual for the tax year in which the payment is made (see Step 2 of the calculation in section 23 of ITA 2007).

## Outputs and companion cases

For an individual, per tax year:

- the amount of the Step 2 reliefs and Step 3 allowances deducted from each component: income other than property, savings and dividend income; property income; savings income; and dividend income;
- those amounts summing to the reliefs and allowances deducted (s. 25(4) and (5)), none exceeding its component;
- from 2027-28, the deduction from income other than property, savings and dividend income equal to the lesser of that income and the reliefs and allowances (s. 25(3A));
- the Step 5 tax on the components left, which no other permitted deduction lowers (s. 25(2));
- no part for the Marriage Allowance in the share, because the taxpayer deducts it at Step 6 as a tax reduction (ss. 23, 26 and 55B(1)).

Each expected value comes from hand arithmetic on the statute and the 2026-27 or 2027-28 rates: Personal Allowance £12,570 tapered by £1 for each £2 of adjusted net income above £100,000 (s. 35); basic rate limit £37,700 and higher rate limit £125,140; starting rate limit for savings £5,000 (s. 12); personal savings allowance £1,000, or £500 with higher rate income (s. 12B); dividend allowance £500 (s. 13A); dividend ordinary, upper and additional rates 10.75%, 35.75% and 39.35% (s. 8); savings basic and higher rates 20% and 40% for 2026-27; property basic rate 22% for 2027-28 (FA 2026 s. 7). The policyengine-uk YAML tests use the same cases (`policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/received_allowances/least_tax_allocation.yaml`), except that its 2027-28 case uses the model's 24% property basic rate (Budget 2026 paragraph 6.246); the 22% figures below follow FA 2026 s. 7. Case 11 comes from `test_statutory_paternity_pay_income_tax_properties.py`, which pins the rise from case 11's income less one pound.

1. 2026-27, dividends £20,000 only: all £12,570 against dividends; tax (20,000 - 12,570 - 500) x 10.75% = £744.975.
2. 2026-27, savings £4,000 and dividends £20,000: £0 against savings, £12,570 against dividends; the savings fall in the starting rate band (s. 12); tax £744.975 (setting the allowance against savings first charges £1,174.975).
3. 2026-27, savings £4,000, dividends £200,000, trading loss £1,000 (s. 64): Personal Allowance nil; the £1,000 of relief against dividends; tax 3,569 + 31,259.80 + 30,637.91 = £65,466.71 (against savings first: £65,574.21).
4. 2027-28, property income £20,000, dividends £200,000, trading loss £1,000: the taxpayer has no other income for s. 25(3A), and the least tax under s. 25(2) comes with the relief against property income. Property 19,000 x 22% = 4,180; dividends from £19,000 to £219,000: 1,956.50 + 31,259.80 + 36,933.91 = 70,150.21; tax £74,330.21. Setting it against dividends instead gives 4,400 + 70,042.71 = £74,442.71, £112.50 more: setting the relief against property income moves the boundary between property and dividends, inside the basic rate band, and leaves the top of the dividends at £219,000.
5. 2026-27, employment income £50,000 and savings £10,000: £12,300 against pay and £270 against savings; tax 37,700 x 20% + (47,430 - 38,200) x 40% = £11,232 (all against pay first: £11,286).
6. 2026-27, employment income £50,000 and dividends £20,000: £12,300 against pay and £270 against dividends; tax 7,540 + 19,230 x 35.75% = £14,414.725 (all against pay first: £14,457.25).
7. 2026-27, savings £20,000 only: all against savings; tax (7,430 - 6,000) x 20% = £286.
8. 2026-27, employment income £50,000, savings £300 and dividends £20,000: £12,300 against pay, nothing against the savings, which sit within the £500 savings allowance (s. 12B), and £270 against dividends; tax 37,700 x 20% + (57,730 - 38,500) x 35.75% = £14,414.725 (all against pay first: £14,457.25).
9. 2026-27, a married couple: employment income £5,000, property income £15,000 and savings £15,000 for one spouse, no income for the other, who elects under s. 55C. Pay, property income and the savings above the £1,000 savings allowance all bear 20%, and the pay and property income leave no starting rate band (s. 12), so every share gives Step 5 tax of 7,430 x 20% + 14,000 x 20% = £4,286. Deducting the s. 55B reduction at Step 6, 20% of £1,260 (s. 55B(3) to (5): 10% of £12,570, rounded up to a multiple of £10) = £252, leaves a liability of £4,034.
10. 2026-27, employment income £30,000 at Step 1 (pay of £40,000 less £10,000 of employee contributions under a net pay arrangement) and savings £30,000: the share has only the £12,570 Personal Allowance to place, and every share keeps the £500 savings allowance below the £37,700 limit, where it saves 20%; tax 17,430 x 20% + (37,700 - 17,930) x 20% + 9,730 x 40% = £11,332. A share that also placed the £10,000 pension relief would set £20,270 against savings, leave pay at the £37,700 limit and charge £11,232, which the law doesn't allow.
11. 2026-27, employment income £48,271 and savings £2,000: £11,070 against pay and £1,500 against savings. Step 3 income is 50,271 - 12,570 = £37,701 in every share, and the pay leaves no starting rate band (s. 12), as in case 9. Savings sit above the pay (s. 16), so the top pound of Step 3 income lies above the £37,700 basic rate limit whatever the share: it bears the higher rate, or it is savings income at the savings nil rate that would bear the higher rate but for s. 12A. Either way some of the income is higher-rate income (s. 12B(8)(b)(i) or (ii)), none exceeds the higher rate limit, so none is additional-rate income, and the savings allowance is £500 (s. 12B(3)). With £1,500 against savings, the pay left of 37,201 x 20% = 7,440.20, and the £500 of savings left, from £37,201 to £37,701, is the excess of no more than £A, which s. 12A(3) charges at the savings nil rate. Step 5 tax £7,440.20. With every allowance against the pay: pay 35,701 x 20% = 7,140.20; savings from £35,701 to £37,701, the lowest £500 at the savings nil rate, 1,499 x 20% = 299.80 and 1 x 40% = 0.40; tax £7,440.40. The allowance here turns on income charged at the savings nil rate, which s. 12B(8)(b)(ii) still counts as higher-rate income: with employment income of £48,270, Step 3 income is £37,700, no income is higher-rate income, the allowance is £1,000 (s. 12B(4)) and tax is 35,700 x 20% + 1,000 x 20% = £7,340.

### review_finding (paste as-is)

> Encode ITA 2007 s. 25 (corpus uk/statute/ukpga/2007/3/25) as a new module at uk/statutes/ukpga/2007/3/25.yaml. Give s. 25(3A) effect from 2027-04-06 under FA 2026 s. 6(8) (corpus uk/statute/ukpga/2026/11/6). uk/statutes/ukpga/2007/3/23.yaml deducts the Step 3 allowances from net income as one total (`income_remaining_after_allowances`) and takes Step 4 tax as the input `tax_calculated_at_applicable_rates_on_income_remaining_after_allowances`. This module must split the Step 2 reliefs and Step 3 allowances between the components, so that 23.yaml can later charge each component left at its own rates. Import the ordering judgments from uk:statutes/ukpga/2007/3/16 (savings and dividend income as the highest part of total income, dividends above savings). Do not import uk:statutes/ukpga/2007/3/23, because 23.yaml will import this module.
>
> Required outputs, each with verbatim proof excerpts from the corpus provision:
> 1. For each component (income other than property, savings and dividend income; property income; savings income; dividend income), the amount of the Step 2 reliefs and Step 3 allowances deducted from it.
> 2. A Judgment that those amounts add up to the reliefs and allowances deducted, that none exceeds what is left of its component (s. 25(4) and (5): "only so far as there is sufficient income from which to deduct it"), and that nothing comes off twice (s. 25(6)).
> 3. A Judgment, from 2027-04-06, that the deduction from income other than property, savings and dividend income equals the lesser of that income and the reliefs and allowances (s. 25(3A): "must be deducted from components of income other than property income, savings income or dividend income (so far as it would otherwise be possible to do so) before").
> 4. A Judgment that the share gives the least Step 5 tax among the shares that s. 25(3) to (6) permit (s. 25(2): "deduct the reliefs and allowances in the way which will result in the greatest reduction in the taxpayer's liability to income tax"). If RuleSpec can't take a minimum over shares, state this judgment on a given share against the alternatives the companion tests name, and say so in the summary.
> 5. No Marriage Allowance input. Parliament gives the allowance as a "tax reduction" (s. 55B(1)), which the taxpayer deducts at Step 6 of s. 23 (s. 26(1)(a), Chapter 3A of Part 3); uk/statutes/ukpga/2007/3/55B.yaml encodes it.
> 6. No pension relief in the share. Take employment income after net pay contributions (FA 2004 s. 193(2)) as a Step 1 input. Leave relief at source to the payer's basic rate deduction and the raised basic and higher rate limits (FA 2004 s. 192(1) and (4)); neither is a Step 2 relief the share places. Only a net pay excess on a claim (s. 193(4) and (6)) and relief on a claim under s. 194(1) are Step 2 deductions; take them as out of scope here.
>
> The s. 25(3) priority rules (s. 65(2) to (4) and the others it lists) stay out of scope; take trade loss relief against general income (s. 64) as one amount the share places.
>
> Companion tests, each assigning every component and the relief and allowance amounts: cases 1 to 11 of this issue, with the expected deductions and Step 5 tax there. In case 4 (2027-28) the taxpayer has no income other than property, savings and dividends, so s. 25(3A) changes nothing. Add a 2027-28 case where it binds: with employment income of £50,000 and dividends of £20,000, all £12,570 of the Personal Allowance comes off the pay and none off the dividends, against £270 off the dividends in case 6 for 2026-27. At policyengine-uk's 2027-28 rates (basic rate limit £37,700, dividend allowance £500, dividend upper rate 35.75%): pay 37,430 x 20% = 7,486; dividends from £37,430 to £57,430, £500 at the nil rate, 19,500 x 35.75% = 6,971.25; Step 5 tax £14,457.25. Until the corpus holds the s. 8 rates, the basic rate limit and the Personal Allowance in force for 2027-28, mark this expected value as pending on the statute's rates.

## Completion boundary

This request stops at s. 25 and its commencement. The follow-up must ingest these provisions into the pinned corpus before it can encode them, and the encoder must not rely on any paraphrase here:

- The commencement of ITA 2007 s. 16A. The text quoted above places property income immediately below savings income, or immediately below dividend income where the person has no savings income, but states no start date. Case 4 of the companion tests and the 2027-28 order depend on it, and neither the corpus nor the modules hold s. 16A at rulespec-uk `0497350d74f9d0f7272b88e08019e66366d3be58`.
- FA 2026 s. 7, for the 22% property basic rate that case 4 uses. Nobody has quoted its text in this issue or in policyengine-uk#2106.

The quoted FA 2004 ss. 192(1) and (4), 193(2), (4) and (6) and 194(1) settle the pension treatment above; the corpus still needs them before the encoder can cite them.

Out of scope here: the s. 25(3) priority rules, the Marriage Allowance (already encoded in `55B.yaml`), and the Step 4 rates themselves.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Ready for hub review

Head: 056a674b9348c6656de2f64fb218eed6c374f938 on claude/allowance-order-tests-2830-93kw8s (one commit on 70633f0897957186ff9128aac91eb98ea9d7c5ec). I didn't push to fix-2106-allowance-order, open a PR or start CI. The three hub documents are the three comments above.

Findings

P1-1(a), policyengine_uk/tests/test_trading_loss_properties.py

  • Renamed the test to test_relief_never_taxes_more_than_income_cut_in_the_old_order (:386), and its key in shard_durations.toml:453. git grep now finds the old name nowhere.
  • :400: the assertion is one-sided, assert a[tax][i] - b[tax][i] <= 0.5 + tolerance(, with the same arguments and message.
  • :403-408: the old two-sided assertion still runs for people with savings_interest_income == 0 and dividend_income == 0, with a two-line comment saying why. Pinned portfolio 3 (pay £300,000, loss £200,000) runs it.
  • reduced docstring (:313-316) and module docstring item 2 (:15-24): the brief's text, rewrapped. I checked "one share the model weighs" against the code. least_tax_deductions (income_tax_allocation.py:635-664) takes the old order's deductions (to_savings, to_dividends) as its default. For searched people it compares step_5_tax at the searched least share (_least_tax_share, :477-496) with step_5_tax at the old order's share, and it moves only if the search wins by more than TOLERANCE (:655-660). People the fast path skips keep the old order (:647-651). The twin's cut, with the allowances set against it in the same order, is that old-order share, and the twin's adjusted net income is the same. So "weighs" holds, even though _least_tax_share itself may not list the old order's point as a candidate.

P1-1(b), policyengine_uk/tests/test_statutory_paternity_pay_income_tax_properties.py

  • :459: - 100.2).

  • :443-455: the comment is the brief's text, wrapped at 78 columns like the file's other comments, with no line break inside "s. 25(2)" or "ITA 2007". I rewrapped all 13 lines, not just the last two.

  • Module docstring :31: "can cost £100 of tax".

  • s. 12B reading: I agree with the hub. With the extra pound, Step 3 income is 50,271 − 12,570 = £37,701 in every share. Savings sit above the pay, so the top pound lies above £37,700 in every share. It is one of two things:

    • non-savings or savings income charged at the higher rate (s. 12B(8)(b)(i));
    • savings income at the savings nil rate under s. 12A(3), which "would be charged at the higher rate … but for section 12A" (s. 12B(8)(b)(ii)).

    Nothing exceeds £125,140, so no income is additional-rate income, and s. 12B(3) gives £500. The model agrees. savings_allowance.py compares net_income - allowances with the basic rate limit plus grossed-up Gift Aid, and no share changes either. Model run at head, 2026-27:

    • HIGH (pay £48,271): income_tax_before_winter_fuel_payment_charge 7,440.2002; received_allowances_earned_income 11,070; received_allowances_savings_income 1,500; savings_allowance 500.
    • LOW: 7,340, with 12,570 against earnings and an allowance of 1,000.
  • By hand, savings left of s ≤ £500 give (37,701 − s) × 20%, which is least at s = 500, so £1,500 against savings is the only least share.

Body bullets (item 3): I added both bullets at the end of "Existing tests whose expected values move, and why", with figures from runs at head:

  • trading loss: £637.47 against £1,282.47;
  • paternity pay: a rise of 100.2002, with the £1,500/£11,070 split.

Test plan (item 4): I added a "Ran at 056a674…" list and labelled the older lists by commit. The checkbox now reads "Lint and format checks pass at 056a674…". The "Not run" line is unchanged.

Merge notes: I reran them from head.

P3-1: I replaced the pension paragraph's last clause with the review's text.

P3-2: income_tax_allocation.py:26-28 (docstring) and :227-229 (comment) now read "(s. 192(4), and for the Scottish rate limits s. 192(4A))". That matches s. 192(4A), which raises "the Scottish basic rate limit, and any other Scottish rate limit … above the Scottish basic rate limit". AST against 70633f0 with docstrings blanked: SAME. The comment change is invisible to the AST anyway.

P3-3 and case 11, AXIOM_ISSUE.md:

  • I replaced the pending sentence in the Companion tests paragraph.
  • I added case 11, worked from ss. 12A(3), 12B(3), (4) and (8)(b)(i) and (ii), and 25(2):
    • the share (£1,500 against savings);
    • the £500 allowance and why the higher-rate test still bites under s. 12B(8)(b)(ii);
    • Step 5 tax of £7,440.20 against £7,440.40, and the £7,340 at £48,270, where s. 12B(4) gives £1,000.
  • I added the 12A and 12B URLs (read 2026-11-02), and the date note now lists them.
  • I quoted s. 12A(3) and s. 12B(3), (4) and (8)(b)(i) and (ii) word for word, eliding (8)(a) with "[...]".
  • "cases 1 to 10" → "cases 1 to 11". It appeared once.
  • One sentence in the intro now points to the paternity pay test as case 11's source.
  • Change to the quoted text: the fetched s. 12B(8)(b) has stray spaces before commas ("higher rate , property higher rate"). I dropped them, as the previous round did for s. 192(1).
  • Case 11 leans on s. 16 (savings above non-savings) and, as case 9 does, on s. 12 for "no starting rate band". Nobody quoted either in the brief, but the packet already cites both and the corpus holds s. 16.

Where I departed from the review's or the brief's text

  • reduced docstring and item 2: the hub's versions, not the review's. They name an actor and add the no-choice sentence.
  • The paternity comment adds the hub's s. 12B(8)(b)(ii) sentence and is rewrapped throughout.
  • Body: I added a judgment call, "The savings allowance under the share", which quotes s. 12B(8)(b)(ii).

Tests at 056a674

  • The trading loss and paternity pay files: 20 passed with -n 4, and 20 passed without xdist.
  • test_income_tax_allocation.py and test_uc_income_tax_properties.py: 24 passed.
  • YAML:
    • income tax directory: 490 passed;
    • least_tax_allocation.yaml: 18;
    • trade_loss_relief_against_general_income.yaml: 22.
  • code_health -n 4: 3,089 passed.
  • check_fix_pr.py on the final title and body, run as pr_fix_evidence.yaml does (changes from merge base 2da212a): "Fix evidence: PASS", 15 changed executable tests.
  • uvx ruff@0.17.2 format --check .: 2167 files already formatted. ruff check . and --select F811,F821: all passed. git diff --check 70633f089..HEAD: clean.

Mutation checks

Each ran on a scratch copy, on the renamed test without xdist, and was then restored.

  1. Two-sided assertion for everyone: fails on pinned person 1 with 637.4749755859375 against 1282.4749755859375, as CI did.
  2. net_income.py:37 subtracts 2 * trade_loss_relief_against_general_income, and the relief variable is untouched. This was a clean mutant.
    • The relief assertion and the one-sided bound pass.
    • The no-choice branch fails at :406 on pinned person 3: 53,703 against 87,453.
    • With that branch disabled, the test passes under the mutant. So only the hub's added check catches it.

Skipped


Generated by Claude Code

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

Copy link
Copy Markdown
Collaborator Author

Ready for hub review

Verdict: APPROVE WITH NITS at 3b54d5397e6da2c28357b84ec5e3b49476d5d27f.

The delta closes P1-1(a), P1-1(b) and P3-1 to P3-3 from the review of 70633f089. CI at this head is green: all seven Python shards, YAML, Lint, Fix evidence and Aggregate impact pass. I reran every figure the delta adds, by hand and in the model, and they match. The hub's s. 12B reading holds against the quoted law. No formula changed. Only text nits remain, and none affects a result.

How I worked: a detached worktree of 3b54d5397, set up with uv sync --frozen --extra dev, with HUGGING_FACE_TOKEN and POLICYENGINE_UK_DEFAULT_DATASET unset. Each mutation ran on that scratch tree and was restored with git checkout; git status was clean afterwards.

P1

None.

P2

None.

P3

P3-1. income_tax_allocation.py:24-30: the s. 192(4A) citation is broader than (4A), and the docstring is left ragged.

  • (4A) doesn't raise every Scottish rate limit. It raises "the Scottish basic rate limit, and any other Scottish rate limit … that is above the Scottish basic rate limit". Its condition also differs from the sentence's "where higher or additional rate tax is chargeable": it needs tax "at a Scottish rate" that "is higher than the Scottish basic rate", which the intermediate rate meets. The equivalence argument stands, as the previous round showed. Only the citation's wording overreaches.
  • 3b54d5397 rewrapped the comment, but the docstring still breaks early at :30 ("limits, as raising them does. Neither arrangement").

Replace docstring lines 25 to 33, from "the payer keeps" to "shares only the rest.", with:

the payer keeps basic rate tax on the contribution (s. 192(1)). On a claim,
Parliament raises the basic and higher rate limits by the contribution where
higher or additional rate tax is chargeable (s. 192(4)), and for a Scottish
taxpayer the Scottish basic rate limit and the Scottish rate limits above it
where a Scottish rate above the Scottish basic rate is chargeable
(s. 192(4A)). Deducting the contribution from the bottom of non-savings
income moves every slice of income down against those limits, as raising
them does. Neither arrangement gives a Step 2 deduction the taxpayer can set
against savings or dividends, so the search holds
``min(pension relief, non-savings income)`` on non-savings income and shares
only the rest.

In the comment at :228, replace "(s. 192(4), and for the Scottish rate limits s. 192(4A))" with "(s. 192(4); for a Scottish taxpayer, the Scottish basic rate limit and those above it, s. 192(4A))", then rewrap. Both changes are text only. Rerun the AST comparison afterwards.

P3-2. Body, test plan and merge notes: production history, and SHAs that lag the head.

  • :128 reads "At 3b54d53…, we rewrapped one comment in income_tax_allocation.py on top of 056a674." The "we" and the rewrap describe how the work was produced. Replace with: "At 3b54d53: income_tax_allocation.py differs from 056a674 only in one comment, and its AST, with docstrings blanked, matches 056a674's, so the runs above hold for this head. At this head CI passes on every job, uvx ruff@0.17.2 format --check ., check . and check --select F811,F821 policyengine_uk pass, and .github/check_fix_pr.py gives "Fix evidence: PASS" with 15 changed executable tests."
  • Checkbox :201: "Lint and format checks pass at 3b54d53." I ran them there (below).
  • Merge notes header :232: replace "from 056a674" with "from 3b54d53". I reran all three from this head with the same results (below).
  • :236: replace the line with "origin/main at 0080274: exit 0, no conflict."
  • "Not run" (:198): CI is green, so the hub can replace it with "CI at 3b54d53 (pull_request run 41520177402): Python shards 1 to 7 pass 11,579 tests (61 skipped, 2 xfailed), and Test / YAML passes 5,428. Not run: any test that needs a survey dataset."

P3-3. Rules cited as actors in the new s. 12B text. The writing rules have Parliament act and cite the rule in brackets.

  • AXIOM_ISSUE.md, case 11. The sentence "With £1,500 against savings, the pay left of 37,201 x 20% = 7,440.20, and the £500 of savings left, from £37,201 to £37,701, is the excess of no more than £A, which s. 12A(3) charges at the savings nil rate." has no verb in its first clause, and it makes s. 12A(3) the actor. Replace with: "With £1,500 against savings, the pay left of £37,201 bears 37,201 x 20% = 7,440.20. The £500 of savings left, from £37,201 to £37,701, is all of the excess, and £X does not exceed £A, so Parliament charges it at the savings nil rate (s. 12A(3))."
  • Same case: replace "which s. 12B(8)(b)(ii) still counts as higher-rate income" with "which Parliament still counts as higher-rate income (s. 12B(8)(b)(ii))".
  • Body, judgment call "The savings allowance under the share": replace "s. 12B(8)(b)(ii) still counts it as higher-rate income (" with "Parliament still counts it as higher-rate income (s. 12B(8)(b)(ii): ".

P3-4. Optional: widen the no-choice branch (test_trading_loss_properties.py:405). A person with only savings, or only dividends, also has one component, so every share is the cut there too. A condition such as "at most one of non-savings income, savings and dividends left after the cut is positive" would also run the equality on pinned portfolio 2 (dividends only). The module docstring's "no savings or dividend income" would then need to match. As written, the branch is correct, and pinned portfolio 3 exercises it.

Earlier findings and this round's rulings

Item Status Evidence
P1-1(a) trading loss test Closed Renamed at :386 and in shard_durations.toml:453. The one-sided bound is at :400, and the two-sided check for no savings or dividends is at :405-408.
P1-1(b) paternity pay test Closed :459 - 100.2). The comment :443-455 matches the brief, and the module docstring :31 reads "can cost £100 of tax".
P3-1 pension paragraph Closed The live body carries "as s. 25(3A) requires of the reliefs and allowances in Steps 2 and 3."
P3-2 Scottish limits Closed, with a nit See P3-1 above.
P3-3 Axiom pending note Closed The sentence is replaced as given.
Case 11 (hub addition) Closed, with a nit The arithmetic and quotations are right; the wording is in P3-3 above.

Checked and sound

P1-1(a): the one-sided bound can fail. It fails wherever the model charges more than the old-order cut, for example when the relief comes off net income short of its full amount:

Mutant (net_income.py:37) Result
- 0 * relief Killed by the one-sided bound on pinned person 1: 2787.47509765625 against 1282.4749755859375
- 0.5 * relief, with the no-choice branch disabled Killed by the one-sided bound on pinned person 1: 1712.4749755859375 against 1282.4749755859375
- 2 * relief (the author's mutation 2) Killed only by the no-choice branch on pinned person 3: 53703.0 against 87453.0, a difference of 33,750. The one-sided bound and the relief assertion pass, as the hand-off says

So the bound catches under-relief, and the kept equality catches over-relief. The author didn't rerun "survives with the branch disabled", and I didn't either.

P1-1(a), the docstrings against the code.

  • least_tax_deductions (income_tax_allocation.py:636-666) starts from the old order's to_savings and to_dividends. It moves a searched person only where step_5_tax at _least_tax_share (:478-498) falls below step_5_tax at the old order's share minus TOLERANCE (:657-661, TOLERANCE = 1e-3 at :142).
  • People that _old_order_is_least clears keep the old order (:647-653).
  • old_order (:514-536) takes the relief off non-savings income first, then the allowances off what is left, then savings, then dividends. That is the twin's cut with the allowances set in the same order.
  • So "one share the model weighs" and "charges no more tax than on this cut" match the code.

Model runs, 2026-27, London, age 40.

Run income_tax_before_winter_fuel_payment_charge Allocation
Pinned person 1 637.4749755859375 savings 4,000, dividends 23,570
Its twin (dividends £25,000) 1282.4749755859375 12,570 against dividends
Paternity LOW (pay £48,270, savings £2,000) 7,340 earned 12,570, savings_allowance 1,000
Paternity HIGH (pay £48,271) 7440.2001953125 earned 11,070, savings 1,500, savings_allowance 500

The paternity rise is 100.2001953125.

Hand checks.

  • Twin: (12,430 − 500) × 10.75% = 1,282.475.
  • Pinned person 1 with the loss: savings of £6,000 sit within the £5,000 starting rate band plus the £1,000 allowance; dividends of 6,430, less the £500 nil band, give 5,930 × 10.75% = 637.475.
  • Paternity HIGH: Step 3 income is £37,701 in every share. With savings s left, tax is 0.2 × (37,701 − s) for s ≤ 500. Above 501 it is a flat 7,440.40. So s = 500, that is £1,500 against savings, is the unique least share at £7,440.20.

The s. 12B reading, against the quoted law: right.

  • Under the share, all £500 of savings left lies above the starting rate limit. It is "the excess", and £X = 500 ≤ £A, so it bears the savings nil rate (s. 12A(2) and (3)).
  • The top pound, from £37,700 to £37,701, would bear the higher rate but for s. 12A, so it is higher-rate income (s. 12B(8)(b)(ii)).
  • No income exceeds £125,140, so none is additional-rate income, and s. 12B(3) gives £500.
  • The reading is consistent with itself: assuming £1,000 still leaves that pound as higher-rate income, so £1,000 can't hold.
  • At £48,270, no income exceeds £37,700, so s. 12B(4) gives £1,000.

AXIOM_ISSUE.md.

  • Case 11's figures, 7,440.20, 7,440.40 (7,140.20 + 299.80 + 0.40) and 7,340, match by hand.
  • Each quotation matches the fetched text word for word: s. 12A(3), s. 12B(3), (4), (8) opening line, (8)(b)(i) and (ii). The one change is the stray spaces before commas in (8)(b)(i) and (ii), which the author dropped and disclosed.
  • "cases 1 to 11" appears once, in review_finding, and the list numbers 1 to 11.
  • The 12A and 12B URLs and the 2026-11-02 date note are present.

3b54d5397.

  • git show touches only the comment at :226-231.
  • The AST of income_tax_allocation.py, with module, class and function docstrings blanked, is SAME for 056a674b9 against 3b54d5397, and for 70633f089 against 3b54d5397.

Merge notes, git merge-tree --write-tree --name-only 3b54d5397 <sha>:

Tests at 3b54d5397.

  • test_trading_loss_properties.py and test_statutory_paternity_pay_income_tax_properties.py, -n 4: 20 passed.
  • test_income_tax_allocation.py and test_uc_income_tax_properties.py: 24 passed.
  • Income tax YAML directory: 490 passed. least_tax_allocation.yaml: 18. trade_loss_relief_against_general_income.yaml: 22.
  • policyengine_uk/tests/code_health -n 4: 3,089 passed.
  • uvx ruff@0.17.2 format --check .: "2167 files already formatted". check . and check --select F811,F821 policyengine_uk: "All checks passed!".
  • git diff --check 70633f089..HEAD: clean. No line exceeds 88 characters in the three changed Python files.
  • .github/check_fix_pr.py on the live title, body and labels, with changes from merge base 2da212abc: "Fix evidence: PASS … Changed executable tests: 15."

CI at the head. Every check run is success except one "Fix evidence" job, 125533851221, which is cancelled. Every step in its log, "Check fix evidence" included, succeeded. Run 41520172452 was cancelled when the next pull_request_target run for the same head, 41520218202, started 34 s later, and that run and the later 41522273627 both succeed. So this delta doesn't cause the cancellation.

Python shards from the job logs:

Shard Result
1 1,635 passed
2 1,659 passed
3 1,682 passed
4 1,636 passed
5 1,671 passed
6 1,621 passed
7 1,675 passed

Test / YAML passed 5,428, and Test / Result succeeded.

Live PR. The title and body match the hub's documents: the first lines are kept, both new "expected values move" bullets are there, and the s. 12B judgment call and the P3-1 text are in place.

What I couldn't check

  • Survey-dataset tests, and the whole suite locally (CI ran it).
  • The 6,520-person and 20,000-person samples, and the Fix Scottish tax bands and test Marriage Allowance rates directly #2828 resolved-merge suites at this head.
  • Any provision nobody quoted, among them ITA 2007 ss. 12 and 16, on which case 11 leans for "no starting rate band" and "savings sit above the pay".
  • Whether rulespec-uk#524 as filed matches AXIOM_ISSUE.md; that repository is outside this session's access.

Generated by Claude Code

FA 2004 s. 192(4A) raises the Scottish basic rate limit and the Scottish
rate limits above it, where a Scottish rate above the Scottish basic rate
is chargeable. The docstring and one comment said "the Scottish rate
limits". Text only; the AST with docstrings blanked matches 3b54d53.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review November 3, 2026 00:44
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

For C1, we've quoted the law behind case 4 in rulespec-uk#524. Parliament set the 2027-28 property basic rate at 22% by FA 2026 s. 7. By s. 6(3) and (4) it inserted ITA 2007 ss. 11CA and 16A, with effect from 2027-28 (s. 6(8)). We quote s. 7, s. 11CA(1) and (4), and the s. 6(4) insertion beside the s. 6(8) commencement text.

We now cite the law for each step of case 4, where we take a taxpayer who is neither a Scottish nor a Welsh taxpayer (s. 11CA(4)). The £19,000 of property income sits immediately below the dividends (s. 16A(4)), within the basic rate band, so Parliament charges it at 22% (s. 11CA(1) and s. 7(a)). We added the same steps to the review_finding block, with uk/statute/ukpga/2026/11/7 as the proposed corpus selector for s. 7.

At 0497350d74f9, rulespec-uk's corpus holds FA 2026 ss. 1, 2, 3, 9 and 10, and neither the corpus nor the modules hold ITA 2007 ss. 11CA or 16A. So we list those sections, and FA 2026 ss. 6 and 7, in the completion boundary for the follow-up to ingest. We read ss. 6 and 7 as enacted on legislation.gov.uk on 3 November, and the current version has the same text.

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed 6602ccce488f1302972331d61ba94f02528d08e7 and the new filed evidence. COMPLETE — C1 RESOLVED; no open findings or material evidence gaps.

The actual Axiom #524 issue now supplies FA 2026 s.7's 22% rate, the operative s.11CA and s.16A insertions, commencement, the non-Scottish/non-Welsh case scope and the module/corpus ingestion boundary. These match the independently checked official legislation. Companion case 4's statutory 22% calculation remains separate from the PR's announced 24% scenario.

Code and description are unchanged. The prior 143 passing checks and native result were verified and retained, with current runtime/dependency pins; no tests or model calculations were rerun. Applicable checks pass at this exact head. Approved.

@juaristi22
juaristi22 added this pull request to the merge queue Nov 5, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 5, 2026
MaxGhenis and others added 2 commits November 6, 2026 05:21
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main added this case after this branch's last CI run. With the allowances
set for the least tax, the parent's income tax falls by 100, so the case now
expects 15,935, with 7,540 on the pay and a deduction of 13,649.

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

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Incremental review at aed86e9a3154db7cdfbc015603dcccd825130c7f: COMPLETE, no remaining findings or material evidence gaps. Prior finding C1 remains resolved. Production relief-allocation logic is unchanged from the previously approved head; this round checked the changed test oracles and affected dependencies, retaining the relevant earlier statutory evidence.

Validation: 11 repository tests passed (9 YAML and 2 Python), plus a fresh two-household control through final income tax and household net income. The earlier 143 checks remain historical evidence and were not repeated. All 33 retained official source originals and their text extracts passed integrity checks; the enacted 2027 property rates remain distinguished from the announced scenario.

This is a completed review comment. Fresh approval is deferred while exact-current-head CI is pending; CI can be checked later without repeating the completed code/source review.

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

All 20 checks pass at aed86e9a3, so this head only needs your approval.

@MaxGhenis
MaxGhenis requested a review from juaristi22 November 6, 2026 11:54

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved at aed86e9a3154db7cdfbc015603dcccd825130c7f. The completed independent review has no remaining findings or material evidence gaps; C1 remains resolved. All 20 latest checks now pass at this exact head, including Aggregate impact and the required Lint and Test / Result checks.

This closes the pending-CI approval hold. The reviewed code and description are unchanged, the retained source evidence was integrity-checked, and the author's approval request adds no new scope. No code review, tests or model calculations were repeated.

MaxGhenis added a commit that referenced this pull request Nov 6, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Nov 6, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@juaristi22
juaristi22 added this pull request to the merge queue Nov 9, 2026
@juaristi22
juaristi22 removed this pull request from the merge queue due to a manual request Nov 9, 2026
…he allowance order branch

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

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review status: COMPLETE. Request changes at 321fb198acd86ec9507a7f3abaa7e586d22c38fc.

C2 — Account for Marriage Allowance in the final-liability objective. The allocator's Step 5 objective/fast path at income_tax_allocation.py:652 misses a lower final bill after the newly merged Step 6 credit.

Executed case: 2026, England, married 40-year-olds; recipient earnings £40,000, savings £9,000 and dividends £1,570, with a zero-income donor and a valid transfer election/UK residence assumed, without Married Couple's Allowance. The current calculation puts the £12,570 Personal Allowance against earnings, gives no Marriage Allowance credit, and charges £7,376.02. A feasible split of £2,500 against earnings, £8,500 against savings and £1,570 against dividends leaves £37,500 non-savings income, £500 savings covered by the savings nil rate, and no dividends. The model then calculates the £252 credit and £7,248 final tax. Eligibility, the credit and final tax were calculated; only the allocation was controlled. This proves at least £128.02 excess tax, rather than claiming £7,248 is the global minimum.

ITA 2007 s25(2) requires the greatest reduction in liability, which s23 defines through the later steps. S55B(2)(b)–(ba) distinguishes savings nil-rate income from the dividend-rate test without the dividend allowance. The objective and pruning need to account for the resulting Step 6 eligibility/credit, with a regression covering this household. A Step 5-only tie-break does not fix this case.

A1 — Refresh the Marriage Allowance description. The present-tense paragraphs still describe the old earned-income subtraction and a future Step 6 fix, although the merged dependency now implements the credit. Update those statements or date them as historical; keep the old grid results attributed to their original heads.

C1 remains resolved. The statutory rate evidence and queued companion-case source packet remain valid.

Validation: 143 current repository tests passed (99 YAML, 44 Python). A separate household assertion in the same bounded campaign exposed C2; a focused actual-versus-two-legal-allocation comparison confirmed it without forcing eligibility, credit or tax. The overall campaign exit 1 is retained as finding evidence. Both independent roles completed; 36 cached official source/extract pairs validated, with no new source acquisition. Exact-head Python/aggregate CI was pending at final capture and is refreshed again before this review is submitted. No approval is issued.

MaxGhenis and others added 2 commits November 9, 2026 17:11
Section 25(2) asks for the greatest reduction in liability, which s. 23
fixes after the Step 6 tax reductions. The share decides whether a
recipient meets the Marriage Allowance rate test (s. 55B(2)(b) and (ba)),
so the search now compares the Step 5 tax less the reduction each share
wins. marriage_allowance_reduction_at_stake gives it that reduction
whatever the share, and the CPS expansion sets it to nil where the reform
gives the allowance at any share.

The search adds the rate test's limit on non-savings income to its lines;
the test's other edges, no dividends left and savings left at the savings
allowance, already lie among them. It counts a share as meeting the test
only with room below each limit, and sets savings or dividends exactly
where the test reads them, so float32 rounding can't deny the reduction.
It judges the old order by the test's own arithmetic, so a recipient the
old order qualifies moves only for a lower Step 5 tax.

The rate test moves to utils/marriage_allowance.py, which the allocation
and meets_marriage_allowance_income_conditions share. YAML cases 19 to 23
and new property tests cover recipients, and case 13's comment, the module
docstring and the changelog describe the Step 6 reduction. Since #2828 the
model no longer subtracts the allowance from earned income, so the
synthetic people's £1,260 exclusion from earned income drops the
allowance's name. The Universal Credit earnings slice takes off the Step 6
reductions, so more savings or dividends can raise it by failing a
recipient on the rate test; its comment and property-test docstring now
say so.

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

Copy link
Copy Markdown
Collaborator Author

For C2, the search at 9f33c78 minimises the Step 5 tax less the Marriage Allowance reduction each share wins. For your household, it sets £2,500 of the Personal Allowance against pay, £8,500 against savings and £1,570 against dividends. Income tax comes to £7,248, as YAML case 19 now checks. For the pruning, the fast path sends to the search each recipient whom the old order denies the reduction, wherever some share could bring their non-savings income within the rate test's limit. Cases 20 and 22 add recipients whose share wins the allowance by clearing dividends and stopping savings at the nil rate, one with pay in 2026-27 and one with property income in 2027-28. In case 21 pay alone exceeds the limit, so no share wins it, and in case 23 a recipient whom the old order qualifies at the limit keeps that order.

For A1, we rewrote the Marriage Allowance paragraphs around the Step 6 reduction that #2828 merged. We also tied the earned-income subtraction, the older grid results and the aggregate estimate to the heads they come from.

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Follow-up review at 9f33c7801d3bf7a40005f9561404b5fc7631d3de: C1, C2 and A1 are resolved. No new code finding; overall status is PARTIAL solely for hosted CI execution.

C2 resolved. With the original 2026 England household (married 40-year-olds; recipient earnings £40,000, savings £9,000 and dividends £1,570; zero-income donor), the model now automatically allocates £2,500/£8,500/£1,570 of deductions to those income components, calculates the £252 Marriage Allowance credit, and returns £7,248 income tax and £41,227.60 household net income. Allocation, eligibility, credit and final tax were not forced. The final-liability objective and pruning now account for the credit. New cases 19–23 and the focused boundary/helper/CPS/UC checks passed.

A1 resolved. The description now states the current Step 6 treatment and attributes the old subtraction/grid/aggregate results to their historical heads. C1 remains resolved with the retained statutory source packet.

Validation: 84 current tests passed (74 YAML, 10 Python), plus the automatic original-household calculation in the same bounded invocation. Six runtime pins matched the lock and UK imports resolved to the captured snapshot. Both independent roles completed; 36 cached official source/extract pairs validated, with no new policy-source acquisition. The earlier 143 passes and the author's grid are not counted as new executions.

CI_EXECUTION remains open. Python 4/7, job 129498543581 is reported against this PR head, with synthetic checkout 6f1da93e on base 93b1711e. Its test step was cancelled after the runner received a shutdown signal. No failed assertion or model defect was identified in the captured job log/annotation. A successful current-head hosted run is still needed before approval. Please rerun the interrupted CI; I will recheck the CI gate without repeating this completed implementation/source review.

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approve at 9f33c7801d3bf7a40005f9561404b5fc7631d3de. The CI-only gap from the completed fix review is closed: all 20 latest exact-head checks passed, including Lint and Test / Result. The replacement Python 4/7 check passed; the earlier interrupted runner attempt is superseded.

C1, C2 and A1 remain resolved, with no open findings or material evidence gaps. This retains the prior exact-head 84 test passes (74 YAML, 10 Python) and automatic Marriage Allowance witness (£7,248 final income tax, £252 credit); no tests or model runs were repeated. The 36 cached source pairs and 107 technical receipt members were integrity-checked.

Head, description, human activity, source dependencies, readiness and applicable required checks are refreshed before publication.

@juaristi22
juaristi22 added this pull request to the merge queue Nov 10, 2026
Merged via the queue into main with commit 0f38c3c Nov 10, 2026
35 of 38 checks passed
@juaristi22
juaristi22 deleted the fix-2106-allowance-order branch November 10, 2026 15:09
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.

Allowances and reliefs are set against savings before dividends, not in the order giving the greatest reduction (ITA 2007 s.25(2))

3 participants