You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Correct Scottish income tax limits and rates from 2018-19 #2783
On 20 February 2018 the Scottish Parliament set a 21% intermediate rate up to £43,430 of income and a 41% higher rate from there to £150,000, for 2018-19 (gov.scot, Official Report). With the £11,850 personal allowance, that puts the higher rate limit at £31,580 of taxable income.
policyengine_uk/parameters/gov/hmrc/income_tax/rates/scotland/rates.yaml holds £31,500 and 40% from April 2017 until April 2021. The file has no entry at any limit for 2019-20 or 2020-21, so those years repeat the 2018-19 limits.
Reproduction
On main (2da212a), a single adult in Scotland with £50,000 of employment income in 2018 has £38,150 of taxable income and pays £9,133.50 of income tax. The rates the Scottish Parliament set give £9,184.00:
Band
Taxable income
Rate
Tax
Starter
£2,000
19%
£380.00
Basic
£10,150
20%
£2,030.00
Intermediate
£19,430
21%
£4,080.30
Higher
£6,570
41%
£2,693.70
Total
£38,150
£9,184.00
Fix
Check every Scottish limit and rate for 2018-19, 2019-20, 2020-21 and 2021-22 against that year's resolution, correct the file, cite the resolution on each dated value, and add YAML cases worked from each resolution.
Fixes #2783
Reproducer: policyengine_uk/tests/test_scottish_income_tax_rates.py::test_scottish_rates_and_limits_match_each_resolution, policyengine_uk/tests/policy/baseline/finance/tax/income_tax/scottish_rate_resolutions.yaml
axiom: hub fills this line
## What and why
Each year the Scottish Parliament sets the Scottish rates and limits for one tax year by a Scottish rate resolution (Scotland Act 1998 s. 80C(1) and (3)). PolicyEngine applied different rates or limits in four tax years:
| Tax year | Motion | Value | Before | After |
|---|---|---|---|---|
| 2018-19 | S5M-10397 (c), (d) | Intermediate rate limit, where the higher rate starts | £31,500 | £31,580 |
| 2018-19 to 2020-21 | S5M-10397, S5M-15879, S5M-21090 (d) | Higher rate | 40% | 41% |
| 2019-20 | S5M-15879 (a)-(c) | Starter, basic and intermediate rate limits | £2,000, £12,150, £31,500 | £2,049, £12,444, £30,930 |
| 2020-21 | S5M-21090 (a)-(c) | Starter, basic and intermediate rate limits | £2,000, £12,150, £31,500 | £2,085, £12,658, £30,930 |
| 2021-22 | S5M-24225 (a), (b) | Starter and basic rate limits | £2,306, £13,991 | £2,097, £12,726 |
PolicyEngine applied 40% through 2020-21, reused the 2018-19 limits in 2019-20 and 2020-21, and applied the 2024-25 starter and basic rate limits (S6M-12252) in 2021-22. The 2017-18 (S5M-03912), 2022-23 (S6M-03019) and 2023-24 (S6M-07853) values already matched, and so did 2024-25 to 2026-27.
I cite Parliament's motion and paragraph beside each rate and limit from 2017-18 to 2026-27, and list every resolution from 2017-18 in the references. I left the personal allowance, the rest-of-UK rates, the uprating metadata, the announced freeze through 2028-29 and every formula as they were, and moved Scottish income tax's verified start year in `programs.yaml` from 2019 to 2017.
I merged #2350 (at `e5b16d14bbf9456945986565798902b720d222f3`) into this branch, and the PR opens after #2350 lands. Without #2350, the model with the correct limits denies the Marriage Allowance to Scottish couples the law gives it to. Over `e5b16d14b`, this branch changes five files: `rates.yaml`, `programs.yaml`, `changelog.d/2783.fixed.md`, `scottish_rate_resolutions.yaml` and `test_scottish_income_tax_rates.py`. It changes no formula.
### Limits of income after the allowance
In the 2017-18 to 2023-24 motions, Parliament charges each rate on "income" up to a limit. It sets those limits on income after the personal allowance: ITA 2007 s. 23 takes the allowance off at Step 3 and charges tax at each rate at Step 4. From 2024-25 Parliament says so in the motions, where s. 11A charges this income "above the personal allowance". I kept the limits in that form in `rates.yaml`, as before.
The Scottish Government's table for each year (gov.scot) adds the standard personal allowance to each limit, and its figures agree with the limits in every year from 2017-18 to 2026-27. s. 35(2) withdraws £1 of the allowance for every £2 of income above £100,000, so none remains at the £150,000 and £125,140 tops, and there gov.scot's figure equals the limit. For example:
| Tax year | Allowance | Resolution limits (income after the allowance) | gov.scot band tops (income) |
|---|---|---|---|
| 2018-19 | £11,850 | £2,000, £12,150, £31,580, £150,000 | £13,850, £24,000, £43,430, £150,000 |
| 2019-20 | £12,500 | £2,049, £12,444, £30,930, £150,000 | £14,549, £24,944, £43,430, £150,000 |
| 2020-21 | £12,500 | £2,085, £12,658, £30,930, £150,000 | £14,585, £25,158, £43,430, £150,000 |
| 2021-22 | £12,570 | £2,097, £12,726, £31,092, £150,000 | £14,667, £25,296, £43,662, £150,000 |
`test_scottish_limits_plus_personal_allowance_match_gov_scot_tables` checks this for every year against the model's personal allowance.
### Model years
`convert_to_fiscal_year_parameters` (`policyengine_uk/utils/parameters.py`) gives each year label the value in force on 30 April, so a value dated 6 April reaches that year's label: 2019-04-06 sets model year 2019 (2019-20). The tests read the model year. They also read the statutory tree on 6 April, on every date `rates.yaml` gives within the tax year and on the following 5 April, and check that no rate or limit takes effect before 6 April 2017.
### Readers
Ten files read this scale: `earned_income_tax`, `property_income_tax`, `tax_band`, `meets_marriage_allowance_income_conditions` (from #2350), `uc_income_tax` (`policyengine_uk/utils/uc_income_tax.py`), `uc_minimum_income_floor_income_tax`, `private_pension_contributions_tax`, `carers_allowance_earnings`, `esa_exempt_work_earnings` and `pension_credit_self_employment_deductions`. They read brackets by position, and `first_extended_scottish_threshold` (`policyengine_uk/utils/income_tax.py`) and `tax_band` tell the schedules apart by their number of limits. I kept the order and number of brackets in every year (three in 2017-18, five from 2018-19 to 2023-24, six from 2024-25), so each reader still finds each band where it was. On `main`, `tax_band` puts a Scottish taxpayer in the band above once their taxable income, savings included, reaches a Scottish limit (`income < threshold`), and the Marriage Allowance test reads that band. #2350 replaces both.
### What moves
I compared this branch at `9f2863f4573735df82847c341db8d590d510d448` with #2350 at `e5b16d14bbf9456945986565798902b720d222f3` for 2015 to 2030, using 930 hand-built units. I built 465 single adults and 465 married couples with a spouse earning £5,000, across Scotland, the North East and Wales. Each adult earns one of 31 pay amounts from £0 to £160,000, with pay alone or with £5,000 of dividends, £5,000 or £29,500 of savings, or £10,000 of property income.
- `income_tax` and `household_net_income` move only in Scotland, and only in 2018 (138 of 930 units), 2019 (251), 2020 (252) and 2021 (243). Nothing moves in 2015-2017 or 2022-2030, or outside Scotland.
- On pay alone, for a single adult:
| Pay | 2018-19 | 2019-20 | 2020-21 | 2021-22 |
|---|---|---|---|---|
| £20,000 | 0 | −£0.49 | −£0.85 | +£2.09 |
| £35,000 | 0 | −£3.43 | −£5.93 | +£14.74 |
| £43,430 | −£15.20 | −£3.43 | −£5.93 | +£14.74 |
| £50,000 | +£50.50 | +£170.57 | +£168.07 | +£14.74 |
| £100,000 | +£550.50 | +£670.57 | +£668.07 | +£14.74 |
| £150,000 and £160,000 | +£1,169.00 | +£1,295.57 | +£1,293.07 | +£14.74 |
- Parliament charges a Scottish taxpayer's savings and dividends at the rest-of-UK rates (ITA 2007 ss. 11D(6) and 13(5)), and `savings_income_tax` and `dividend_income_tax` move in no unit, single or couple, in any year. `earned_income_tax` moves (130, 240, 240 and 230 units in 2018 to 2021), and so does `property_income_tax` (30, 46, 50 and 22), since property income bears the Scottish rates. In 2018-19 a single adult with £45,000 of pay pays £0.50 more on the pay (£7,133.50 to £7,134.00), and £5,000 of dividends or savings on top still costs £975 or £1,630. With £10,000 of property income instead, the tax on the property income rises from £4,000 to £4,100, and their income tax by £100.50, from £11,133.50 to £11,234.00.
- `tax_band` moves for Scottish taxpayers between an old and a new limit: 2 units in 2018, 30 in 2019, 32 in 2020 and 8 in 2021.
- The Marriage Allowance changes in 19 couple-year observations across 10 of the couples. In 2018-19 the earner with £43,430 of pay (£31,580 after the allowance) gains £1,190: their last pounds now stay at the intermediate rate. In 2019-20 and 2020-21, the same nine earners in each year, with £30,931 to £31,500 after the allowance (pay of £43,431, £43,662 or £44,000, alone or with £5,000 of dividends or savings), lose £1,250: their last pounds now bear the 41% higher rate, which s. 55B(2)(b) doesn't list.
I used no survey data, so the run gives no aggregate.
### Marriage Allowance and #2350
#2350 replaces `main`'s band test. Its `tax_band` bands a Scottish taxpayer's non-savings income on the Scottish limits and their savings and dividends on the rest-of-UK limits, and keeps income at a limit in the band below. Its Marriage Allowance test no longer reads `tax_band`: it applies the rate conditions of ITA 2007 s. 55B(2)(b) and (ba) to the person's income directly. Without it, the model with the correct limits denies the allowance to couples the law gives it to. On this PR's own commits before the merge (`eff166f46`), a 2019-20 Scottish earner with £43,430 of pay (£30,930 after the allowance, exactly the intermediate rate limit) and a spouse on £5,000 lands in the higher band and gets no allowance. So does an earner with £14,000 of pay and £29,500 of savings, whose savings stay inside the £37,500 basic rate limit.
The couple cases in `scottish_rate_resolutions.yaml` cover 2018-19, 2019-20 and 2020-21. An earner at the limit, and one with pay and savings, can receive the allowance, and one £1 above the limit cannot. Twin couples who make no election pay tax at the rates Parliament set. At `eff166f46` all five claim cases at the limit or with savings fail.
The model takes the transferred amount off the earner's income (#1947). Parliament instead gives the earner a tax reduction of 20% of it, at Step 6 of s. 23 (s. 55A(2), s. 55B(1) and (3)). So a claiming couple's income tax on this branch differs from the law's. The model column comes from the differential at `9f2863f45`, where each spouse pays no tax and the earner receives the transfer shown:
| Tax year | Earner's income | Law | This branch | Difference |
|---|---|---|---|---|
| 2018-19 | £43,430 of pay | £6,252.30 | £6,240.40 | −£11.90 |
| 2019-20 | £43,430 of pay | £6,100.37 | £6,087.87 | −£12.50 |
| 2019-20 | £14,000 of pay, £29,500 of savings | £5,035.00 | £4,797.50 | −£237.50 |
| 2020-21 | £43,430 of pay | £6,097.87 | £6,085.37 | −£12.50 |
| 2020-21 | £14,000 of pay, £29,500 of savings | £5,035.00 | £4,797.50 | −£237.50 |
The tests assert eligibility, the amount transferred and the tax before the reduction, and none of these totals.
## Invariants
For every input:
1. PolicyEngine gives each model year from 2017 to 2026 the rates and limits of that year's resolution, in the same order.
2. The statutory tree gives each resolution's rates and limits on every day from 6 April to the following 5 April (s. 80C(3)(b), ITA 2007 s. 4(3)), and dates no Scottish rate or limit before 6 April 2017.
3. Each limit plus the year's standard personal allowance equals gov.scot's figure where the allowance applies in full; above £100,000 plus twice the allowance, the limit equals gov.scot's figure.
4. A Scottish taxpayer's income tax on pay alone equals the resolution's bands applied to pay less the personal allowance (s. 35), in every year from 2017-18 to 2026-27.
5. A Scottish taxpayer's income tax on pay alone never falls as pay rises.
6. No taxpayer outside Scotland, and no year outside 2018-19 to 2021-22, moves.
`test_scottish_income_tax_rates.py` tests 1 to 3 on every year, and 4 and 5 with Hypothesis on hand-built Scottish taxpayers: 4 examples of 48 people with any whole-pound pay up to £320,000, each read in all ten years. The differential above checks 6.
## Test plan
- [x] For policy changes: YAML tests whose expected values come from an external source: `scottish_rate_resolutions.yaml`, 41 cases worked by hand. 29 cover pay alone, from S5M-10397, S5M-15879, S5M-21090 and S5M-24225 with each year's personal allowance, among them the issue's £50,000 of pay in 2018-19 (£9,184.00; `main` gives £9,133.50). 12 cover married couples, from those motions and ITA 2007 ss. 10, 12, 12A, 12B, 35 and 55B.
- [x] `ruff format --check .`, `ruff check .` and `ruff check --select F811,F821 policyengine_uk` (0.17.2) pass at `9f2863f45`; `git diff --check` reports nothing.
- [x] Changelog fragment `changelog.d/2783.fixed.md`.
I ran these locally without survey data, at `9f2863f4573735df82847c341db8d590d510d448`:
| Tests | Result |
|---|---|
| `scottish_rate_resolutions.yaml` | 41 passed |
| `test_scottish_income_tax_rates.py`, with its Hypothesis test | 40 passed |
| #2350's `scottish_rates.yaml` | 36 passed |
| `meets_marriage_allowance_income_conditions.yaml` | 26 passed |
| `policyengine_uk/tests/policy/reforms/cps/marriage_tax_reforms.yaml` | 10 passed |
| #2350's `test_tax_band_scottish_properties.py` (2017 to 2026) | 5 passed |
| `test_uc_minimum_income_floor_properties.py::test_notional_deductions_equal_tax_on_threshold_as_only_income`, alone | 1 passed in 222 s (231 s wall, including start-up) |
| `test_uc_minimum_income_floor_properties.py::test_more_earnings_never_lower_earned_income_or_raise_uc`, alone | 1 passed in 86 s (93 s wall) |
| `policyengine_uk/tests/code_health` | 3,086 passed |
The file's other four UC properties (`test_floor_never_lowers_earned_income`, `test_floor_holds_for_singles_and_couples`, `test_top_up_never_takes_couple_above_couple_threshold`, `test_matches_closed_form_of_regulation_62`) passed at `d07335b96`, before #2350 moved; I didn't rerun them. CI runs the file.
On #2350 alone (`e5b16d14b`, with this branch's two test files copied in), `scottish_rate_resolutions.yaml` gives 28 failures and 13 passes, and `test_scottish_income_tax_rates.py`, without its Hypothesis test, gives 12 failures (the 2018 to 2021 rows of the table, whole-year and gov.scot tests) and 27 passes. The cases that pass sit where the two schedules agree, such as £1,000 of taxable income at 19%, or where the old limits give the same answer, such as the couples with pay of £14,000 and savings of £29,500.
I ran these at `eff166f46`, before merging #2350:
| Tests | Result |
|---|---|
| `tests/policy/baseline/finance/tax/income_tax/` | 58 passed |
| `tests/policy/baseline/gov/hmrc/income_tax/` | 472 passed, in two runs (the first stopped at its 25-minute limit inside `gift_aid.yaml`, which the second ran whole) |
| The 13 YAML files outside the income tax folders that mention Scotland and a year from 2018 to 2021 (Universal Credit unearned income and minimum income floor, early years entitlement, Scottish Child Payment, Carer Support Payment, council tax reduction, energy bills rebate, LBTT, LTT, SDLT, Housing Benefit, household) | 151 passed |
| `test_pension_annual_allowance_properties.py::test_charge_matches_band_by_band_reference` (2020 among its years) | 1 passed |
| `test_pension_credit_earnings_disregard_properties.py::test_scottish_basic_rate_band_is_twenty_percent_in_every_year` | 1 passed |
No existing test's expected value changes.
I checked that the new tests reject scratch mutations of `rates.yaml`, none of them committed. At `9f2863f45`, a harness copies the tree, applies each mutation, and runs the table, whole-year, gov.scot and first-date tests (34 tests, all passing on the unmutated copy). Every one of 64 mutations fails:
- each of the 41 finite limits raised by £1;
- each year's 6 April values moved to 1 April, for 2017-18 to 2022-23;
- a £2,050 starter rate limit for July 2019 only;
- each of the 11 dated rates raised by one point;
- a higher rate of 42% for July 2019 only;
- one-day basic rate limit changes on 7 April 2019, 5 April 2020 and 5 April 2027;
- the 2017-18 values copied to 6 April 2016.
The July 2019 limit and rate fail `test_each_resolution_holds_for_its_whole_tax_year[2019]`, the 1 April 2017 start and the 2016 copy fail `test_scottish_rates_take_effect_on_6_april_2017`, and the 1 April moves for 2018-19 to 2022-23 fail the previous year's whole-year test. At `eff166f46` four more mutations each fail both the table tests and the YAML cases: the higher rate back at 40% until 2021-22 fails 6 table tests and 6 YAML cases; the 2018-19 limit kept for 2019-20 fails 3 and 2; `main`'s 2021-22 basic rate limit fails 3 and 3; and the 2019-20 limits dated 1 May fail 3 and 7. The Hypothesis test catches the third change too, failing in 2021.
## Merge notes
`git merge-tree --write-tree --name-only` from `9f2863f4573735df82847c341db8d590d510d448`, in both orders:
| Other head | Result |
|---|---|
| `main` at `6c6e229a51fbd804dab2062dac1f94af04c609c2` | Clean, same tree in both orders. |
| #2670 at `3f67112c834ae8b26c42357fe7e7854657bd2a94` | Clean, same tree in both orders. |
| #2106's `claude/fix-2106-allowance-order-979j96` at `def18d3a3043d1b65e74de2a69b169d4f0a550db` | Conflict in `policyengine_uk/variables/gov/hmrc/income_tax/bracketized_earned_income/non_savings_income_after_allowances.py`. |
| #1963 at `69c919f6054023fa5a51b42d1ffbbcb0078e8082` | Conflicts in seven files under `policyengine_uk/`: `parameters/gov/hmrc/income_tax/income_tax_subtractions.yaml`, `reforms/cps/marriage_tax_reforms.py`, `tests/policy/baseline/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.yaml`, `tests/test_marriage_allowance_cps_switch.py`, `variables/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.py`, `variables/gov/hmrc/income_tax/allowances/uses_property_allowance.py` and `variables/gov/hmrc/income_tax/reliefs/property_finance_cost_relief.py`. |
#2350's head `e5b16d14b` alone gives the same conflicting files against #2106 and #1963, so they come from #2350, not from this branch's five files.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Title: Encode the Scottish income tax rates and limits for 2017-18 to 2023-24 from each year's Scottish rate resolution
Labels: pe-parity
PolicyEngine UK corrects its Scottish income tax rates and limits for 2018-19 to 2021-22 in PolicyEngine/policyengine-uk#2783 (PR: <PE PR>; branch head `9f2863f4573735df82847c341db8d590d510d448`), and checks every year from 2017-18 to 2026-27 against that year's Scottish rate resolution. rulespec-uk does not yet hold any of the years before 2026-27.
On `main` (`0497350`), only `uk/statutes/income_tax/individual/scottish_income_tax_oracle_pipeline.yaml` carries Scottish rates. Its six rates and five limits each have one version, `effective_from: '2026-01-01'`, sourced from the 2026-27 resolution (`uk/guidance/scottish-government/scottish-rate-resolution-2026-27/draft-motion/block-1`). No module encodes the resolutions for 2017-18 to 2025-26.
## Blocker
The pinned corpus holds no Scottish rate resolution before 2026-27, so the signed encoder cannot cite any year this issue asks for. A maintainer also approves each billed run of the encoder, and nobody writes RuleSpec by hand.
We checked over plain HTTP:
- rulespec-uk `main` (`0497350d74f9d0f7272b88e08019e66366d3be58`) pins, in `.axiom/toolchain.toml`, release `uk-rulespec-2026-10-25-pe-child-definitions` with content sha256 `76b42156de23f2c000dab657dafacfb5b96ec9fe5670a75563574bd00793048c`.
- That release's manifest, `manifests/releases/uk-rulespec-2026-10-25-pe-child-definitions.json` in axiom-corpus at `9d174d88119fd6e6ace02d7c94eec7bb47e363f0`, lists 35 UK scopes. We did not retrieve the signed release object itself, so we have not checked its content against the pinned sha256.
- Those scopes' corpus locks name 35 UK provision files. We fetched the 33 that carry a git blob, and each matched the sha256 its lock records. The other two (`statute` and `regulation` `2026-09-28-uk-pe-child-definitions`) have no git blob in their lock, so we could not fetch them; their scopes hold definitions of a child and a couple.
- The 33 files hold rows for ITA 2007 ss. 6, 10, 11A, 23 and 35, Scotland Act 1998 s. 80C, and one Scottish rate resolution, `uk/guidance/scottish-government/scottish-rate-resolution-2026-27/draft-motion/block-1`. They hold no row for any resolution from 2017-18 to 2025-26.
- axiom-corpus's ingest manifest `manifests/uk-scottish-rate-resolution-official-documents.yaml` lists one document, the 2026-27 draft motion. The pinned release leaves out the `2026-07-05-uk-scottish-income-tax` scope, which ingested it.
Axiom can unblock this by ingesting the seven motions below (parliament.scot motion pages, or gov.scot's draft motions for the same years), pinning a release that holds them and approving an encoder run.
## Provisions
| Proposed module path | Corpus citation (for Axiom to ingest) | What it adds |
|---|---|---|
| `uk/statutes/income_tax/individual/scottish_income_tax_oracle_pipeline.yaml` (existing) | One row per resolution, for example `uk/guidance/scottish-parliament/scottish-rate-resolution-2018-19/motion/S5M-10397`, with `uk/statute/ukpga/1998/46/80C` and `uk/statute/ukpga/2007/3/11A` | A dated version of each Scottish rate and limit for each tax year from 2017-18 to 2023-24, and the three-rate schedule of 2017-18 |
We propose these paths and citation paths as targets; the corpus sets the citation paths and the encoder binds the source expressions. rulespec-uk lacks 2024-25 (S6M-12252) and 2025-26 (S6M-16531) too; the same ingest can carry them.
### The law
Scotland Act 1998 s. 80C(1): "The Scottish Parliament may by resolution (a “Scottish rate resolution”) set the Scottish basic rate, and any other rates, for the purposes of section 11A of the Income Tax Act 2007". (3): "A Scottish rate resolution applies— (a) for only one tax year, and (b) for the whole of that year." ITA 2007 s. 4(3): "A tax year begins on 6 April and ends on the following 5 April."
The Scottish Parliament agreed each motion below on the date shown (parliament.scot, Votes and motions, "Result ... Agreed"). Quoted from https://www.parliament.scot/chamber-and-committees/votes-and-motions/<motion>:
- 2017-18, S5M-03912, agreed 21 February 2017: "That the Parliament agrees that, for the purposes of section 11A of the Income Tax Act 2007 (which provides for income tax to be charged at Scottish rates on certain non-savings and non-dividend income of a Scottish taxpayer), the Scottish rates and limits for the tax year 2017-18 are as follows— (a) the Scottish basic rate is 20%, charged on income up to a Scottish basic rate limit of £31,500, (b) a Scottish higher rate of 40%, charged on income above that Scottish basic rate limit and up to a Scottish higher rate limit of £150,000, and (c) a Scottish additional rate of 45%, charged on income above that Scottish higher rate limit."
- 2018-19, S5M-10397, agreed 20 February 2018: "... the Scottish rates and limits for the tax year 2018-19 are as follows— (a) a starter rate of 19%, charged on income up to a limit of £2,000, (b) the Scottish basic rate is 20%, charged on income above £2,000 and up to a limit of £12,150, (c) an intermediate rate of 21%, charged on income above £12,150 and up to a limit of £31,580, (d) a higher rate of 41%, charged on income above £31,580 and up to a limit of £150,000, and (e) a top rate of 46%, charged on income above £150,000."
- 2019-20, S5M-15879, agreed 19 February 2019: "... the Scottish rates and limits for the tax year 2019-20 are as follows— (a) a starter rate of 19%, charged on income up to a limit of £2,049, (b) the Scottish basic rate is 20%, charged on income above £2,049 and up to a limit of £12,444, (c) an intermediate rate of 21%, charged on income above £12,444 and up to a limit of £30,930, (d) a higher rate of 41%, charged on income above £30,930 and up to a limit of £150,000, and (e) a top rate of 46%, charged on income above £150,000."
- 2020-21, S5M-21090, agreed 4 March 2020: "... the Scottish rates and limits for the tax year 2020-21 are as follows— (a) a starter rate of 19%, charged on income up to a limit of £2,085, (b) the Scottish basic rate is 20%, charged on income above £2,085 and up to a limit of £12,658, (c) an intermediate rate of 21%, charged on income above £12,658 and up to a limit of £30,930, (d) a higher rate of 41%, charged on income above £30,930 and up to a limit of £150,000, and (e) a top rate of 46%, charged on income above £150,000."
- 2021-22, S5M-24225, agreed 25 February 2021: "... the Scottish rates and limits for the tax year 2021-22 are as follows— (a) a starter rate of 19%, charged on income up to a limit of £2,097, (b) the Scottish basic rate is 20%, charged on income above £2,097 and up to a limit of £12,726, (c) an intermediate rate of 21%, charged on income above £12,726 and up to a limit of £31,092, (d) a higher rate of 41%, charged on income above £31,092 and up to a limit of £150,000, and (e) a top rate of 46%, charged on income above £150,000."
- 2022-23, S6M-03019, agreed 2 February 2022: "... the Scottish rates and limits for the tax year 2022-23 are as follows— (a) a starter rate of 19%, charged on income up to a limit of £2,162, (b) the Scottish basic rate is 20%, charged on income above £2,162 and up to a limit of £13,118, (c) an intermediate rate of 21%, charged on income above £13,118 and up to a limit of £31,092, (d) a higher rate of 41%, charged on income above £31,092 and up to a limit of £150,000, and (e) a top rate of 46%, charged on income above £150,000."
- 2023-24, S6M-07853, agreed 9 February 2023: "That the Parliament agrees that, for the purposes of section 11A of the Income Tax Act 2007 (which provides for Income Tax to be charged at Scottish rates on certain non-savings and non-dividend income of a Scottish taxpayer), the Scottish rates and limits for the tax year 2023-24 are as follows— (a) a starter rate of 19%, charged on income up to a limit of £2,162, (b) the Scottish basic rate is 20%, charged on income above £2,162 and up to a limit of £13,118, (c) an intermediate rate of 21%, charged on income above £13,118 and up to a limit of £31,092, (d) a higher rate of 42%, charged on income above £31,092 and up to a limit of £125,140, and (e) a top rate of 47%, charged on income above £125,140."
The 2018-19 to 2022-23 motions open with the same words as the 2017-18 one, which we shorten here to "...".
In these seven motions Parliament charges each rate on "income" up to a limit. ITA 2007 s. 23 deducts the personal allowance at Step 3 and charges tax at each rate at Step 4, so the limits apply to income after the allowance. From 2024-25 Parliament says so in the motions' opening words: S6M-12252 describes s. 11A as charging certain income of a Scottish taxpayer "to be charged above the personal allowance". gov.scot's table for each year adds the standard personal allowance to each limit; for 2018-19, for example, "Over £11,850 to £13,850", "Over £13,850 to £24,000", "Over £24,000 to £43,430", "Over £43,430 to £150,000" and "Over £150,000", with an £11,850 allowance (https://www.gov.scot/publications/scottish-income-tax-2018-2019/). The £150,000 and £125,140 tops carry no allowance: s. 35(2) withdraws £1 of it for every £2 of income above £100,000, so none remains there, and gov.scot's figure equals the limit.
### Required outputs
For each tax year from 2017-18 to 2023-24, dated from 6 April of that year (s. 80C(3), ITA 2007 s. 4(3)):
- the rates: `uk_scotpit_starter_rate`, `uk_scotpit_basic_rate`, `uk_scotpit_intermediate_rate`, `uk_scotpit_higher_rate` and the top rate (the "Scottish additional rate" in 2017-18), with no starter or intermediate rate in 2017-18;
- the limits of income at which each rate stops;
- the income tax a Scottish taxpayer pays on non-savings, non-dividend income, from those bands applied to income less the personal allowance.
| Tax year | Motion | Starter 19% to | Basic 20% to | Intermediate 21% to | Higher rate and limit | Top rate |
|---|---|---|---|---|---|---|
| 2017-18 | S5M-03912 | none | £31,500 | none | 40% to £150,000 | 45% |
| 2018-19 | S5M-10397 | £2,000 | £12,150 | £31,580 | 41% to £150,000 | 46% |
| 2019-20 | S5M-15879 | £2,049 | £12,444 | £30,930 | 41% to £150,000 | 46% |
| 2020-21 | S5M-21090 | £2,085 | £12,658 | £30,930 | 41% to £150,000 | 46% |
| 2021-22 | S5M-24225 | £2,097 | £12,726 | £31,092 | 41% to £150,000 | 46% |
| 2022-23 | S6M-03019 | £2,162 | £13,118 | £31,092 | 41% to £150,000 | 46% |
| 2023-24 | S6M-07853 | £2,162 | £13,118 | £31,092 | 42% to £125,140 | 47% |
### review_finding (paste as-is)
> Encode the Scottish rate resolutions for 2017-18 to 2023-24 (corpus uk/guidance/scottish-parliament/scottish-rate-resolution-<year>/motion/<motion>, once ingested: S5M-03912, S5M-10397, S5M-15879, S5M-21090, S5M-24225, S6M-03019, S6M-07853) as dated versions in the existing module uk/statutes/income_tax/individual/scottish_income_tax_oracle_pipeline.yaml. That module carries one version of each rate and limit, effective_from 2026-01-01, sourced from the 2026-27 resolution, and none for an earlier year. The Scottish Parliament adopts each resolution for one whole tax year under Scotland Act 1998 s. 80C(3). Each tax year runs from 6 April to the following 5 April (ITA 2007 s. 4(3)); date each year's version from 6 April and keep the existing 2026-27 version. Entity Person. Parliament charges each rate on "income" up to a limit; ITA 2007 s. 23 deducts the personal allowance at Step 3 before it charges the rates at Step 4, so apply the limits to income less the personal allowance. Income exactly at a limit bears the lower rate ("up to a limit of"); income above it bears the next. Do not hand-write RuleSpec.
>
> Required outputs, each with verbatim proof excerpts from the corpus provision:
>
> 1. Rates (Rate): for 2018-19 to 2023-24, the starter, Scottish basic, intermediate, higher and top rates in paragraphs (a) to (e); for 2017-18, the Scottish basic rate of 20%, higher rate of 40% and additional rate of 45% in paragraphs (a) to (c), with no starter or intermediate rate.
> 2. Limits (Money): for each year, the limit of income at which each rate stops, as the table in this issue gives it, from the same paragraphs.
> 3. Scottish income tax on non-savings, non-dividend income (Money): the bands applied to that income less the personal allowance. The pipeline's `uk_scotpit_personal_allowance_base_amount` has one version, from 2026-01-01. Use dated allowances of £11,500 (2017-18), £11,850 (2018-19), £12,500 (2019-20 and 2020-21) and £12,570 (2021-22 to 2023-24), with the s. 35(2) and (3) taper. Ingest ITA 2007 s. 35(1) for those years.
### Companion tests
Each case has one Scottish taxpayer aged 40 with pay only. We worked each value by hand from the motions above, with each year's standard personal allowance: £11,500 in 2017-18 (gov.scot's 2017-18 table, https://www.gov.scot/news/income-tax-rates-set/), and from ITA 2007 s. 35(1) as at 6 April of each later year, £11,850 in 2018-19, £12,500 in 2019-20 and 2020-21 and £12,570 from 2021-22. s. 35(2) withdraws £1 of the allowance for every £2 of income above £100,000, and s. 35(3) rounds what remains up to a whole pound, so none remains at £130,000 and above. The 2018-19 to 2021-22 cases match the pay cases in policyengine-uk's `policyengine_uk/tests/policy/baseline/finance/tax/income_tax/scottish_rate_resolutions.yaml`; `policyengine_uk/tests/test_scottish_income_tax_rates.py` checks the other years against the same motions. Both files pass at `9f2863f4573735df82847c341db8d590d510d448`.
| Tax year | Pay | Taxable income | Income tax | Working |
|---|---|---|---|---|
| 2017-18 | £43,000 | £31,500 | £6,300.00 | 31,500 x 20% |
| 2017-18 | £43,001 | £31,501 | £6,300.40 | 6,300 + 1 x 40% |
| 2017-18 | £50,000 | £38,500 | £9,100.00 | 6,300 + 7,000 x 40% |
| 2017-18 | £160,000 | £160,000 | £58,200.00 | 6,300 + 118,500 x 40% + 10,000 x 45% |
| 2018-19 | £13,000 | £1,150 | £218.50 | 1,150 x 19% |
| 2018-19 | £20,000 | £8,150 | £1,610.00 | 380 + 6,150 x 20% |
| 2018-19 | £30,000 | £18,150 | £3,670.00 | 380 + 2,030 + 6,000 x 21% |
| 2018-19 | £43,430 | £31,580 | £6,490.30 | 380 + 2,030 + 19,430 x 21% |
| 2018-19 | £50,000 | £38,150 | £9,184.00 | 6,490.30 + 6,570 x 41% |
| 2018-19 | £160,000 | £160,000 | £59,642.50 | 6,490.30 + 118,420 x 41% + 10,000 x 46% |
| 2019-20 | £13,500 | £1,000 | £190.00 | 1,000 x 19% |
| 2019-20 | £14,549 | £2,049 | £389.31 | 2,049 x 19% |
| 2019-20 | £20,000 | £7,500 | £1,479.51 | 389.31 + 5,451 x 20% |
| 2019-20 | £24,944 | £12,444 | £2,468.31 | 389.31 + 10,395 x 20% |
| 2019-20 | £35,000 | £22,500 | £4,580.07 | 2,468.31 + 10,056 x 21% |
| 2019-20 | £43,430 | £30,930 | £6,350.37 | 2,468.31 + 18,486 x 21% |
| 2019-20 | £50,000 | £37,500 | £9,044.07 | 6,350.37 + 6,570 x 41% |
| 2019-20 | £160,000 | £160,000 | £59,769.07 | 6,350.37 + 119,070 x 41% + 10,000 x 46% |
| 2020-21 | £13,500 | £1,000 | £190.00 | 1,000 x 19% |
| 2020-21 | £14,585 | £2,085 | £396.15 | 2,085 x 19% |
| 2020-21 | £20,000 | £7,500 | £1,479.15 | 396.15 + 5,415 x 20% |
| 2020-21 | £25,158 | £12,658 | £2,510.75 | 396.15 + 10,573 x 20% |
| 2020-21 | £35,000 | £22,500 | £4,577.57 | 2,510.75 + 9,842 x 21% |
| 2020-21 | £43,430 | £30,930 | £6,347.87 | 2,510.75 + 18,272 x 21% |
| 2020-21 | £50,000 | £37,500 | £9,041.57 | 6,347.87 + 6,570 x 41% |
| 2020-21 | £160,000 | £160,000 | £59,766.57 | 6,347.87 + 119,070 x 41% + 10,000 x 46% |
| 2021-22 | £13,570 | £1,000 | £190.00 | 1,000 x 19% |
| 2021-22 | £14,667 | £2,097 | £398.43 | 2,097 x 19% |
| 2021-22 | £20,000 | £7,430 | £1,465.03 | 398.43 + 5,333 x 20% |
| 2021-22 | £25,296 | £12,726 | £2,524.23 | 398.43 + 10,629 x 20% |
| 2021-22 | £35,000 | £22,430 | £4,562.07 | 2,524.23 + 9,704 x 21% |
| 2021-22 | £50,000 | £37,430 | £8,979.67 | 2,524.23 + 18,366 x 21% + 6,338 x 41% |
| 2021-22 | £160,000 | £160,000 | £59,733.37 | 6,381.09 + 118,908 x 41% + 10,000 x 46% |
| 2022-23 | £14,732 | £2,162 | £410.78 | 2,162 x 19% |
| 2022-23 | £25,688 | £13,118 | £2,601.98 | 410.78 + 10,956 x 20% |
| 2022-23 | £43,662 | £31,092 | £6,376.52 | 2,601.98 + 17,974 x 21% |
| 2022-23 | £50,000 | £37,430 | £8,975.10 | 6,376.52 + 6,338 x 41% |
| 2023-24 | £43,662 | £31,092 | £6,376.52 | 2,601.98 + 17,974 x 21% |
| 2023-24 | £50,000 | £37,430 | £9,038.48 | 6,376.52 + 6,338 x 42% |
| 2023-24 | £130,000 | £130,000 | £48,160.88 | 6,376.52 + 94,048 x 42% + 4,860 x 47% |
| 2023-24 | £150,000 | £150,000 | £57,560.88 | 6,376.52 + 94,048 x 42% + 24,860 x 47% |
The UK Parliament sets the personal allowance, a reserved matter, outside these resolutions.
Head: 9f2863f4573735df82847c341db8d590d510d448 on claude/restack-2783-scottish-rates-r0nfbo (the harness pins this session's pushes to that branch). No PR opened. I didn't push to fix-2783-scottish-rates (still 9aa5ee4c9) or fix-2350-scottish-tax-band (still e5b16d14b).
Before starting I found no comments on #2783 and no reviews or review comments on #2828. fix-2350-scottish-tax-band got a commit about 2 h 45 min before I started (e5b16d14b, 16:17 UTC). That is the head this brief names, so I went ahead rather than stopping. Nothing has moved on either branch since.
9f2863f4573735df82847c341db8d590d510d448: the review's comment and docstring edits only (3 files, +25 −20).
Findings and resolutions
Finding
Resolution
P3: Axiom placeholder
The body's line is now axiom: hub fills this line.
P3: couple-years
The body says "The Marriage Allowance changes in 19 couple-year observations across 10 of the couples." I also added "the same nine earners in each year", which the fresh run confirms: rows 545–547, 550–552 and 555–557 in both 2019 and 2020, plus row 540 in 2018.
P3: YAML header top-limit exception
scottish_rate_resolutions.yaml:11–13 uses the review's text.
Voice: PR_BODY.md:42
I dropped the "Position alone…" lead-in, so the paragraph now ends with the review's sentence. I checked it against main's tax_band (income < threshold on net income less allowances) and main's Marriage Allowance test (it reads tax_band), so the sentence stays true.
Voice: PR_BODY.md:46
I used the review's text with one change: it compares with #2350 at e5b16d14b, not 3af8cb785, and names this head. The old base is stale.
Voice: PR_BODY.md:70
I used the review's text.
Voice: rates.yaml:11–15, :71, :92, :106–107
I used the review's text at rates.yaml:11–15, :71–72, :93–94 and :108–110.
Voice: Python test :13–17, :129–131, :150
I used the review's text at test_scottish_income_tax_rates.py:13–17, :129–132 and the docstring at :151.
Voice: AXIOM_ISSUE.md:69, :75
I used the review's text. For :75 I kept the opening clause saying the pipeline's allowance has one version from 2026-01-01 as its own sentence, because it explains why dated allowances are needed.
Other edits I made because the merged tree made the old text false or stale:
Pins. Every pinned head and result now names 9f2863f45 or, for older evidence, the commit it ran at (eff166f46, d07335b96).
Body order.Reproducer: and axiom: now come right after Fixes #2783, following the brief's body rules. The hub's draft had them at the end.
New "Merge notes" section.
Differential component counts. The body now gives the earned_income_tax and property_income_tax counts.
Differential
I ran the reviewer's differential.py, verify_differential.py and differential_static_counts.py with only the paths changed. The base was e5b16d14bbf9456945986565798902b720d222f3 and the head 9f2863f4573735df82847c341db8d590d510d448. Each run printed its imported policyengine_uk/__init__.py, which confirms the scratch worktree for each tree. Both runs covered 930 units over 2015–2030.
verify_differential.py printed PASS. Every assertion holds unchanged against the new base:
income_tax, unit_income_tax and household_net_income moved in 138/251/252/243 units in 2018–2021.
earned_income_tax moved in 130/240/240/230 units and property_income_tax in 30/46/50/22.
Savings and dividend tax are identical in every unit and year.
tax_band moved in 2/30/32/8 units.
The Marriage Allowance moved in 1/9/9 units: 19 couple-years across 10 couples.
Every result is exactly equal outside Scotland and outside 2018–2021.
The policyengine-core CLI collected the YAML files directly, with no bridge needed.
Revert and mutation checks
Revert. I took e5b16d14b's tree and copied in this branch's two test files, then confirmed that the copied tree differs from the head only in rates.yaml and programs.yaml.
test_scottish_income_tax_rates.py without Hypothesis gives 12 failed / 27 passed: the 2018–2021 rows of the table, whole-year and gov.scot tests.
My first YAML attempt passed 41 because the console script imported the editable install rather than the copied tree. I discarded that run and reran with PYTHONPATH pointing at the copy, which __file__ confirms.
Mutations. A new harness copies the head tree and applies each of 64 scratch mutations to rates.yaml, matching the list in the body. For each one it runs the table, whole-year, first-date and gov.scot tests (34 tests). The unmutated control passes 34 of 34, and all 64 mutations fail.
The July 2019 limit and rate mutations fail test_each_resolution_holds_for_its_whole_tax_year[2019].
The 1 April 2017 start and the 2016 copy fail test_scottish_rates_take_effect_on_6_april_2017.
The 1 April moves for 2018–2022 fail the previous year's whole-year test.
The one-day mutations fail the whole-year test for 2019, 2019 and 2026 respectively.
The harness ran with -x, so it records only the first failing test for each mutation.
Merge-tree
I ran git merge-tree --write-tree --name-only from 9f2863f4573735df82847c341db8d590d510d448 in both orders.
Verdict: APPROVE WITH NITS at 9f2863f4573735df82847c341db8d590d510d448
This reviews git diff e5b16d14bbf9456945986565798902b720d222f3..9f2863f4573735df82847c341db8d590d510d448 and git show 9f2863f45. I found no P1 or P2. The merge brings #2350 in unchanged, and the rates and limits match the seven saved resolutions. The differential, the revert and the mutation results reproduce. Three P3s concern the hub documents' figures and wording. I checked the head out detached in scratch worktrees, with HUGGING_FACE_TOKEN and POLICYENGINE_UK_DEFAULT_DATASET unset. I used no survey data.
Findings
P3: the Marriage Allowance YAML count is stale
PR_BODY.md:114 (the table of results at 9f2863f45) says meets_marriage_allowance_income_conditions.yaml | 26 passed, and the hand-off gives the same count. That was the count at 3af8cb785. #2350 added five cases between 3af8cb785 and e5b16d14b, so the file holds 31 cases at the head, and all 31 pass.
$ for c in 3af8cb785 9aa5ee4c9 e5b16d14b 9f2863f45; do echo "$c $(git show $c:policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.yaml | grep -c '^- name:')"; done
3af8cb785 26
9aa5ee4c9 26
e5b16d14b 31
9f2863f45 31
$ uv run policyengine-core test policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.yaml -c policyengine_uk
======================== 31 passed, 1 warning in 9.80s =========================
The other counts in that table match #2350's files at e5b16d14b (36, 10 and 5) and my runs.
P3: the #2106 merge note names a branch that isn't #2830's head
PR_BODY.md:157 names #2106's claude/fix-2106-allowance-order-979j96 at def18d3a…. The brief named that head. #2830, the PR for #2106, has head branch fix-2106-allowance-order at 70633f0897957186ff9128aac91eb98ea9d7c5ec, pushed at 17:40 UTC on 2 November. def18d3a3 is an ancestor of 70633f089. The result doesn't change: both heads conflict in the same file only, and e5b16d14b alone conflicts in that same file.
$ gh api repos/PolicyEngine/policyengine-uk/pulls/2830 --jq '"\(.head.ref) \(.head.sha)"'
fix-2106-allowance-order 70633f0897957186ff9128aac91eb98ea9d7c5ec
$ git merge-tree --write-tree --name-only 9f2863f4573735df82847c341db8d590d510d448 70633f0897957186ff9128aac91eb98ea9d7c5ec
e07ec4f4fb131044fa0d9b49d3d747a44a0cf2d5
policyengine_uk/variables/gov/hmrc/income_tax/bracketized_earned_income/non_savings_income_after_allowances.py
... (exit 1; the same file in the other order and from e5b16d14b)
Replacement row for PR_BODY.md:157:
| #2830 (#2106) at `70633f0897957186ff9128aac91eb98ea9d7c5ec` | Conflict in `policyengine_uk/variables/gov/hmrc/income_tax/bracketized_earned_income/non_savings_income_after_allowances.py`. |
The sentence after the table, "#2350's head e5b16d14b alone gives the same conflicting files against #2106 and #1963", stays true for both #2106 heads.
P3: the new text still has a document or a statute as the actor in places
The author used the previous review's replacement text, but these sentences still give the action to a website, a document or a statute, which breaks the writing rules. None of them changes a number.
scottish_rate_resolutions.yaml:11–13: "gov.scot adds the standard allowance to the lower limits. At the £150,000 top, s. 35(2) leaves no allowance, so gov.scot's figure equals the limit." Replacement:
# £100,000, leaving none at £160,000. The Scottish Government's tables
# (gov.scot) add the standard allowance to the lower limits. No allowance
# remains at the £150,000 top (s. 35(2)), so the tables give the limit itself.
rates.yaml:9–10: "gov.scot's tables add the standard personal allowance to each limit, except at the £150,000 and £125,140 tops, where s. 35(2) leaves no allowance." Replacement:
# The Scottish Government's tables (gov.scot) add the standard personal
# allowance to each limit, except at the £150,000 and £125,140 tops, where no
# allowance remains (s. 35(2)).
test_scottish_income_tax_rates.py:13: "We transcribe…" describes how the data was produced. Replacement:
# The Scottish Parliament's rates and limits for each year, paragraph by
# paragraph, from each motion at
# https://www.parliament.scot/chamber-and-committees/votes-and-motions/<motion>.
# Each pair gives the rate and the upper taxable-income limit; `None` marks the
# final rate. Taxable income excludes the personal allowance.
PR_BODY.md:29: "The Scottish Government's table for each year (gov.scot) adds the standard personal allowance to each limit" gives the action to the table. Replacement opening: "The Scottish Government adds the standard personal allowance to each limit in its table for each year (gov.scot), and its figures agree with the limits in every year from 2017-18 to 2026-27."
Older sentences that the delta didn't touch, such as "s. 35(1) sets the allowance" and "s. 35(2) withdraws £1…", follow the same pattern. I leave them to the hub's judgement.
Checked and sound
The merge.ac8a67339 has parents 9aa5ee4c9 and e5b16d14b. git diff --stat e5b16d14b ac8a67339 lists only the five files (changelog.d/2783.fixed.md, rates.yaml, programs.yaml, scottish_rate_resolutions.yaml, test_scottish_income_tax_rates.py). Each of those five blobs in ac8a67339 equals its blob in 9aa5ee4c9. That diff touches no non-test .py file, so every other file, #2350's helper and its callers included, matches e5b16d14b byte for byte. 9f2863f45 changes only comments and a docstring in three files (+25 −20).
The previous round's findings.
Finding
Status
Evidence
P3 Axiom placeholder
Closed, as the brief directs: axiom: hub fills this line (PR_BODY.md:5). The hub still files the issue.
Posted body
P3 couple-years
Closed (PR_BODY.md:66). "The same nine earners in each year" holds.
Differential below: rows 545–547, 550–552 and 555–557 in 2019 and 2020, and row 540 in 2018
P3 YAML header top exception
Closed in substance (scottish_rate_resolutions.yaml:11–13). The voice nit is above.
git show 9f2863f45
Voice PR_BODY.md:42
Closed. I checked the sentence against main (6c6e229a5): tax_band.formula_2018_06_01 bands Scottish taxpayers by income < threshold, where income = net_income - allowances, savings included. main's meets_marriage_allowance_income_conditions reads tax_band against eligible_bands.
git show 6c6e229a:…/tax_band.py, …/meets_marriage_allowance_income_conditions.py
Voice PR_BODY.md:46
Closed, with the base corrected to e5b16d14b. The correction is right.
Posted body
Voice PR_BODY.md:70
Closed
Posted body
Voice rates.yaml:11–15, :71, :92, :106–107
Closed at :11–15, :71–72, :93–94 and :108–110
git show 9f2863f45
Voice Python :13–17, :129–131, :150
Closed. The "We transcribe" wording is noted above.
git show 9f2863f45
Voice YAML header :11–12
Closed
git show 9f2863f45
Voice AXIOM_ISSUE.md:69, :75
Closed. Line 69 matches the review's text. Line 75 keeps the one-version sentence and then uses the review's text. Both are accurate.
Posted AXIOM_ISSUE.md
What the author changed beyond the review's text.
The "Marriage Allowance and 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" paragraph (PR_BODY.md:72) matches the merged tree. scottish_tax_band bands non_savings_income_after_allowances on the Scottish limits with >, so income at a limit stays in the band below. It bands savings and dividends on the rest-of-UK limits. liable_only_at_marriage_allowance_rates doesn't read tax_band; it tests non-savings income against the Scottish limit (thresholds[3] from 2018) and tests savings and dividends against the rest-of-UK basic rate limit.
Pinned heads: every SHA in the body and AXIOM_ISSUE.md is 9f2863f45, e5b16d14b, a merge-check head, or older evidence labelled eff166f46 or d07335b96. Both older commits are ancestors of 9aa5ee4c9.
Body order: Reproducer: and axiom: come right after Fixes #2783.
Component counts: earned_income_tax 130/240/240/230 and property_income_tax 30/46/50/22 reproduce exactly.
The differential. I ran the previous reviewer's differential.py unchanged, once in each tree, each with its own uv sync venv. The base venv printed …/base/policyengine_uk/__init__.py and the head venv …/head/…. PYTHONPATH doesn't override the editable install, so I used separate venvs. Each run covered 930 units over 2015–2030. verify_differential.py, with only its paths changed, printed PASS, so every assertion holds:
income_tax, unit_income_tax and household_net_income move in 138, 251, 252 and 243 units in 2018 to 2021, and in no unit outside Scotland or outside those years;
savings and dividend tax are identical in every unit and year;
tax_band moves in 2, 30, 32 and 8 units;
the Marriage Allowance moves in 1, 9 and 9 units, with 19 couple-years across 10 distinct couples;
the seven pay-only rows hold;
the 2018 £45,000 component examples hold (£7,133.50 → £7,134.00, £975, £1,630, £4,000 → £4,100, £11,133.50 → £11,234.00);
differential_static_counts.py on the fresh arrays gives allowance_observations: 19, distinct_couples_with_allowance_change: 10 (rows 540, 545, 546, 547, 550, 551, 552, 555, 556 and 557), and no differences at or below half a penny. The body's "£30,931 to £31,500 after the allowance (pay of £43,431, £43,662 or £44,000…)" matches those rows.
The revert. I copied the head's two test files onto e5b16d14b's worktree, ran them and restored the worktree.
scottish_rate_resolutions.yaml gave 28 failed and 13 passed.
test_scottish_income_tax_rates.py -k "not pay_is_taxed" gave 12 failed and 27 passed. The failures are the 2018 to 2021 rows of …match_each_resolution, …whole_tax_year and …gov_scot_tables.
Both match the hand-off and PR_BODY.md:123.
The mutations. I ran 14 mutations of rates.yaml on a separate scratch worktree, one at a time, with -k "match_each_resolution or whole_tax_year or 6_april_2017 or gov_scot". The unmutated control passed 31 of 31 (the author's 34 also includes the three thresholds_match_the_resolution cases). I restored the worktree afterwards, and git status reports it clean. All 14 mutations were killed:
Mutation
Tests that failed
Starter limit £2,050 for July 2019 only
whole_tax_year[2019]
Higher rate 42% for July 2019 only
whole_tax_year[2019]
Every 2017-04-06 date moved to 1 April 2017
take_effect_on_6_april_2017
2017 basic limit copied to 6 April 2016
take_effect_on_6_april_2017
2019-04-06 dates moved to 1 April
whole_tax_year[2018]
Basic limit +£1 on 7 April 2019 only
whole_tax_year[2019]
Basic limit +£1 on 5 April 2020
whole_tax_year[2019]
Basic limit +£1 on 5 April 2027
whole_tax_year[2026]
2018 intermediate limit £31,581
table, whole-year and gov.scot tests [2018]
2021 basic limit £12,727
table, whole-year and gov.scot tests [2021]
2023 higher limit £125,141
table, whole-year and gov.scot tests [2023]
2017 basic limit £31,501
table, whole-year and gov.scot tests [2017]
2023 top rate 48%
table and whole-year tests [2023]
2017 higher rate 41%
table and whole-year tests [2017]
These agree with the body's statements about which tests catch the July 2019, 1 April 2017, 2016-copy, 1 April and one-day mutations.
Tests at 9f2863f45.
Test
Result
scottish_rate_resolutions.yaml
41 passed (29 single, 12 couple cases)
scottish_rates.yaml
36 passed
meets_marriage_allowance_income_conditions.yaml
31 passed
reforms/cps/marriage_tax_reforms.yaml
10 passed
test_tax_band_scottish_properties.py + test_scottish_income_tax_rates.py with Hypothesis, -n 4
45 passed in 64 s
test_notional_deductions_equal_tax_on_threshold_as_only_income, alone, no xdist
1 passed in 294 s (305 s wall)
test_more_earnings_never_lower_earned_income_or_raise_uc, alone, no xdist
2,167 files already formatted / all checks passed / all checks passed
git diff --check e5b16d14b HEAD
Clean
The UC times differ from the body's 222 s and 86 s. That is expected on different hardware; the body labels its times as its own runs.
Merges. I ran git merge-tree --write-tree --name-only from 9f2863f45 in both orders. The heads come from gh api repos/PolicyEngine/policyengine-uk/pulls/<n>.
e5b16d14b alone gives the same conflicting files against #2830, def18d3a3 and #1963.
AXIOM_ISSUE.md and the rates.
All seven quotations match the saved resolutions character for character. Five are quoted after the elided opening, and the elided opening is identical in each. The S6M-07853 quotation keeps its capital "Income Tax".
The agreed dates (21 February 2017, 20 February 2018, 19 February 2019, 4 March 2020, 25 February 2021, 2 February 2022 and 9 February 2023) match each motion's "Status".
The provisions table and every dated value in rates.yaml for 2017-18 to 2023-24 match the motions' rates and limits.
An independent Decimal recomputation reproduces all 41 companion rows (taxable income and tax), using the allowances the issue states and s. 35(2)/(3) as the issue quotes them.
The body's gov.scot band tops for 2018-19 to 2021-22 equal each limit plus the allowance, apart from the £150,000 top.
Readers. The ten readers in PR_BODY.md:46 match grep -rl "scotland.rates" over the non-test code at the head (utils/parameters.py also matches, as the year converter).
What I couldn't check
The 2024-25 quotation from S6M-12252 ("to be charged above the personal allowance") and gov.scot's 2018-19 table wording: neither source is in this brief, and I don't quote law from memory.
The rulespec-uk and axiom-corpus facts in AXIOM_ISSUE.md (0497350, 9d174d88, the 35 scopes, the 33 hashes): those repositories are outside this session's GitHub scope, and the delta changed none of them.
The eff166f46 results (broader suites, the four extra mutations, and the statement that all five claim cases fail there). I didn't rerun them; the body labels them with that commit.
The other four UC minimum income floor properties, which the body labels d07335b96.
The hand-off's logs on the hub's machine. I reran every figure above instead.
Activity
MaxGhenis commentedon Nov 2, 2026
Picking this up for the UK hub. Reply here if you're mid-change and I'll stop.
Generated by Claude Code
MaxGhenis commentedon Nov 2, 2026
PR_TITLE.txtGenerated by Claude Code
MaxGhenis commentedon Nov 2, 2026
PR_BODY.mdGenerated by Claude Code
MaxGhenis commentedon Nov 2, 2026
AXIOM_ISSUE.mdGenerated by Claude Code
MaxGhenis commentedon Nov 2, 2026
Ready for hub review
Head:
9f2863f4573735df82847c341db8d590d510d448onclaude/restack-2783-scottish-rates-r0nfbo(the harness pins this session's pushes to that branch). No PR opened. I didn't push tofix-2783-scottish-rates(still9aa5ee4c9) orfix-2350-scottish-tax-band(stille5b16d14b).Before starting I found no comments on #2783 and no reviews or review comments on #2828.
fix-2350-scottish-tax-bandgot a commit about 2 h 45 min before I started (e5b16d14b, 16:17 UTC). That is the head this brief names, so I went ahead rather than stopping. Nothing has moved on either branch since.Commits
ac8a673391ac5264919f03347ae3aef95277426b: merge ofe5b16d14bbf9456945986565798902b720d222f3into9aa5ee4c9, with no rebase. It merged cleanly with no conflicts.git diff --stat e5b16d14b ac8a6733lists only this branch's five files (changelog.d/2783.fixed.md,rates.yaml,programs.yaml,scottish_rate_resolutions.yaml,test_scottish_income_tax_rates.py). Filtering that diff to non-test.pyfiles gives an empty result, so the merge changes no formula.tax_bandcompares a Scottish taxpayer's total income with the Scottish thresholds; the Scottish rates apply to non-savings, non-dividend income only #2350's helper and callers are byte-for-byte astax_bandcompares a Scottish taxpayer's total income with the Scottish thresholds; the Scottish rates apply to non-savings, non-dividend income only #2350 has them.9f2863f4573735df82847c341db8d590d510d448: the review's comment and docstring edits only (3 files, +25 −20).Findings and resolutions
axiom: hub fills this line.scottish_rate_resolutions.yaml:11–13uses the review's text.PR_BODY.md:42main'stax_band(income < thresholdon net income less allowances) andmain's Marriage Allowance test (it readstax_band), so the sentence stays true.PR_BODY.md:46e5b16d14b, not3af8cb785, and names this head. The old base is stale.PR_BODY.md:70rates.yaml:11–15,:71,:92,:106–107rates.yaml:11–15,:71–72,:93–94and:108–110.:13–17,:129–131,:150test_scottish_income_tax_rates.py:13–17,:129–132and the docstring at:151.AXIOM_ISSUE.md:69,:75:75I kept the opening clause saying the pipeline's allowance has one version from 2026-01-01 as its own sentence, because it explains why dated allowances are needed.Other edits I made because the merged tree made the old text false or stale:
tax_bandcompares a Scottish taxpayer's total income with the Scottish thresholds; the Scottish rates apply to non-savings, non-dividend income only #2350 description. Ate5b16d14b,tax_bandcompares a Scottish taxpayer's total income with the Scottish thresholds; the Scottish rates apply to non-savings, non-dividend income only #2350's Marriage Allowance test no longer readstax_band. It applies the s. 55B(2)(b) and (ba) rate conditions to income directly. I reworded the "Marriage Allowance andtax_bandcompares a Scottish taxpayer's total income with the Scottish thresholds; the Scottish rates apply to non-savings, non-dividend income only #2350" paragraph to say so, after readingmeets_marriage_allowance_income_conditions.pyandtax_band.pyin the merged tree.9f2863f45or, for older evidence, the commit it ran at (eff166f46,d07335b96).Reproducer:andaxiom:now come right afterFixes #2783, following the brief's body rules. The hub's draft had them at the end.earned_income_taxandproperty_income_taxcounts.Differential
I ran the reviewer's
differential.py,verify_differential.pyanddifferential_static_counts.pywith only the paths changed. The base wase5b16d14bbf9456945986565798902b720d222f3and the head9f2863f4573735df82847c341db8d590d510d448. Each run printed its importedpolicyengine_uk/__init__.py, which confirms the scratch worktree for each tree. Both runs covered 930 units over 2015–2030.verify_differential.pyprinted PASS. Every assertion holds unchanged against the new base:income_tax,unit_income_taxandhousehold_net_incomemoved in 138/251/252/243 units in 2018–2021.earned_income_taxmoved in 130/240/240/230 units andproperty_income_taxin 30/46/50/22.tax_bandmoved in 2/30/32/8 units.differential_static_counts.pygave 19 observations across 10 couples and no sub-half-penny differences.Tests at
9f2863f45scottish_rate_resolutions.yamlscottish_rates.yamlmeets_marriage_allowance_income_conditions.yamlreforms/cps/marriage_tax_reforms.yamltest_tax_band_scottish_properties.py+test_scottish_income_tax_rates.py(with Hypothesis), one-n 4runtest_notional_deductions_equal_tax_on_threshold_as_only_income, alone, no xdisttest_more_earnings_never_lower_earned_income_or_raise_uc, alone, no xdistpolicyengine_uk/tests/code_health -n 4ruff@0.17.2 format --check .ruff@0.17.2 check .ruff@0.17.2 check --select F811,F821 policyengine_ukgit diff --checke5b16d14b..HEADandmain..HEADThe policyengine-core CLI collected the YAML files directly, with no bridge needed.
Revert and mutation checks
Revert. I took
e5b16d14b's tree and copied in this branch's two test files, then confirmed that the copied tree differs from the head only inrates.yamlandprograms.yaml.scottish_rate_resolutions.yamlgives 28 failed / 13 passed.test_scottish_income_tax_rates.pywithout Hypothesis gives 12 failed / 27 passed: the 2018–2021 rows of the table, whole-year and gov.scot tests.My first YAML attempt passed 41 because the console script imported the editable install rather than the copied tree. I discarded that run and reran with
PYTHONPATHpointing at the copy, which__file__confirms.Mutations. A new harness copies the head tree and applies each of 64 scratch mutations to
rates.yaml, matching the list in the body. For each one it runs the table, whole-year, first-date and gov.scot tests (34 tests). The unmutated control passes 34 of 34, and all 64 mutations fail.test_each_resolution_holds_for_its_whole_tax_year[2019].test_scottish_rates_take_effect_on_6_april_2017.The harness ran with
-x, so it records only the first failing test for each mutation.Merge-tree
I ran
git merge-tree --write-tree --name-onlyfrom9f2863f4573735df82847c341db8d590d510d448in both orders.main6c6e229a51fbd804dab2062dac1f94af04c609c2(stillorigin/main)2854b211…in both orders3f67112c834ae8b26c42357fe7e7854657bd2a94def18d3a3043d1b65e74de2a69b169d4f0a550dbvariables/gov/hmrc/income_tax/bracketized_earned_income/non_savings_income_after_allowances.py69c919f6054023fa5a51b42d1ffbbcb0078e8082e5b16d14balone conflicts in the same files against #2106 and #1963, so those conflicts are #2350's.Skipped
eff166f46suites. The body labels those as older-commit results.eff166f46mutations (which ran against the table tests and YAML cases); the body labels them with that commit.Judgment calls
tax_bandcompares a Scottish taxpayer's total income with the Scottish thresholds; the Scottish rates apply to non-savings, non-dividend income only #2350's commit from about 3 hours earlier as the anticipated state, because it is the exact SHA this brief names.Generated by Claude Code
MaxGhenis commentedon Nov 2, 2026
Ready for hub review
Verdict: APPROVE WITH NITS at
9f2863f4573735df82847c341db8d590d510d448This reviews
git diff e5b16d14bbf9456945986565798902b720d222f3..9f2863f4573735df82847c341db8d590d510d448andgit show 9f2863f45. I found no P1 or P2. The merge brings #2350 in unchanged, and the rates and limits match the seven saved resolutions. The differential, the revert and the mutation results reproduce. Three P3s concern the hub documents' figures and wording. I checked the head out detached in scratch worktrees, withHUGGING_FACE_TOKENandPOLICYENGINE_UK_DEFAULT_DATASETunset. I used no survey data.Findings
P3: the Marriage Allowance YAML count is stale
PR_BODY.md:114(the table of results at9f2863f45) saysmeets_marriage_allowance_income_conditions.yaml | 26 passed, and the hand-off gives the same count. That was the count at3af8cb785. #2350 added five cases between3af8cb785ande5b16d14b, so the file holds 31 cases at the head, and all 31 pass.Replacement for
PR_BODY.md:114:The other counts in that table match #2350's files at
e5b16d14b(36, 10 and 5) and my runs.P3: the #2106 merge note names a branch that isn't #2830's head
PR_BODY.md:157names#2106's claude/fix-2106-allowance-order-979j96 at def18d3a…. The brief named that head. #2830, the PR for #2106, has head branchfix-2106-allowance-orderat70633f0897957186ff9128aac91eb98ea9d7c5ec, pushed at 17:40 UTC on 2 November.def18d3a3is an ancestor of70633f089. The result doesn't change: both heads conflict in the same file only, ande5b16d14balone conflicts in that same file.Replacement row for
PR_BODY.md:157:The sentence after the table, "#2350's head
e5b16d14balone gives the same conflicting files against #2106 and #1963", stays true for both #2106 heads.P3: the new text still has a document or a statute as the actor in places
The author used the previous review's replacement text, but these sentences still give the action to a website, a document or a statute, which breaks the writing rules. None of them changes a number.
scottish_rate_resolutions.yaml:11–13: "gov.scot adds the standard allowance to the lower limits. At the £150,000 top, s. 35(2) leaves no allowance, so gov.scot's figure equals the limit." Replacement:rates.yaml:9–10: "gov.scot's tables add the standard personal allowance to each limit, except at the £150,000 and £125,140 tops, where s. 35(2) leaves no allowance." Replacement:test_scottish_income_tax_rates.py:13: "We transcribe…" describes how the data was produced. Replacement:PR_BODY.md:29: "The Scottish Government's table for each year (gov.scot) adds the standard personal allowance to each limit" gives the action to the table. Replacement opening: "The Scottish Government adds the standard personal allowance to each limit in its table for each year (gov.scot), and its figures agree with the limits in every year from 2017-18 to 2026-27."Older sentences that the delta didn't touch, such as "s. 35(1) sets the allowance" and "s. 35(2) withdraws £1…", follow the same pattern. I leave them to the hub's judgement.
Checked and sound
The merge.
ac8a67339has parents9aa5ee4c9ande5b16d14b.git diff --stat e5b16d14b ac8a67339lists only the five files (changelog.d/2783.fixed.md,rates.yaml,programs.yaml,scottish_rate_resolutions.yaml,test_scottish_income_tax_rates.py). Each of those five blobs inac8a67339equals its blob in9aa5ee4c9. That diff touches no non-test.pyfile, so every other file, #2350's helper and its callers included, matchese5b16d14bbyte for byte.9f2863f45changes only comments and a docstring in three files (+25 −20).The previous round's findings.
axiom: hub fills this line(PR_BODY.md:5). The hub still files the issue.PR_BODY.md:66). "The same nine earners in each year" holds.scottish_rate_resolutions.yaml:11–13). The voice nit is above.git show 9f2863f45PR_BODY.md:42main(6c6e229a5):tax_band.formula_2018_06_01bands Scottish taxpayers byincome < threshold, whereincome = net_income - allowances, savings included.main'smeets_marriage_allowance_income_conditionsreadstax_bandagainsteligible_bands.git show 6c6e229a:…/tax_band.py,…/meets_marriage_allowance_income_conditions.pyPR_BODY.md:46e5b16d14b. The correction is right.PR_BODY.md:70rates.yaml:11–15,:71,:92,:106–107:11–15,:71–72,:93–94and:108–110git show 9f2863f45:13–17,:129–131,:150git show 9f2863f45:11–12git show 9f2863f45AXIOM_ISSUE.md:69,:75AXIOM_ISSUE.mdWhat the author changed beyond the review's text.
tax_bandcompares a Scottish taxpayer's total income with the Scottish thresholds; the Scottish rates apply to non-savings, non-dividend income only #2350" paragraph (PR_BODY.md:72) matches the merged tree.scottish_tax_bandbandsnon_savings_income_after_allowanceson the Scottish limits with>, so income at a limit stays in the band below. It bands savings and dividends on the rest-of-UK limits.liable_only_at_marriage_allowance_ratesdoesn't readtax_band; it tests non-savings income against the Scottish limit (thresholds[3]from 2018) and tests savings and dividends against the rest-of-UK basic rate limit.AXIOM_ISSUE.mdis9f2863f45,e5b16d14b, a merge-check head, or older evidence labelledeff166f46ord07335b96. Both older commits are ancestors of9aa5ee4c9.Reproducer:andaxiom:come right afterFixes #2783.earned_income_tax130/240/240/230 andproperty_income_tax30/46/50/22 reproduce exactly.The differential. I ran the previous reviewer's
differential.pyunchanged, once in each tree, each with its ownuv syncvenv. The base venv printed…/base/policyengine_uk/__init__.pyand the head venv…/head/….PYTHONPATHdoesn't override the editable install, so I used separate venvs. Each run covered 930 units over 2015–2030.verify_differential.py, with only its paths changed, printedPASS, so every assertion holds:income_tax,unit_income_taxandhousehold_net_incomemove in 138, 251, 252 and 243 units in 2018 to 2021, and in no unit outside Scotland or outside those years;tax_bandmoves in 2, 30, 32 and 8 units;differential_static_counts.pyon the fresh arrays givesallowance_observations: 19,distinct_couples_with_allowance_change: 10(rows 540, 545, 546, 547, 550, 551, 552, 555, 556 and 557), and no differences at or below half a penny. The body's "£30,931 to £31,500 after the allowance (pay of £43,431, £43,662 or £44,000…)" matches those rows.The revert. I copied the head's two test files onto
e5b16d14b's worktree, ran them and restored the worktree.scottish_rate_resolutions.yamlgave 28 failed and 13 passed.test_scottish_income_tax_rates.py -k "not pay_is_taxed"gave 12 failed and 27 passed. The failures are the 2018 to 2021 rows of…match_each_resolution,…whole_tax_yearand…gov_scot_tables.Both match the hand-off and
PR_BODY.md:123.The mutations. I ran 14 mutations of
rates.yamlon a separate scratch worktree, one at a time, with-k "match_each_resolution or whole_tax_year or 6_april_2017 or gov_scot". The unmutated control passed 31 of 31 (the author's 34 also includes the threethresholds_match_the_resolutioncases). I restored the worktree afterwards, andgit statusreports it clean. All 14 mutations were killed:whole_tax_year[2019]whole_tax_year[2019]take_effect_on_6_april_2017take_effect_on_6_april_2017whole_tax_year[2018]whole_tax_year[2019]whole_tax_year[2019]whole_tax_year[2026]These agree with the body's statements about which tests catch the July 2019, 1 April 2017, 2016-copy, 1 April and one-day mutations.
Tests at
9f2863f45.scottish_rate_resolutions.yamlscottish_rates.yamlmeets_marriage_allowance_income_conditions.yamlreforms/cps/marriage_tax_reforms.yamltest_tax_band_scottish_properties.py+test_scottish_income_tax_rates.pywith Hypothesis,-n 4test_notional_deductions_equal_tax_on_threshold_as_only_income, alone, no xdisttest_more_earnings_never_lower_earned_income_or_raise_uc, alone, no xdistpolicyengine_uk/tests/code_health -n 4ruff@0.17.2 format --check ./check ./check --select F811,F821 policyengine_ukgit diff --check e5b16d14b HEADThe UC times differ from the body's 222 s and 86 s. That is expected on different hardware; the body labels its times as its own runs.
Merges. I ran
git merge-tree --write-tree --name-onlyfrom9f2863f45in both orders. The heads come fromgh api repos/PolicyEngine/policyengine-uk/pulls/<n>.main6c6e229a51fbd804dab2062dac1f94af04c609c2(unchanged)2854b211…in both orders3f67112c834ae8b26c42357fe7e7854657bd2a94e2fe57b1…in both orders70633f0897957186ff9128aac91eb98ea9d7c5ec…/bracketized_earned_income/non_savings_income_after_allowances.pyonlydef18d3a3043d1b65e74de2a69b169d4f0a550db69c919f6054023fa5a51b42d1ffbbcb0078e8082e5b16d14bbf9456945986565798902b720d222f3e5b16d14balone gives the same conflicting files against #2830,def18d3a3and #1963.AXIOM_ISSUE.mdand the rates.rates.yamlfor 2017-18 to 2023-24 match the motions' rates and limits.Decimalrecomputation reproduces all 41 companion rows (taxable income and tax), using the allowances the issue states and s. 35(2)/(3) as the issue quotes them.Readers. The ten readers in
PR_BODY.md:46matchgrep -rl "scotland.rates"over the non-test code at the head (utils/parameters.pyalso matches, as the year converter).What I couldn't check
AXIOM_ISSUE.md(0497350,9d174d88, the 35 scopes, the 33 hashes): those repositories are outside this session's GitHub scope, and the delta changed none of them.eff166f46results (broader suites, the four extra mutations, and the statement that all five claim cases fail there). I didn't rerun them; the body labels them with that commit.d07335b96.Generated by Claude Code