Skip to content

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

Open
MaxGhenis wants to merge 19 commits into
mainfrom
fix-2106-allowance-order
Open

MaxGhenis wants to merge 19 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, 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 5d9ce13'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; nobody has rerun it at 6429896.

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 (5d9ce13). 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 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; nobody has timed 6429896 or later.

Aggregate impact

We estimate that income tax receipts fall, and household net income rises, 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 gain over £1, and 0.0% lose 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, 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 (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.
  • 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 70633f0 and on origin/main 6c6e229 alike.

Test plan

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 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 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 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 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 10.

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 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.

  • 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.
  • Lint and format checks pass at 6602ccc.
  • 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 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, 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)).

Merge notes

git merge-tree --write-tree --name-only with explicit SHAs, from 6602ccc:

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.
  • 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 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 0080274d78 against head 3b1d34afc6, 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: 2 (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

Measure Year Base Head Change
Decile 5: mean household_net_income per household 2030-31 £48,584 £48,724 +£140
Decile 6: mean household_net_income per household 2030-31 £53,942 £53,827 -£114

Receipts by tax head

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Income tax -£18.7m -£19.5m -£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 -£284 -£0 0 -£0 0 -£4,815 -£9,447
Universal Credit +£2 0 +£2 0 0 0 0
Housing Benefit -£53 0 0 0 0 0 0
Council Tax Reduction -£0 -£1,090 -£1,125 -£1,166 -£1,211 -£1,255 -£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.7m -£19.5m -£9.5m -£10.7m -£20.4m -£20.6m -£11.3m
Government spending -£336 † -£1,090 † -£1,123 † -£1,166 † -£1,211 † -£6,070 † -£10,748 †
Government balance, each receipt once -£18.7m † -£19.5m † -£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.7m -£19.5m -£9.5m -£10.7m -£20.4m -£20.6m -£11.3m
household_tax -£18.7m -£19.5m -£9.5m -£10.7m -£20.4m -£20.6m -£11.3m
pre_budget_change_household_tax -£18.7m -£19.5m -£9.5m -£10.7m -£20.4m -£20.6m -£11.3m
gov_spending -£336 † -£1,090 † -£1,123 † -£1,166 † -£1,211 † -£6,070 † -£10,748 †
household_benefits -£336 † -£1,090 † -£1,123 † -£1,166 † -£1,211 † -£6,070 † -£10,748 †
gov_balance -£18.7m † -£19.5m † -£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.7m † +£19.5m † +£9.5m † +£10.7m † +£20.4m † +£20.6m † +£11.3m †
Household net income (HBAI definition) +£18.1m +£18.9m +£9.0m +£10.2m +£19.9m +£20.2m +£10.9m

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.17 † +£0.15 † -£0.40 † +£0.13 † +£0.11 †
Decile 2: mean household_net_income per household +£0.05 † +£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.05 † +£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 † +£140 † +£0.12 †
Decile 6: mean household_net_income per household +£0.05 † +£0.01 † +£0.00 † +£0.00 † +£0.00 † -£114 † +£0.02 †
Decile 7: mean household_net_income per household +£0.33 † +£0.76 † +£0.44 † +£0.36 † +£0.39 † +£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.02 † +£0.03 †
Decile 10: mean household_net_income per household +£2.59 † +£1.89 † +£0.37 † +£0.61 † +£3.46 † -£99 † +£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.7m +£2.8m +£385 +£568 +£6 -£485 +£106
Raise higher rate by 1pp -£807 +£513 +£249 +£372 -£307 -£1,311 -£251
Raise personal allowance by ~800GBP/year -£2.9m -£0.4m -£0.3m -£0.1m +£1.3m -£0.3m -£1.6m
Raise child benefit by 25GBP/week per additional child 0 -£3 +£13 +£18 +£5 -£7 +£46
Reduce Universal Credit taper rate to 20% +£1 +£4 -£12 -£211 +£25 -£9 +£2
Raise Class 1 main employee NICs rate to 10% +£212 -£400 -£98 -£288 -£246 suppressed suppressed
Raise VAT standard rate by 2pp -£1,285 +£734 +£216 -£243 -£456 -£237 +£341
Raise additional rate by 3pp -£327 +£2,359 -£9 +£645 -£166 suppressed suppressed
Raise the Universal Credit standard allowance by 10% +£1 +£0 +£3 -£85 -£11 -£23 +£0
Raise the State Pension by 10% -£0.6m +£0.5m +£0.1m +£0.3m +£1.0m +£1.3m +£1.2m

The report suppresses a reform's costing and target change in these years, for the reasons below (privacy.py):

  • Raise Class 1 main employee NICs rate to 10%, 2030-31 to 2031-32 (base and head): the reform changes income_tax in fewer than 10 records.
  • Raise additional rate by 3pp, 2030-31 to 2031-32 (base and head): the reform changes universal_credit in fewer than 10 records.

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 +£129 -£11 -£238 +£58 -£47
Raise higher rate by 1pp: change in income_tax -£151 +£533 +£2 -£23 -£252 -£242 -£279
Raise personal allowance by ~800GBP/year: change in income_tax -£2.9m -£0.3m -£0.3m -£0.1m +£1.3m -£0.3m -£1.6m
Reduce Universal Credit taper rate to 20%: change in universal_credit +£0 +£0 -£2 +£0 0 0 +£1
Raise Class 1 main employee NICs rate to 10%: change in national_insurance 0 0 0 0 0 suppressed suppressed
Raise additional rate by 3pp: change in income_tax -£155 +£469 +£1 -£1 +£0 suppressed suppressed
Raise the Universal Credit standard allowance by 10%: change in universal_credit +£0 0 0 0 0 0 0

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 £-53,888,684 in total; 2026-27: 76 units below zero, a weighted £-55,777,260 in total; 2027-28: 76 units below zero, a weighted £-57,727,917 in total
Counted-once check

Each of 92 components was raised by £1 per unit in 2026-27, and every head of its totals was read 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 head that breaks a rule is measured again twice: with every other head held at its baseline value, and with every other head held at its value after the first £1 while the component rises again. It fails where either still shows the response. 0 failed; 0 were skipped.

  • 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 0080274d78 uk 2.171.0 (files d72b65d494db), core 3.33.12, py 3.13.18 aarch64 c07a637ca255 7cb61324a46b 2042.75s
head #2830 merged into main 3b1d34afc6 uk 2.171.0 (files c17686240e5f), core 3.33.12, py 3.13.18 aarch64 c07a637ca255 7cb61324a46b 2129.67s

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.21bn £335.82bn £351.27bn £366.94bn £385.34bn £399.99bn
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 £249.37bn £256.87bn £266.33bn £272.70bn £278.97bn £285.24bn £291.59bn
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.49bn £132.99bn £136.66bn £140.42bn
Pension Credit £6.66bn £7.03bn £7.29bn £7.48bn £7.60bn £7.84bn £8.03bn
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.48bn £80.52bn £82.95bn £84.96bn £86.11bn £87.61bn £88.21bn
Housing Benefit £8.56bn £8.89bn £9.23bn £9.46bn £9.69bn £9.99bn £10.27bn
Council Tax Reduction £2.07bn £2.20bn £2.31bn £2.45bn £2.58bn £2.72bn £2.86bn
Employment and Support Allowance (income-related) £0.79bn £0.82bn £0.84bn £0.86bn £0.88bn £0.90bn £0.92bn
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
Carer's Allowance Supplement (Scotland) £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,486k 1,480k 1,472k 1,452k 1,450k 1,455k 1,464k
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,342k 6,347k 6,336k 6,297k 6,292k 6,271k 6,223k
Housing Benefit 1,249k 1,245k 1,244k 1,227k 1,227k 1,229k 1,232k
Council Tax Reduction 3,176k 3,195k 3,200k 3,214k 3,224k 3,235k 3,242k
Employment and Support Allowance (income-related) 71k 71k 71k 71k 71k 71k 71k
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
Carer's Allowance Supplement (Scotland) 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 £891.98bn £934.64bn £980.49bn £1,015.92bn £1,054.22bn £1,091.54bn £1,124.88bn
Government spending £529.38bn † £545.66bn † £550.77bn † £559.51bn † £567.20bn † £575.95bn † £583.60bn †
Government balance, each receipt once £362.60bn † £388.98bn † £429.73bn † £456.41bn † £487.02bn † £515.59bn † £541.28bn †
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 £891.98bn £934.64bn £980.49bn £1,015.92bn £1,054.22bn £1,091.54bn £1,124.88bn
household_tax £491.95bn £520.39bn £551.20bn £576.00bn £600.98bn £626.68bn £648.41bn
pre_budget_change_household_tax £484.13bn £512.26bn £542.81bn £566.94bn £591.70bn £617.14bn £638.60bn
gov_spending £529.38bn † £545.66bn † £550.77bn † £559.51bn † £567.20bn † £575.95bn † £583.60bn †
household_benefits £529.38bn † £545.66bn † £550.77bn † £559.51bn † £567.20bn † £575.95bn † £583.60bn †
gov_balance £362.60bn † £388.98bn † £429.73bn † £456.41bn † £487.02bn † £515.59bn † £541.28bn †
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.71bn † £1,687.84bn † £1,715.46bn † £1,747.50bn † £1,778.71bn † £1,817.04bn † £1,859.70bn †
Household net income (HBAI definition) £1,449.59bn £1,499.85bn £1,530.87bn £1,565.18bn £1,597.91bn £1,635.62bn £1,676.63bn
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.3% 15.5% 16.4% 16.3% 16.5% 16.5% 16.6%
Absolute poverty, before housing costs: children 24.7% 22.1% 23.3% 22.9% 23.1% 23.2% 23.5%
Absolute poverty, before housing costs: working-age adults 14.1% 13.8% 14.7% 14.7% 14.8% 14.8% 15.0%
Absolute poverty, before housing costs: pensioners 13.7% 13.4% 14.0% 13.8% 13.9% 13.9% 13.9%
Absolute poverty, after housing costs: everyone 21.6% 21.0% 22.0% 22.6% 22.9% 23.1% 23.4%
Absolute poverty, after housing costs: children 33.3% 31.3% 32.3% 32.5% 32.9% 33.4% 34.2%
Absolute poverty, after housing costs: working-age adults 19.9% 19.8% 20.8% 21.6% 21.9% 22.0% 22.3%
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 17.0% 16.0% 16.4% 16.3% 16.4% 16.3% 16.6%
Relative poverty, before housing costs: children 25.9% 22.9% 23.3% 22.9% 23.0% 22.9% 23.5%
Relative poverty, before housing costs: working-age adults 14.5% 14.1% 14.6% 14.7% 14.7% 14.7% 15.0%
Relative poverty, before housing costs: pensioners 14.5% 14.0% 13.9% 13.8% 13.8% 13.9% 13.9%
Relative poverty, after housing costs: everyone 21.4% 21.0% 21.3% 21.3% 21.3% 21.5% 21.7%
Relative poverty, after housing costs: children 33.1% 31.1% 31.2% 31.0% 31.1% 31.2% 31.8%
Relative poverty, after housing costs: working-age adults 19.9% 19.8% 20.2% 20.4% 20.5% 20.6% 20.8%
Relative poverty, after housing costs: pensioners 12.5% 12.4% 12.2% 11.7% 11.7% 11.8% 12.0%
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,278k 10,779k 11,402k 11,365k 11,476k 11,513k 11,651k
Absolute poverty, before housing costs: children 3,688k 3,314k 3,496k 3,445k 3,480k 3,502k 3,549k
Absolute poverty, before housing costs: working-age adults 5,965k 5,893k 6,308k 6,362k 6,420k 6,430k 6,521k
Absolute poverty, before housing costs: pensioners 1,624k 1,572k 1,598k 1,558k 1,576k 1,580k 1,581k
Absolute poverty, after housing costs: everyone 14,944k 14,600k 15,310k 15,721k 15,937k 16,141k 16,426k
Absolute poverty, after housing costs: children 4,975k 4,682k 4,837k 4,888k 4,945k 5,040k 5,179k
Absolute poverty, after housing costs: working-age adults 8,463k 8,461k 8,971k 9,344k 9,491k 9,584k 9,706k
Absolute poverty, after housing costs: pensioners 1,506k 1,457k 1,502k 1,490k 1,501k 1,517k 1,542k
Relative poverty, before housing costs: everyone 11,750k 11,084k 11,388k 11,364k 11,416k 11,415k 11,651k
Relative poverty, before housing costs: children 3,872k 3,423k 3,496k 3,445k 3,466k 3,454k 3,549k
Relative poverty, before housing costs: working-age adults 6,159k 6,013k 6,301k 6,362k 6,389k 6,389k 6,521k
Relative poverty, before housing costs: pensioners 1,720k 1,648k 1,590k 1,557k 1,562k 1,572k 1,581k
Relative poverty, after housing costs: everyone 14,849k 14,552k 14,776k 14,809k 14,874k 15,014k 15,234k
Relative poverty, after housing costs: children 4,942k 4,660k 4,673k 4,663k 4,679k 4,707k 4,806k
Relative poverty, after housing costs: working-age adults 8,426k 8,440k 8,711k 8,823k 8,873k 8,967k 9,062k
Relative poverty, after housing costs: pensioners 1,482k 1,452k 1,392k 1,323k 1,322k 1,339k 1,365k
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.3080 0.3052 0.3058 0.3058 0.3063 0.3069 0.3085
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,681 † £17,997 † £17,915 † £18,152 † £18,307 † £18,531 † £18,717 †
Decile 2: mean household_net_income per household £27,939 † £29,023 † £28,711 † £29,013 † £29,275 † £29,691 † £30,150 †
Decile 3: mean household_net_income per household £34,320 † £35,485 † £36,073 † £36,611 † £37,197 † £37,737 † £38,187 †
Decile 4: mean household_net_income per household £38,933 † £40,687 † £41,723 † £42,727 † £43,334 † £44,132 † £44,432 †
Decile 5: mean household_net_income per household £43,239 † £45,567 † £46,014 † £46,959 † £47,712 † £48,724 † £50,282 †
Decile 6: mean household_net_income per household £47,927 † £49,210 † £50,892 † £51,939 † £52,945 † £53,827 † £54,680 †
Decile 7: mean household_net_income per household £55,662 † £56,571 † £57,443 † £57,670 † £58,730 † £59,657 † £61,083 †
Decile 8: mean household_net_income per household £68,790 † £70,807 † £71,859 † £73,468 † £73,882 † £75,322 † £77,179 †
Decile 9: mean household_net_income per household £78,891 † £81,012 † £80,861 † £82,647 † £85,465 † £86,941 † £89,509 †
Decile 10: mean household_net_income per household £117,007 † £120,341 † £124,742 † £126,632 † £125,975 † £128,502 † £130,939 †
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.49bn
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.81bn £-3.92bn £-3.98bn £-4.04bn £-4.10bn £-4.23bn £-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.77bn £-21.32bn £-21.75bn £-21.99bn £-22.34bn £-22.75bn £-22.94bn
Raise Class 1 main employee NICs rate to 10% £10.89bn £11.30bn £11.71bn £12.01bn £12.31bn suppressed suppressed
Raise VAT standard rate by 2pp £24.89bn £25.71bn £26.63bn £27.24bn £27.87bn £28.50bn £29.14bn
Raise additional rate by 3pp £2.90bn £3.15bn £3.22bn £3.41bn £3.61bn suppressed suppressed
Raise the Universal Credit standard allowance by 10% £-3.06bn £-3.18bn £-3.26bn £-3.35bn £-3.44bn £-3.49bn £-3.50bn
Raise the State Pension by 10% £-7.78bn £-8.01bn £-8.24bn £-8.47bn £-8.82bn £-8.55bn £-8.89bn
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.13bn £-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.74bn £21.31bn £21.78bn £22.04bn £22.37bn £22.78bn £22.99bn
Raise Class 1 main employee NICs rate to 10%: change in national_insurance £11.15bn £11.63bn £12.00bn £12.29bn £12.61bn suppressed suppressed
Raise VAT standard rate by 2pp: change in vat £24.69bn £25.51bn £26.42bn £27.03bn £27.65bn £28.28bn £28.91bn
Raise additional rate by 3pp: change in income_tax £2.90bn £3.15bn £3.22bn £3.42bn £3.62bn suppressed suppressed
Raise the Universal Credit standard allowance by 10%: change in universal_credit £3.06bn £3.19bn £3.28bn £3.37bn £3.43bn £3.49bn £3.51bn
Raise the State Pension by 10%: change in state_pension £10.68bn £11.17bn £11.28bn £11.60bn £12.03bn £12.47bn £12.97bn

@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

@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.

Reviewed 6602ccce488f1302972331d61ba94f02528d08e7. COMPLETE — requesting changes for one repository-completion requirement.

C1 — Finish the statutory evidence in the filed Axiom handoff. rulespec-uk#524 is open and labelled pe-parity, but companion case 4 depends on the 22% property rate in Finance Act 2026 s.7 while its completion boundary explicitly says that provision has not been quoted. It supplies no operative corpus/companion cross-reference for that premise. The repository's docs/engineering/skills/github-prs.md requires a dispatch-ready queued issue with verbatim law and externally sourced companion tests.

Append the operative s.7 rate text and relevant s.6(4), (8) hierarchy/commencement evidence, or identify the existing supported corpus/companion reference. This can be fixed in the filed handoff without running the encoder. The recorded signed-run/corpus-ingestion blockers are not themselves the finding.

This concerns the queue's 22% enacted-law companion, separately from this PR's 24% announced Budget 2026 model case. I found no source-backed wrong-money defect in the reviewed allocation change.

All 143 selected checks passed, including 96 YAML cases, 46 repository Python checks and a final income-tax/household-net-income diagnostic. The changed allocation rules and 2027 commencement were independently checked against primary sources; 33 source records/extracts passed integrity validation. CI is green at the reviewed head. Once the filed source premise is completed, that evidence update can be reviewed without repeating unchanged code tests.

@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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants