Skip to content

Bar income-related ESA at pensionable age and on Pension Credit - #2807

Merged
juaristi22 merged 43 commits into
mainfrom
fix-2565-esa-pension-credit
Nov 10, 2026
Merged

juaristi22 merged 43 commits into
mainfrom
fix-2565-esa-pension-credit

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2565

Tests under the abolition of income-related ESA

main models the abolition from 1 July 2026 (1 October 2026 in Northern Ireland; #2813). The abolition reaches an unsaved award reported in 2026-27, and a family that would claim Universal Credit on the move ends the award for the year, so several 2026 tests would lose the award they test. Under the claimant's appointee saving DWP keeps the award in payment (SI 2025/1148 art. 3A(2), esa_income_appointee_saving), and the model then pays the claim for the whole of 2026-27 (esa_income_payable_share). Each of these tests saves the 2026 awards it tests, so it still tests the pension-age bar and the Pension Credit condition:

  • esa_income_pension_age.yaml cases 4, 5, 6, 12, 13 and 14: each ESA reporter has the saving. Each case asserts a full-year award in 2026-27, which the abolition would otherwise cut to 86/365 or close.
  • test_couple_esa_follows_pension_credit_worked_out_without_it: every adult drawn in 2026 has the saving. Without it, a couple aged 58 and 65 reached the reference's abolition rule, which covers no age from 65 to 69.
  • legacy_award_work_reference.esa_abolition_claim: reads the saving as esa_income_payable_share does. Where a member who reports the couple's award has the saving, DWP keeps the whole claim in payment, and the model ignores a saving held only by a member who reports nothing. No caller without the saving changes.
  • test_a_parameter_reform_keeps_the_age_and_take_up_conditions: the working-age claimant and the younger member of the couple aged 70 and 60 have the saving. Unsaved, each claims Universal Credit when the abolition reaches the award, and gets no ESA whatever the reform.
  • test_the_screen_reads_no_pension_credit_condition_without_a_couple_claim: every reporter has the saving. Unsaved, the claimant aged 45 out of work claims Universal Credit, so no couple's claim reaches the Pension Credit conditions, and the claimant in remunerative work fails on a closed report and not the work test.
  • test_no_branch_opens_without_a_couple_that_could_be_on_pension_credit: every reporter has the saving. Unsaved, the couple's awards end in a Universal Credit claim and the member outside the couple gets 86/365 of £5,000.

The abolition's own rules stay with #2813's tests, test_esa_income_abolition_properties.py among them.

What and why

On origin/main (2da212a), esa_income_eligible screens a reported income-related ESA award on capital and remunerative work alone, so the model pays it beside State Pension Credit and to claimants past pensionable age. With £5,000 of reported income-related ESA in 2026 and no Universal Credit claim, a single person aged 70 gets £5,000 of ESA and £12,376 of Pension Credit. A couple aged 70 and 68 gets £5,000 and £18,889, and so does a mixed-age couple aged 73 and 60 that has kept Pension Credit since May 2019.

Parliament bars both overlaps in the Welfare Reform Act 2007:

  • Under s.1(3)(c), a claimant who has reached pensionable age gets no ESA. In s.1(6) Parliament took pensionable age from the Pensions Act 1995 Sch 4 para 1: the claimant's own State Pension age, which the model holds as is_SP_age.
  • In Sch 1 para 6(1)(c) and (d) Parliament set two more conditions for the income-related allowance: the claimant "is not entitled to state pension credit", and "is not a member of a couple the other member of which is entitled to ... state pension credit".

This PR applies both:

  • esa_income_eligible refuses a candidate claimant who has reached pensionable age.
  • It reads the State Pension Credit the couple would get with no income-related ESA in the family, B(0) below, and refuses the couple's claim where B(0) exceeds nil. It reads pension_credit, so the claim (would_claim_pc) and an entered award count.
  • The model's own formulas pay Pension Credit only to a couple that meets the Pension Credit age conditions and would claim, so where they calculate it, the screen reads B(0) only for such a couple. A user can instead enter pension_credit, pension_credit_entitlement, is_pension_credit_eligible, guarantee_credit or savings_credit for the year. A reform can change anything Core calculates one of those five from: a formula, the default a family outside defined_for gets, defined_for itself or what the entitlement sums. It can also freeze Pension Credit at the baseline's award (gov.contrib.freeze_pension_credit). The screen then reads B(0) for every couple whose claim passes the other tests (pension_credit_follows_model_formulas). Under a parameter reform the model keeps its formulas, and meets_pension_credit_age_conditions reads the reform's parameters.
  • The screen also reads B(0) for every couple whose claim passes the other tests when a reform gives would_claim_pc a formula, since that formula could read the couple's ESA. With take-up entered as an input, it still reads B(0) only for a couple that meets the age conditions and would claim. When would_claim_pc has no formula, the screen reads it in the simulation itself, so a reform that makes take-up read the couple's ESA through defined_for or adds stops the calculation with CycleError.
  • The model also refuses an own-right claim from a member outside the couple once they reach pensionable age, in esa_income and is_on_income_related_esa. That member keeps their claim whatever the couple's Pension Credit: in para 6(1)(d) Parliament named only the other member of the claimant's couple.
  • pension_credit pays nothing to a family while the claimant or partner has income-related ESA (claimant_or_partner_esa_income above nil), so the couple never gets both awards. It reads the couple's award and not esa_income, so a member outside the couple keeps their own award beside the couple's Pension Credit. Under the freeze (gov.contrib.freeze_pension_credit) the formula still returns the baseline's award, and an entered pension_credit still replaces the formula.
  • A member past pensionable age can't be the couple's claimant, so is_on_income_related_esa and the Housing Benefit and Universal Credit benefit cap readers pass can_claim to is_payee_of_couple_award, as is_on_pension_credit does. own_report_is_paid takes barred in place of works, and is_on_income_based_jsa passes the same work test as before.

With the fix, the three families above get no ESA and the same Pension Credit: £12,376 = 52 × £238.00 and £18,889 = 52 × £363.25, the weekly rates the Secretary of State set in SPC Regs 2002 reg 6(1) by SI 2026/148 art 29(2). The same 73 and 60 couple without Pension Credit since May 2019 gets no Pension Credit (SPCA 2002 s.4(1A)), and keeps its £5,000 where DWP keeps the award in payment through 2026-27 under an appointee saving (case 4). A man aged 63 in 2016 who reports £5,000 of income-related ESA and has £2,000 of Pension Credit as an input gets the £2,000 and no ESA, whether or not he would claim Pension Credit; so does a man aged 40 whose Pension Credit entitlement or eligibility an input sets. Under a reform that gives a family outside the entitlement's defined_for £2,000 of it, a man aged 40 in 2016 who reports £5,000 of income-related ESA and would claim Pension Credit gets the £2,000 and no ESA. Under a reform in which a family claims Pension Credit while it gets no income-related ESA, two men aged 63 each get £8,091.20 of Pension Credit and no ESA, and a man aged 40 keeps his £5,000 and gets no Pension Credit.

replaces_definition, in utils/supplied_inputs.py, compares the simulation's variable with a fresh instance of the model's own class. It compares everything Core calculates the variable from: the formula for the period, by its code, the default, defined_for, adds and subtracts, the period, the end date and neutralization. It ignores the label, documentation, reference, unit, metadata and the other descriptive attributes. It also counts an input for the year, and the five Pension Credit variables join SUPPLIED_INPUT_VARIABLES so that it can ask about inputs.

Pension Credit without the award

The model's Pension Credit reads the couple's award five ways, so a screen that read pension_credit directly would stop the engine with CycleError:

  • pension_credit itself pays nothing while the couple has the award (claimant_or_partner_esa_income), so that no couple gets both, since Parliament allows the award only where neither member of the couple has an entitlement to State Pension Credit (WRA 2007 Sch 1 para 6(1)(c) and (d));
  • pension_credit_earnings_disregard reads claimant_or_partner_esa_income, from the list gov.dwp.pension_credit.earnings_disregard.higher.disability_benefit_unit_benefits, for the higher earnings disregard (SPC Regs 2002 Sch VI para 4(1)(a)(vii));
  • pension_credit_income counts working tax credit, and the tax credit income test reads the award (TCA 2002 s.7(2), tax_credits_applicable_income); savings_credit_income takes working tax credit back out, and pension_credit_earnings_disregard reads working tax credit's disability elements;
  • that test also reads Income Support, whose gate (income_support_eligible) reads the award;
  • Pension Credit's route test (left_pension_route_at_legacy_closure) reads Universal Credit entitlement, whose work capability test reads esa_income (has_own_esa_award).

pension_credit_without_income_related_esa follows pension_credit_with_passported_tax_credits. It opens a throwaway branch under a name neither the simulation nor its branches use, and sets esa_income, claimant_or_partner_esa_income and esa_income_eligible to nil there. It then reads pension_credit through the branch's population and pops the branch in finally. Core reads an input that a parent branch sets. So in a tax credit year the passport branch inside it reads the award as nil, and an ESA branch inside a passport branch keeps the passport's input.

The screen returns before it reads any Pension Credit condition when no couple's claim passes the age, work and capital tests. Otherwise it opens the branch only where some couple's claim passes them and, while the model's own formulas calculate Pension Credit, that couple meets the age conditions and would claim.

Write B(e) for the family's Pension Credit with the couple's award e. The model pays:

B(0) B(e) The model pays YAML case
more than nil more than nil B(0), no ESA 15
nil nil ESA, no Pension Credit 16, 17, 20
more than nil nil B(0), no ESA 19
nil more than nil ESA, no Pension Credit 18, 25, 26

In the third row, working tax credit decides the result. With the award, HMRC pays working tax credit without its income test, and the full credit puts the couple's income over the minimum guarantee; without the award, the income-tested credit leaves the couple some Pension Credit. Both states hold. The model takes the one with Pension Credit and no income-related ESA, since Parliament made the allowance depend on the absence of Pension Credit (para 6(1)(c) and (d)) and not the reverse.

In the fourth row the couple gets Pension Credit only with the award, through the higher earnings disregard, and the model pays the award and no Pension Credit. Parliament allows the award only while the claimant "is not entitled to state pension credit" and "is not a member of a couple the other member of which is entitled to ... state pension credit" (para 6(1)(c) and (d)). With Pension Credit in payment the couple's ESA would stop, so the £20 that DWP disregards for a claimant or partner "in receipt of ... employment and support allowance" (SPC Regs 2002 Sch VI para 4(1)(a)(vii)) would stop with it. Where the claimant or partner had an award of income-related ESA in the 8 weeks before Pension Credit began, DWP keeps disregarding the £20 under para 4(2) only where:

(a) £20 was disregarded in respect of earnings taken into account in that award;
(b) the person whose earnings qualified for the disegard [sic] continues in employment after the termination of that award.

For a claimant in permitted work, DWP disregards "any earnings derived from that work which do not exceed the limits specified for that work" (ESA Regs 2008 Sch 7 para 5): all of those earnings up to the limit, not a fixed £20. Case 18's man has the support component and works 15 hours a week for £115.50, which the 2016-17 limit of £115.50 covers: 16 hours at the £7.20 national living wage comes to £115.20, and esa_exempt_work_higher_earnings_limit rounds that up to the next 50p, citing ESA Regs 2008 reg 45(9A). On 6 April 2016 reg 45(4)(b) covered work "done by a claimant who has or is treated as having limited capability for work-related activity", a condition Parliament set for the support component (WRA 2007 s.4(4)(b)). Sch VI para 4(2), Sch 7 para 5 and reg 45(9A) read the same then as they do now. No formula applies para 4(2) (pension_credit_earnings_disregard lists it as not modelled), so the model's result agrees with one reading of it and not the other. It agrees with the reading under which "£20 was disregarded" refers to the £20 disregard of Sch 7 paras 6 and 7(1) and not to para 5's. On that reading he fails para 4(2)(a): his Pension Credit would fall to B(0), nil, and his ESA would come back, so ESA with no Pension Credit holds over time, and Pension Credit with no ESA doesn't. On another reading, para 4(2)(a) holds wherever DWP disregarded at least £20 of earnings in the award, and while he stays in employment (para 4(2)(b)) he would then keep £525.20 of Pension Credit with no ESA.

A family lands in this row only with earnings, an open ESA report, Pension Credit's age conditions and no other route to the higher disregard. Its income must also exceed the minimum guarantee after the standard disregard by less than the gap between the two disregards: £15 a week, or £10 for a couple. Case 18's man has £115.50 a week of earnings and a £50 pension against a £155.60 minimum guarantee: £160.50 of income after the £5 disregard, and £145.50 after the £20 one. So B(0) is nil and B(e) comes to 52 × £10.10 = £525.20, and the model pays the £5,491.20 award and no Pension Credit.

The model applies this rule where DWP might keep a couple on Pension Credit. DWP disregards £20 of earnings in an ESA award where the claimant's partner is in part-time employment and paragraph 6 doesn't apply (Sch 7 para 7(4)), where the claimant's permitted-work earnings fall £20 or more short of the limit (from the partner's earnings, Sch 7 para 6), and where one of the regulations Sch 7 para 7(2) lists applies to the claimant (subject to para 7(3)). Where the person whose earnings qualified stays in work, DWP could keep such a couple on Pension Credit at B(e), with no ESA, once it claimed (para 4(2)). The model holds no record of how an award started, and pension_credit_earnings_disregard doesn't apply para 4(2), so the model pays such a couple the award and no Pension Credit too.

  • In case 25 (2016), a man aged 63 reports £5,491.20 of income-related ESA and has a £135 weekly pension, and his partner, a woman aged 60, earns £115.50 a week for 15 hours, so DWP disregards £20 of her earnings in the award under para 7(4) if that work is part-time employment. B(0) is nil (£250.50 − £10 = £240.50, over the £237.55 couple minimum guarantee), B(e) comes to 52 × £7.05 = £366.60, and the model pays the award and no Pension Credit.
  • In case 26 (2016), a man aged 63 reports £5,491.20 of income-related ESA, earns £90 a week in 15 hours and has a £130 weekly pension, and his partner, a woman aged 60, earns £30 a week in 5 hours. His £90 falls more than £20 short of 16 × £7.20 = £115.20, so DWP disregards £20 of her £30 in the award under para 6. B(0) is nil (£250 − £10 = £240, over £237.55), B(e) comes to 52 × (£237.55 − £230) = £392.60, and the model pays the award and no Pension Credit. Neither member owes income tax: the model treats the couple as married (is_married) and as claiming the marriage allowance (would_claim_marriage_allowance), so it transfers £1,100 of her unused personal allowance to him (marriage_allowance), which covers his £440 over the £11,000 personal allowance.

What this PR does not change

  • Para 6(1)(g) (not receiving education): the screen reads no education test, as before.
  • The other limbs of para 6(1)(d). The model makes the couple's reports one award, so the other member never holds a separate entitlement to an income-related allowance. income_support_eligible already refuses Income Support while the couple has income-related ESA (SSCBA 1992 s.124(1)(h)), and a screen that read Income Support would close another cycle. The screen doesn't test the other member's income-based JSA.
  • income_support_eligible keeps its own reading of s.124(1)(g): the couple meets the Pension Credit age conditions and would claim.
  • s.1(3)(c) for contributory ESA: esa_contrib adds up the reported amounts, with no age test.
  • s.1(3)(e) and (f) (not entitled to Income Support or JSA) for the claimant.
  • The abolition of income-related ESA (Abolish income-related ESA from 1 July 2026 (NI 1 October 2026) #2562), which main models.

Northern Ireland

The Northern Ireland Assembly enacted s.1(3)(c) and Sch 1 para 6(1) word for word in the Welfare Reform Act (Northern Ireland) 2007, and changed two provisions. In s.1(3)(d) the claimant must be "in Northern Ireland". In s.1(6) the Assembly took pensionable age from the Pensions (Northern Ireland) Order 1995 Sch 2 para 1, where Parliament took it from the Pensions Act 1995 Sch 4 para 1. A person born on a given date reaches pensionable age on the same day under either schedule: their nine rules and four tables, 77 rows in all, match line for line. Northern Ireland has the same exclusion of mixed-age couples in the State Pension Credit Act (Northern Ireland) 2002 s.4(1A) and the same saving in SR 2019/4 art 4(1). So the model applies one rule across the UK.

Years this reaches

Income-related ESA has no active parameter, so the screen runs in every model year, and the model applies s.1(3)(c) in every year. The model pays an unsaved award for the share of 2026-27 before the abolition (esa_income_payable_share: 86/365 in Great Britain, 178/365 in Northern Ireland) and closes its report from 2027-28 (esa_income_report_closed), so from 2027-28 only an award under an appointee saving reaches the screen. Only a man with a birth date before 6 December 1953 can fail para 6(1)(c) while still under pensionable age: past the State Pension Credit qualifying age but not his pensionable age of 65. Such men appear only in model years up to 2018. A mixed-age couple that reports no Universal Credit can fail para 6(1)(d) up to model year 2019, since the model reads that year's parameters at 30 April, before the 15 May 2019 exclusion. From then on only a couple that keeps the SI 2019/37 art 4 saving can fail it on the model's formulas. In each case the couple must also get Pension Credit without the award. Where a user enters or reforms Pension Credit, a couple fails para 6(1)(c) or (d) in any year and at any age wherever that Pension Credit exceeds nil.

Invariants

For every input:

  1. Take a couple with an open report whose claim passes the age, work and capital tests. Where B(0) exceeds nil, the model pays the couple no income-related ESA, and the family's Pension Credit equals B(0). This holds whether the model works out Pension Credit or a user enters or reforms it.
  2. Where B(0) is nil, the couple's take-up of Pension Credit changes neither the couple's income-related ESA nor the family's.
  3. Claiming Pension Credit never raises income-related ESA.
  4. Nobody who has reached pensionable age receives income-related ESA (is_on_income_related_esa), and a family whose every report comes from such a member gets none.
  5. esa_income_eligible equals the family-by-family reading of the law in legacy_award_work_reference.esa_screen, given is_SP_age and B(0).
  6. On the model's own formulas, reading B(0) for every couple whose claim passes the other tests gives every family the same ESA, Pension Credit and income as reading it only for a couple that meets the age conditions and would claim.
  7. Without a couple's claim that passes the age, work and capital tests, the screen reads neither the Pension Credit age conditions nor the claim. A population in which no such couple meets the age conditions and would claim opens no branch while the model's formulas calculate Pension Credit, a parameter reform included, and no branch outlives the calculation.
  8. For families to which the model applies neither new refusal, and to which origin/main doesn't pay both Pension Credit and the couple's income-related ESA, every output equals origin/main's.
  9. A reform that changes anything Core calculates a Pension Credit variable from, or gives would_claim_pc a formula, makes the screen read B(0) for every couple whose claim passes the other tests. A change to a variable's label, documentation, reference, unit or metadata alone, a take-up input and a parameter reform keep the age and take-up conditions.
  10. Wherever the model's own formulas calculate Pension Credit, no family has both pension_credit and claimant_or_partner_esa_income above nil.

A family can get both awards in one way: a member outside the couple keeps their own income-related ESA beside the couple's Pension Credit (case 12).

Cost

The branch costs a second Pension Credit calculation for the whole population in each year in which some couple could fail the Pension Credit condition. The screen also opens another inside each branch that recomputes Pension Credit: the tax credit passport and the non-dependant Pension Credit test (uc_non_dep_receives_pension_credit). A Microsimulation pays this in each year up to 2026-27. From 2027-28 the model closes every unsaved report (esa_income_report_closed), so in a dataset run, where nobody has the appointee saving, the screen returns before it opens a branch. At 741054b16, a couple aged 74 and 61 with the mixed-age saving, in which the younger member reports £5,000, opens 2 ESA branches in 2026 and in 2027 with the appointee saving, and none without it (esa_income_report_closed [True, True]).

On a synthetic population of 480 families and 889 people (seed 256502), with the same inputs on each tree, the model opens these branches, ESA branches in brackets:

Year origin/main (2da212a) 0d157f078 b6ddfcf0d
2016 8 (0) 28 (5) 28 (5)
2018 8 (0) 28 (5) 28 (5)
2024 8 (0) 28 (5) 28 (5)
2025 2 (0) 8 (2) 8 (2)
2026 2 (0) 11 (3) 11 (3)

b6ddfcf0d, the first commit with the entered-award test and the early return, gives every family the same 24 outputs as 0d157f078 in each of the five years. Every year of this population has some couple whose claim passes the age, work and capital tests, so the early return skips nothing there; it saves the reads of the age conditions and the claim only in a population with no such couple. An entered or reformed Pension Credit, or a take-up formula, costs more: the screen then reads Pension Credit for every couple whose claim passes the other tests, and opens the branch wherever one does. These counts come from commits before the branch took in #2813, and we haven't timed the early return.

At d37e42de9, on a separate population of 480 families (785 to 810 people, seed 20261031), each year ran in its own process, with origin/main and this PR side by side on a shared host, twice. The table gives the seconds each run took to build the simulation, then to calculate the outputs:

Year origin/main, run 1 This PR, run 1 origin/main, run 2 This PR, run 2
2016 20.1, 4.1 55.0, 12.4 78.0, 24.6 79.4, 44.8
2018 30.2, 9.4 42.2, 33.1 48.3, 13.5 29.4, 21.0
2026 61.0, 10.3 68.8, 9.1 117.6, 16.1 128.5, 25.0

The calculation took 1.6 to 3.5 times as long in 2016 and 2018, years with tax credits, and between 0.9 and 1.6 times as long in 2026. Host load moved between runs, so the branch counts measure the cost more steadily than the clock.

Test plan

CI runs the suite at the head, e2615e80b, which merges main at b8b383cde into eab8ebba3. It resolves one conflict, in the imports of utils/supplied_inputs.py, by keeping this branch's cache and #2308's Tuple. eab8ebba3 merges main at 040e9a5f0 into 3f7735fd6 with no conflict. main gained #2813 at the head this branch already held, dc69f3ada, and #2819, which speeds up slow property tests. At 3f7735fd6 the local runs below cover every test file this PR or #2813 changed against main at 47a2817e2, this PR's two Income Support property files among them. Earlier sections name each target they ran.

Local runs at 3f7735fd6

3f7735fd6 adds these commits to d0ffe7fd3:

  • a8f46e3d8 merges Model the 2026 income-related ESA abolition and its UC claims #2813's head dc69f3ada, with no conflict.
  • 6cd70815a adds case 26's couple (COUPLE_ROW) beside case 18's man to every generated 2016 example of examples(), and an explicit 2018 example (ROWS_2018: case 19's working tax credit couple and case 20's couple) to the combined property and the differential.
  • 0822532e4 changes text only: case names in which DWP, HMRC or the couple acts, case 18's and 26's comments, the docstring of pension_credit_without_income_related_esa, esa_abolition_claim's docstring and the appointee-saving comments.
  • 44ea4c161 changes text only: case 18's comment says which reading its expected values agree with, and pension_credit's guard comment gives the formula the verb.
  • 9327cec76 merges main at 95dfc62f8, with no conflict.
  • 741054b16 changes text only: comments and docstrings in which DWP or the formula acts.
  • 3f7735fd6 merges main at 47a2817e2, with no conflict.

Each file ran alone, with no survey data, after uv sync --frozen --extra dev.

YAML (uv run policyengine-core test <file> -c policyengine_uk), every case passed:

  • esa_income/esa_income_abolition.yaml 41, esa_income/esa_income_capital.yaml 10, esa_income/esa_income_pension_age.yaml 26, esa_income/esa_income_remunerative_work.yaml 47, housing_benefit/housing_benefit_eligible.yaml 15, housing_benefit/housing_benefit_universal_credit_passport.yaml 9, housing_benefit/would_claim_housing_benefit.yaml 6, legacy_award_readers/council_tax_reduction_passports.yaml 11, legacy_award_readers/housing_benefit_pension_age_regulations.yaml 4, legacy_award_readers/pension_credit_earnings_disregard.yaml 4, universal_credit/forgoes_universal_credit_for_tax_free_childcare.yaml 10, universal_credit/housing_costs_element/uc_housing_costs_element.yaml 8, universal_credit/work_allowance/uc_work_allowance.yaml 5, gov/dwp/benefit_cap_by_scheme.yaml 22, gov/dwp/pension_credit/mixed_age_couples.yaml 26 and gov/dwp/universal_credit/person_legacy_benefits_closed.yaml 3.

Python (uv run pytest -q -o xfail_strict=true -n 4 <file>, 30-minute cap; none stopped), every test passed:

  • test_benefit_cap_uc_and_hb_properties.py 1 (58.10 s), test_disable_simulated_benefits.py 49 (149.82 s), test_esa_income_abolition_properties.py 31 (76.56 s), test_esa_income_held_outside_award.py 3 (17.76 s), test_esa_income_pension_age_properties.py 19 (75.21 s), test_esa_income_universal_credit_claim_properties.py 2 (92.58 s), test_esa_permitted_work_nlw_reforms.py 5 (33.27 s), test_housing_benefit_accommodation_dates_properties.py 12 (18.05 s), test_housing_benefit_accommodation_take_up_data_built.py 8 (16.32 s), test_housing_benefit_universal_credit_passport_properties.py 6 (377.40 s), test_income_support_eligibility_properties.py 4 (120.77 s), test_income_support_gate_reads_claimant_or_partner_awards.py 13 (239.57 s), test_legacy_award_readers_properties.py 20 (194.52 s), test_legacy_award_work_properties.py 4 (102.51 s), test_legacy_closure_scope.py 21 (31.95 s), test_mixed_age_couple_properties.py 1 (762.86 s) and test_uc_legacy_closure_properties.py 4 (137.97 s).
  • policyengine_uk/tests/code_health: 3,166 passed in 223.79 s.

Hypothesis 6.170.11 statistics at 3f7735fd6 (--hypothesis-show-statistics, generated examples only): the combined property draws 1 example in 2016 and 3 in 2026, and "Pension Credit pays only with the award" appears in 25% of them, the 2016 one, which carries case 18's man and case 26's couple. The differential draws 1 in 2016 and 1 in 2026. Neither property draws a 2018 example, so the explicit ROWS_2018 example gives each a 2018 family on every run. 6cd70815a appends the couple after the draws. legacy_award_work_reference.py, the one other file the list names, holds the shared test reference and no tests; five of the property files above import it.

We ran mutant M5 as one change to pension_credit.py in a copy of 741054b16: the guard skips couples (where((couple_esa > 0) & ~is_couple, 0, entitlement * would_claim)).

Variant of the property file test_esa_income_pension_age_properties.py under M5
As at 741054b16 Killed, 1 failed and 18 passed in 86.29 s, first on the explicit 2018 example at line 428: case 19's couple gets no Pension Credit against B(0) of 955.40
ROWS_2018 example removed from the combined property Killed on the combined property's generated 2016 example, which carries case 26's couple: assert not ((couple_esa > 0) & (pension_credit > 0)).any() fails (that node alone, run without -n)
ROWS_2018 example removed and COUPLE_ROW out of examples() Survives: the combined property passes (1 passed in 44.23 s, that node alone, run without -n)

In 2018 case 19's couple would get £7.60 of Pension Credit with the award. Under M5 the tax credit passport branch then pays them Pension Credit, HMRC pays working tax credit without its income test, and the couple gets neither Pension Credit nor ESA. In esa_income_pension_age.yaml, M5 fails cases 25 and 26 (2 failed, 24 passed); case 19 runs in 2016, when that couple gets no Pension Credit with the award.

Other checks:

  • .github/check_fix_pr.py from the head e2615e80b, on this title and body, as pr_fix_evidence.yaml runs it, with no labels, against the changes from the merge base (main at b8b383cde) to the head: PASS, 4 changed executable tests.
  • uvx ruff@0.17.2 format --check . (2,235 files already formatted), uvx ruff@0.17.2 check . and uvx ruff@0.17.2 check --select F811,F821 policyengine_uk: all passed. git diff --check d0ffe7fd3..3f7735fd6: clean.
  • git merge-tree --write-tree --name-only 3f7735fd6 origin/main (47a2817e2): clean, tree 49b8bbbe3.

Local runs at d0ffe7fd3

d0ffe7fd3 adds four commits to 254669a0d:

  • 562d067ab merges Model the 2026 income-related ESA abolition and its UC claims #2813's head 3c9890a19, with no conflict.
  • 419bccb44 adds case 26 (the para 6 couple), names Sch 7 paras 6 and 7 in the docstring of pension_credit_without_income_related_esa and in case 25, states the other reading of para 4(2)(a) in case 18, gives DWP or the formula the verb in comments in esa_income_eligible.py and cases 18 and 25 and in pension_credit's documentation string, and rewraps the YAML header.
  • d6bf2f657 adds case 18's man to every generated 2016 example of examples() in test_esa_income_pension_age_properties.py. The explicit @example((ROWS, 2016)) stays.
  • d0ffe7fd3 rewords the appointee-saving comments of esa_income_pension_age.yaml, the property file's docstring and examples()'s comment, legacy_award_work_reference.esa_abolition_claim, case 19's savings-credit comment and case 18's "as under para 5 DWP disregards".

Each file ran alone, with no survey data, after uv sync --frozen --extra dev.

YAML (uv run policyengine-core test <target> -c policyengine_uk), every case passed:

  • esa_income/esa_income_pension_age.yaml: 26 passed.
  • esa_income/esa_income_abolition.yaml: 40 passed.
  • housing_benefit/housing_benefit_eligible.yaml: 14 passed.
  • The directory baseline/finance/benefit/family/esa_income: 132 passed.
  • The directory baseline/gov/dwp/pension_credit: 320 passed.

Python (uv run pytest -q -o xfail_strict=true -n 4 <target>, 30-minute cap; none stopped), every test passed:

  • test_esa_income_pension_age_properties.py: 19 passed in 92.25 s.
  • policyengine_uk/tests/code_health: 3,093 passed in 277.74 s.

We worked case 26 from the 2016-17 parameter files (a couple minimum guarantee of £237.55 a week, disregards of £10 and £20), and the model gives the same figures: pension_credit_entitlement 392.60, pension_credit_income 11,960 (£13,000 less 52 × £20), pension_credit_employment_income_tax 0 and pension_credit 0. It pays the reported award, esa_income 5,491.20.

We ran each mutant as one change to a copy of d6bf2f657:

Mutation What fails
Guard removed from pension_credit YAML cases 18, 25 and 26 (3 failed, 23 passed)
The same, with the explicit @example((ROWS, 2016)) removed from the combined property The combined property, on a generated example: assert not ((couple_esa > 0) & (pension_credit > 0)).any() (1 failed in 118.03 s). Case 18's man, whom examples() now adds to every 2016 example, gets the award beside Pension Credit

At a1771bb2e the second mutant survived, because none of the 4 generated examples reached the fourth row.

Other checks:

  • .github/check_fix_pr.py on this title and body, as pr_fix_evidence.yaml runs it, against the changes from the main merge base (0080274d7) to d0ffe7fd3: PASS, 33 changed executable tests.
  • uvx ruff@0.17.2 format --check . (2,178 files already formatted), uvx ruff@0.17.2 check . and uvx ruff@0.17.2 check --select F811,F821 policyengine_uk: all passed. git diff --check 254669a0d..d0ffe7fd3: clean.
  • git merge-tree --write-tree --name-only d0ffe7fd3 origin/main (52b30809f): clean.

Local runs at a1771bb2e

a1771bb2e adds two commits to 71ba27523, and git merge-tree of it against main at 0080274d7 is clean. 70563bbd gives pension_credit the guard, changes case 18, adds case 25, adds invariant 10 to the combined property and updates the docstrings and the changelog fragment. a1771bb2 checks the table's outside-reporter row in that property. Each file ran alone, with no survey data.

YAML (uv run policyengine-core test <file> -c policyengine_uk), every case passed:

  • esa_income/esa_income_pension_age.yaml: 25 passed.
  • Model the 2026 income-related ESA abolition and its UC claims #2813's YAML test files: esa_income_abolition.yaml 36, esa_income_capital.yaml 10, esa_income_remunerative_work.yaml 47, housing_benefit_eligible.yaml 6, council_tax_reduction_passports.yaml 11, housing_benefit_pension_age_regulations.yaml 4, legacy_award_readers/pension_credit_earnings_disregard.yaml 4, forgoes_universal_credit_for_tax_free_childcare.yaml 10, mixed_age_couples.yaml 26 and person_legacy_benefits_closed.yaml 3.
  • Files whose cases set the couple's ESA and read Pension Credit: income_support_claimant_partner_gates.yaml 27, legacy_award_readers/passports.yaml 20, claims_universal_credit.yaml 22, housing_benefit_pension_age_means_test.yaml 5, gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml 33, relevant_benefit_receipt.yaml 40, winter_fuel_payment_charge.yaml 41, warm_home_discount.yaml 17, warm_home_discount_receipt.yaml 7, pawhp.yaml 27, scottish_child_payment.yaml 21 and winter_fuel_allowance/taxable_income_test.yaml 18.
  • Whole directories: baseline/finance/benefit 1,839 passed in 110.98 s and baseline/gov/dwp 804 passed in 49.04 s.

No existing case outside esa_income_pension_age.yaml needed a change.

Python (uv run pytest -q -o xfail_strict=true -n 4 <file>, 30-minute cap; none stopped), every test passed:

  • test_esa_income_pension_age_properties.py: 19 passed in 56.24 s.
  • test_council_tax_reduction_carer_income_properties.py 4 (167.82 s), test_council_tax_reduction_claimant_family_scheme_properties.py 3 (214.34 s), test_council_tax_reduction_pension_credit_properties.py 9 (370.53 s), test_hbai_property_income_properties.py 1 (28.27 s), test_income_support_eligibility_properties.py 4 (108.70 s), test_income_support_gate_reads_claimant_or_partner_awards.py 13 (191.23 s), test_legacy_award_readers_properties.py 18 (182.13 s), test_legacy_closure_scope.py 21 (25.66 s), test_pension_credit_earnings_disregard_properties.py 5 (309.12 s), test_targeted_childcare_guarantee_credit_properties.py 7 (123.28 s), test_trading_loss_properties.py 12 (148.58 s), test_uc_legacy_closure_properties.py 4 (104.67 s), test_wales_childcare_offer_income_sources.py 84 (10.08 s), test_winter_fuel_payment_charge_means_tests_properties.py 1 (84.17 s) and test_winter_heating_payment_properties.py 4 (51.24 s).
  • test_disable_simulated_benefits.py 49 (128.20 s), test_esa_income_abolition_properties.py 31 (60.66 s), test_esa_income_held_outside_award.py 3 (15.10 s), test_esa_income_universal_credit_claim_properties.py 2 (68.35 s), test_esa_permitted_work_nlw_reforms.py 5 (26.93 s), test_legacy_award_work_properties.py 4 (78.98 s) and test_mixed_age_couple_properties.py 1 (503.51 s).
  • policyengine_uk/tests/code_health: 3,093 passed in 176.31 s.

We ran each mutant as one change to a copy of a1771bb2e:

Mutation What fails
Guard removed from pension_credit YAML cases 18 and 25 (2 failed, 23 passed); the combined property on its explicit table example, at the new assertion: the third row, case 18's man, gets £5,491.20 of ESA beside £525.20 of Pension Credit
The same, with the explicit table example removed The 4 generated examples (1 in 2016 and 3 in 2026) pass: none reaches the fourth row
esa_income read in place of claimant_or_partner_esa_income YAML case 12 (1 failed, 24 passed); the combined property on the outside-reporter row (Pension Credit 0, expected 8,091.20)

Differential against 71ba27523: a generator outside the repository draws 480 families a year from seed 20261031 (825, 840 and 834 people in 2016, 2018 and 2026), with case 18's earnings, pensions and awards among the draws, and runs each year in its own process on each tree. At 71ba27523, 3, 1 and 0 families have both pension_credit and claimant_or_partner_esa_income above nil. Every other family gives the same pension_credit, esa_income, claimant_or_partner_esa_income, household_net_income and household_benefits on both trees. Each of the 4 families with both loses its Pension Credit and nothing else, and household net income falls by the same amount:

Year Family Couple's ESA Pension Credit, 71ba27523 → a1771bb2e Household net income, 71ba27523 → a1771bb2e
2016 99 5,075.20 525.20 → 0 15,209.40 → 14,684.20
2016 145 5,755.20 29.54 → 0 21,607.74 → 21,578.20
2016 261 7,500.00 366.60 → 0 21,895.60 → 21,529.00
2018 237 7,419.20 120.02 → 0 24,164.71 → 24,044.70

On the same populations B(0) (pension_credit_without_income_related_esa) gives the same value for every family on both trees, no branch outlives the calculation, and each tree opens the same branches: 10 in 2016 and 2018 (2 ESA branches) and 8 in 2026 (3 ESA branches). The guard reads claimant_or_partner_esa_income, which an ESA branch sets as an input, so the branches read the same variables.

Other checks:

  • .github/check_fix_pr.py on this title and body, as pr_fix_evidence.yaml runs it, against the changes from the main merge base (0080274d7) to a1771bb2e: PASS, 24 changed executable tests.
  • uvx ruff@0.17.2 format --check . (2,177 files already formatted), uvx ruff@0.17.2 check . and uvx ruff@0.17.2 check --select F811,F821 policyengine_uk: all passed. git diff --check 71ba27523..HEAD: clean.

Local runs at 71ba27523

71ba27523 rewraps one docstring of d2ce8b54a, and a comparison of the parsed code shows no other change. At 71ba27523, test_esa_income_pension_age_properties.py 19 passed in 77.67 s, and the ruff checks, git diff --check and check_fix_pr.py pass. Every other run below ran at d2ce8b54a.

71ba27523 merges #2813's head 5a5ac5fb1 and then main at 0080274d7 into 1d6df1a43, with no conflict. git merge-tree of 71ba27523 against main at 0080274d7 and against 5a5ac5fb1 is clean, since it contains both. Each file ran alone, with no survey data.

YAML (uv run policyengine-core test <file> -c policyengine_uk), every case passed:

  • esa_income/esa_income_pension_age.yaml: 24 passed. At the merge before the appointee savings (15b61ba2), cases 4, 5, 6, 12, 13 and 14 fail and 18 pass.
  • The YAML test files Model the 2026 income-related ESA abolition and its UC claims #2813 adds or changes: esa_income/esa_income_abolition.yaml 36, esa_income/esa_income_capital.yaml 10, esa_income/esa_income_remunerative_work.yaml 47, housing_benefit/housing_benefit_eligible.yaml 6, legacy_award_readers/council_tax_reduction_passports.yaml 11, legacy_award_readers/housing_benefit_pension_age_regulations.yaml 4, legacy_award_readers/pension_credit_earnings_disregard.yaml 4, universal_credit/forgoes_universal_credit_for_tax_free_childcare.yaml 10, gov/dwp/pension_credit/mixed_age_couples.yaml 26 and gov/dwp/universal_credit/person_legacy_benefits_closed.yaml 3.

Python (uv run pytest -q -o xfail_strict=true -n 4 <file>, 30-minute cap; none stopped), every test passed:

  • test_esa_income_pension_age_properties.py: 19 passed in 80.68 s.
  • test_income_support_eligibility_properties.py: 4 passed in 142.54 s.
  • test_income_support_gate_reads_claimant_or_partner_awards.py: 13 passed in 292.40 s.
  • test_disable_simulated_benefits.py: 49 passed in 171.75 s.
  • test_esa_income_abolition_properties.py: 31 passed in 115.09 s.
  • test_esa_income_held_outside_award.py: 3 passed in 20.57 s.
  • test_esa_income_universal_credit_claim_properties.py: 2 passed in 107.66 s.
  • test_esa_permitted_work_nlw_reforms.py: 5 passed in 37.92 s.
  • test_legacy_award_readers_properties.py: 18 passed in 249.39 s.
  • test_legacy_award_work_properties.py: 4 passed in 112.84 s.
  • test_legacy_closure_scope.py: 21 passed in 34.44 s.
  • test_uc_legacy_closure_properties.py: 4 passed in 147.61 s.
  • test_mixed_age_couple_properties.py: 1 passed in 895.28 s.
  • policyengine_uk/tests/code_health: 3,093 passed in 244.70 s.

The four changed property nodes, at the merge before the change: each fails as on the merged train (4 failed in 439.44 s), at line 691 ([0.0] == [5000]), line 1141 ([False, False, True, False]), legacy_award_work_reference.py:254 (a couple aged 58 and 65) and line 469. At d2ce8b54a: 4 passed in 58.75 s. In the combined property, 1 of the 4 generated examples falls in 2026.

We ran each mutant as one change to a copy of d2ce8b54a, against the four changed nodes:

Mutation Nodes that fail
s.1(3)(c) dropped from esa_income_eligible and from the own-right claim in esa_income combined, no branch, screen reads
The Pension Credit condition passes every couple combined, parameter reform
Age and take-up conditions dropped for the model's formulas no branch, screen reads, parameter reform
Early return removed screen reads

Other checks:

Earlier runs

After b6ddfcf0d, two commits change the screen's code: 09e0c0872 compares whole Pension Credit definitions, and 3fc1e1d78 checks take-up for a formula. The rest change tests, comments, the changelog fragment and the Housing Benefit file, which returns to origin/main's. On the model's own formulas and under a parameter reform, pension_credit_follows_model_formulas returns true at abdcede2d wherever it did at cef744bc2. In a fresh system, a clone and a simulation's system, each of the five variables matches a fresh instance of its class in 2016 and 2026 (30 of 30 comparisons), and would_claim_pc has no formula. Results at earlier commits below cover that unchanged path. Each ran before this branch contained #2813.

  • YAML cases from the legislation: esa_income_pension_age.yaml, 24 cases, each with its arithmetic. All 24 pass at eec9f6480 in 42.91 s. On origin/main 14 fail (cases 1, 2, 3, 7, 9, 10, 11, 13, 14, 15, 19, 21, 22 and 23) and 10 pass; on 0d157f078 cases 21, 22 and 23 fail. Cases 15 to 19 take the four pairings of B(0) and B(e) in the table above. Case 16 takes a man aged 63 in 2016 with £115.50 a week of permitted earnings and a £61 pension: he gets no Pension Credit and keeps £4,919.20 of ESA, and case 17 gives him no Pension Credit claim and the same ESA. In case 20 a couple aged 66 and 58 in 2018 has a £200 weekly pension and £124 a week of permitted earnings, and keeps £2,537.60 of support-group ESA and both benefit cap exceptions, so no Universal Credit cap applies. Cases 3 to 5 build the saving from facts: the older member's birth date, 1 March 1953, and the couple's Pension Credit report. Case 14 gives only the older member of an unsaved couple aged 70 and 60 the support component, so neither cap exception applies. Cases 21 to 24 enter Pension Credit or a part of it: £2,000 of pension_credit for a man aged 63 who would not claim (no ESA), £2,000 of pension_credit_entitlement for a man aged 40 (no ESA), is_pension_credit_eligible for a man aged 40 with no income (the £8,091.20 minimum guarantee and no ESA), and £2,000 of guarantee_credit for the man aged 63 who would not claim (no Pension Credit, so he keeps £5,000 of ESA).
  • Property and example tests: test_esa_income_pension_age_properties.py, 19 tests, all passed at eec9f6480 in 1,554.74 s. On origin/main the file stops at import (NO_INCOME_RELATED_ESA_BRANCH), and on 0d157f078 it stops at import too (pension_credit_follows_model_formulas).
    • At eec9f6480 the combined property checked invariants 1 to 5 against a second simulation of the same people in which nobody reports ESA. It drew earnings, pensions, savings and working tax credit over 2016, 2018 and 2026, in 4 generated examples of 4 to 12 families, and an explicit example added the four table cases and an outside reporter.

    • At eec9f6480 the differential (invariant 6) ran 2 generated examples and the explicit one through a reform that calculates the model's Pension Credit by a formula of its own, and compared 6 outputs with the model's.

    • Example tests for invariants 1 and 7, each with its own families:

      • £2,000 of entered Pension Credit for the man aged 63 who would claim, the same man who would not and a man aged 40: no ESA for any of them.
      • Under a reform in which every family gets £2,000 of Pension Credit: no ESA for the same three.
      • Under a reform with the mixed-age exclusion in force in 2018 and Pension Credit frozen at the baseline's award: a couple aged 70 and 60 keeps the £12,937.60 baseline award and gets no ESA.
      • Under a parameter reform without the 2026 exclusion: the same couple gets £18,889 and no ESA, and a working-age claimant alone opens no branch.
      • An input for each of the five Pension Credit variables, one at a time.
      • In a 2026 population with no couple's claim that passes the age, work and capital tests, the screen reads neither the age conditions nor the claim; add a claimant who passes, and it reads both.
    • Under each of five reforms, a family outside a Pension Credit variable's defined_for gets something other than the model gives it, in 2016:

      • £2,000 of entitlement by default: the man aged 40 gets £2,000 of Pension Credit and no ESA;
      • the same, with Pension Credit calculated by a formula of the reform's: the same results;
      • £2,000 of Pension Credit by default for a family not eligible for it: £2,000 and no ESA;
      • eligibility by default for a family that fails the age conditions: the £8,091.20 minimum guarantee and no ESA;
      • the guarantee credit and entitlement for every family with income within the minimum guarantee, a change of coverage alone: £8,091.20 and no ESA.

      In each, the man aged 63 who would not claim keeps his £5,000, and the man aged 63 who would claim gets £8,091.20 and no ESA. With the model's own five definitions registered again, the screen keeps the age and take-up conditions, and the man aged 40 keeps his £5,000.

    • Under a reform in which a family claims Pension Credit while it gets no income-related ESA, with Pension Credit by the model's formula and by a formula of the reform's: ESA [0, 5,000, 0], Pension Credit [8,091.20, 0, 8,091.20] and take-up [true, false, true], with no CycleError.

    • Exhaustive over the five variables and twelve attributes: each of seven changes to what Core calculates a variable from (default, defined_for, neutralization, formula, adds, period and end date) drops the age and take-up conditions, and each of five descriptive changes (label, documentation, reference, unit and metadata) keeps them. A take-up formula drops them; a take-up default or input keeps them.

  • Mutations of the entered-award test and the early return, each caught. Each mutant changes one line or block in a copy of a commit from b6ddfcf0d to cef744bc2, which share the screen's code, and runs against the tests the table names:
Mutation YAML cases that fail Python tests that fail
Age and take-up conditions kept for entered and reformed Pension Credit 21, 22, 23 entered award, reform for all
Age and take-up conditions dropped for the model's formulas too not run parameter reform, no branch
Inputs not counted 21, 23 entered award, each entered part
Replaced formulas not counted not run reform for all
Freeze not checked not run frozen award
Entitlement not checked 22 each entered part
is_pension_credit_eligible not checked 23 each entered part
Guarantee and savings credits not checked not run each entered part
Early return removed not run screen reads
Early return keyed to any claim, an outside member's included not run screen reads
Entered Pension Credit read only for a couple that would claim 21 entered award
Entered Pension Credit read only for a couple that meets the age conditions 22, 23 entered award
  • Mutations of the definition and take-up checks, each caught. Each mutant changes one line of an export of abdcede2d, and runs at a 2 GB cap against the tests the table names. The entitlement row ran on eec9f6480, whose tests match abdcede2d's and whose code differs only in docstrings:
Mutation Python tests that fail, and how
Default not compared entitlement default (ESA [5,000, 5,000, 0]); every change
defined_for not compared guarantee credit coverage (ESA [5,000, 5,000, 0]); every change
Take-up formula not checked take-up formula (CycleError); every change
Entitlement left out of the compared variables entitlement default (ESA [5,000, 5,000, 0]); every change
Inputs not counted entered award (ESA [0, 5,000, 5,000]); each entered part
Formula not compared reform for all (ESA [0, 5,000, 5,000]); every change
  • 1d6df1a43 rewords three docstrings from abdcede2d. A comparison of the parsed code shows no other change, so the runs named at abdcede2d hold for it.
  • A matrix of 23 input and reform cases at abdcede2d, run against the same fixtures as at cef744bc2. The three default reforms give the man aged 40 no ESA (ESA [5,000, 0, 0], where cef744bc2 paid [5,000, 5,000, 0]), and take-up that reads ESA gives ESA [0, 5,000, 0] and Pension Credit [8,091.20, 0, 8,091.20], where cef744bc2 raised CycleError. Two take-up formulas that return a constant now drop the age and take-up conditions and give the same awards. The other 16 cases give the same ESA, Pension Credit and shortcut as at cef744bc2, among them the 2026 parameter reform (ESA [0], Pension Credit [18,889], age and take-up conditions kept) and the entered and replaced Pension Credit cases. In a population of the man aged 40 alone, the entitlement default reform gives him no Pension Credit, so he keeps his £5,000.
  • The two-copy entered-award check at abdcede2d: two men aged 63 with £2,000 of entered Pension Credit and opposite take-up get ESA [0, 0] and Pension Credit [2,000, 2,000] (1 passed in 217.22 s), and its YAML form passes (1 passed in 25.30 s).
  • Hand-built datasets through the aggregate-impact guard at abdcede2d: a household already paid Pension Credit and three families with none give the same 92 baseline fiscal heads, perturbations, changed heads and counted-once verdicts as at cef744bc2, and the same baseline heads and verdicts as at 0d157f078. Pension Credit passes the counted-once check, and resetting the baseline reproduces it. In the second fixture the synthetic £1 of Pension Credit also takes the middle family's £5,000 of ESA (ESA change [0, -5,000, 0]).
  • Mutations of the bar at d37e42de9, each caught:
Mutation YAML cases that fail Property tests that fail
Bar on the age conditions and the claim alone 16, 18, 20 combined, branch
Bar on Pension Credit with the award, B(e) 18, 19 combined, branch
Bar on entitlement without the claim 5, 8 combined
Bar reaches a member outside the couple 12 combined, no-branch
Branch left in simulation.branches none branch
Branch opens whatever the population none no-branch
Both benefit cap payee masks removed 14 none
s.1(3)(c) dropped from the screen 9, 11 stopped at the 1,500 s limit
is_on_income_related_esa as on origin/main 11, 13, 14 stopped at the 1,500 s limit
  • Differential against origin/main (invariant 8), at d37e42de9: 480 synthetic families a year (785, 794 and 810 people) in 2016, 2018 and 2026, from seed 20261031, through both trees. All 243, 262 and 303 families to which the model applies neither new refusal give the same values as origin/main on 16 benefit-unit outputs, is_on_income_related_esa, household_net_income and household_benefits. No family gains income-related ESA. Some families have an open couple report, the Pension Credit age conditions and a claim, and no report from anyone past pensionable age: 23, 9 and 3 of them. Of these, 5, 2 and 0 lose the couple's award, 11, 2 and 0 keep it at origin/main's amount, and 1, 0 and 0 get both awards. On origin/main 49, 35 and 36 families got both. The population in the cost section gives the same 24 outputs at b6ddfcf0d as at 0d157f078 in all five years.
  • Existing YAML files whose cases report ESA and enter Pension Credit or a part of it, each alone at 2ee68e799: 232 cases, all passed (relevant_benefit_receipt.yaml 40, warm_home_discount.yaml 17, pawhp.yaml 27, warm_home_discount_receipt.yaml 7, housing_benefit_pension_age_means_test.yaml 5, legacy_award_readers/passports.yaml 20, winter_fuel_allowance/taxable_income_test.yaml 18, housing_benefit_specified_or_temporary_accommodation_disregard.yaml 50, housing_benefit/housing_benefit_income_related_benefit_passport.yaml 7 and winter_fuel_payment_charge.yaml 41). At d37e42de9, baseline/finance/benefit 1,793 passed and baseline/gov/dwp 804 passed.
  • Python files that read the input register, at 2ee68e799: aggregate_impact/test_aggregate_impact_guard.py 39 passed and test_input_records.py 11 passed.
  • Python files that read the screen, each alone with a 3,000 s limit, one at a time, at b6ddfcf0d. None failed; CI runs each in full:
    • test_income_support_gate_reads_claimant_or_partner_awards.py: 7 tests passed, then the limit.
    • test_income_support_eligibility_properties.py: 1 test passed, then the limit.
    • test_income_support_esa_entered_directly.py: 18 tests passed, then the limit.
    • test_income_support_direct_inputs.py: 17 passed in 1,801 s.
    • test_legacy_award_work_properties.py: 1 test passed in the 24 minutes we ran it; CI must complete this file.
    • test_esa_income_universal_credit_claim_properties.py: 2 passed in 2,410.51 s at cef744bc2, with a 3,000 s limit.
  • At e7f5b0a58 and d37e42de9: test_claimant_or_partner_awards_entered_directly.py 25 passed, test_entered_directly_lifecycle.py 16, test_reported_legacy_award_screening.py 18 and test_esa_permitted_work_nlw_reforms.py 5.
  • ruff format --check, ruff check and ruff check --select F811,F821 policyengine_uk on the changed Python files, and git diff --check.
  • git merge-tree --write-tree --name-only at eec9f6480 against origin/main (2da212a), Value means-tested capital less 10% sale expenses and secured debt #1969 (3a3241c), Derive economic assumption index horizon from the growth series #2756 (c346627), Classify the Budget helper variables in the fiscal partition #2763 (6ca062e), Disregard WTC before the Housing Benefit childcare deduction #2776 (1d4a3e4) and the Abolish income-related ESA from 1 July 2026 (NI 1 October 2026) #2562 branch (68ccc08): no conflict.
  • Changelog fragment changelog.d/2565.fixed.md.

The shared test reference takes the couple's entitlement to Pension Credit: legacy_award_work_reference.esa_screen gains an on_pension_credit argument. The Income Support property tests pass it the couple's claim and the model's pension_credit, and their own s.124(1)(g) reading keeps the age conditions and the claim; the gate test reads Pension Credit only for its ESA reference. No YAML expected value outside the new file changed.

Reproducer

Reproducer: policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_pension_age.yaml; policyengine_uk/tests/test_esa_income_pension_age_properties.py::test_couple_esa_follows_pension_credit_worked_out_without_it; policyengine_uk/tests/test_esa_income_pension_age_properties.py::test_an_entered_pension_credit_award_bars_the_award_whatever_the_age_and_take_up; policyengine_uk/tests/test_esa_income_pension_age_properties.py::test_a_reform_that_pays_outside_the_age_conditions_bars_the_award; policyengine_uk/tests/test_esa_income_pension_age_properties.py::test_a_take_up_formula_makes_the_model_read_pension_credit_for_every_couple

axiom: TheAxiomFoundation/rulespec-uk#515 queued

🤖 Generated with Claude Code

MaxGhenis and others added 22 commits October 31, 2026 06:04
Welfare Reform Act 2007 s.1(3)(c) bars ESA for a claimant who has reached
pensionable age, and Sch 1 para 6(1)(c) and (d) bar the income-related
allowance where the claimant, or the other member of the couple, is
entitled to State Pension Credit. esa_income_eligible now applies both.
Pension Credit reads the couple's ESA, so the screen takes the couple to be
on Pension Credit where it meets the age conditions and would claim it, as
income_support_eligible does for SSCBA 1992 s.124(1)(g). An outside
member's own claim, and the payee of the couple's award, follow s.1(3)(c).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Property tests: no family gets both awards, nobody over pensionable age is
on income-related ESA, the screen matches the reference reading, and
claiming Pension Credit never raises income-related ESA.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sch 1 para 6(1)(c) and (d) bar the couple's income-related ESA on
entitlement to State Pension Credit, which age and the claim alone do not
establish. esa_income_eligible now reads pension_credit in a branch with no
income-related ESA in the family (pension_credit_without_income_related_esa)
and bars the couple's claim where that pays. The branch opens only when
some couple has an open report, meets the Pension Credit age conditions and
would claim.

The YAML cases now cover each pairing of Pension Credit with and without
the award, Sol's 2016 and 2018 witnesses, a saved couple built from facts
(an older member born 1 March 1953) and the asymmetric cap payee.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The property test now builds, for each example, a second simulation of the
same people with no income-related ESA reported, whose Pension Credit is
the bar's B(0) with no branch. Where that pays nothing, take-up leaves the
couple's ESA alone; where it pays, the couple's ESA is nil and Pension
Credit equals it. The generator now draws earnings, pensions, savings and,
in tax-credit years, working tax credit. Two tests count the branches: none
opens without a couple that could be on Pension Credit, none outlives the
calculation, and the passport branch inside it reads the award as nil.

The four property tests built a fresh tax-benefit system per example (about
a minute each under load); one combined test on the cloned system builds
two simulations per example and five examples.

The shared reference now accepts tax credit reports in years with tax
credits, and the Income Support tests pass the ESA screen entitlement (the
couple would claim and Pension Credit pays) in place of the proxy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pensions Act 1995 Sch 4 para 1 gives 66 to a person born from 6 October
1954 to 5 April 1960 under rule 6, 66 and some months under rule 7 and 67
under rule 8. The changelog now states the bar on Pension Credit worked
out without the award.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bar can change the screen only for a couple whose claim passes the age,
work and capital tests, so the branch now opens only where such a couple
meets the Pension Credit age conditions and would claim. Each family's
result is unchanged. The Universal Credit modifier reads ESA for later
years when the simulation is built, and in those years many reporters have
reached pensionable age; the branch no longer opens for them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments, docstrings, the YAML header and the changelog now say what the
model does in active voice. No formula changes.

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

The income-related ESA screen inferred that Pension Credit could not pay a
couple that failed the Pension Credit age conditions or would not claim. That
holds only for the model's own formulas. Where a caller enters pension_credit,
its entitlement, its eligibility or either credit, a reform replaces one of
their formulas or changes the entitlement's sum, or the reform freezes
Pension Credit at the baseline's award, the screen now reads Pension Credit
without the award for every couple whose claim passes the other tests
(pension_credit_follows_model_formulas). A parameter reform keeps the age and
take-up conditions.

The screen also returns before it reads any Pension Credit condition when no
couple's claim passes the age, work and capital tests, and the Income Support
gate test reads Pension Credit only for its ESA reference.

replaces_formula, shared with Housing Benefit's hb_uses_annual_override, tells
an input or a replaced formula from the model's calculation; the five Pension
Credit variables join SUPPLIED_INPUT_VARIABLES so that it can ask about them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The open fix-2730-hb-wtc-disregard branch rewrites hb_uses_annual_override,
so delegating it to replaces_formula here would conflict with that branch.
replaces_formula makes the same test for the ESA screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments and docstrings no longer make a statute bar, exclude or lift: the
claimant fails or meets a condition, DWP disregards earnings and HMRC pays
working tax credit. The changelog fragment covers entered and reformed
Pension Credit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The branch that works out Pension Credit without employment income gives a
claimant in paid work no earnings, so the screen can read the Pension Credit
conditions there for a family whose claim fails the work test in the
simulation itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The model also runs the screen for 2027 to 2032 in that population, where
the situation enters no earnings or hours, so the claimant in paid work can
pass the work test there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pension_credit_follows_model_formulas now asks replaces_definition about all
five Pension Credit variables. It compares every attribute Core calculates a
variable from (formula for the period, default, defined_for, adds and
subtracts, period, neutralization, end) with the model's own class, and
counts an input for the period. A reform that keeps the formula but gives a
family outside defined_for a default of £2,000 now drops the age and take-up
conditions, so the man aged 40 who would get that £2,000 gets no ESA.

Tests: the four reforms (entitlement default, entitlement default with
another payment formula, payment default, eligibility default), a reform that
changes only the guarantee credit's coverage, the model's own definitions
registered again (shortcut kept), and an exhaustive change of each attribute
of each variable. The parameter reform test now also asserts the shortcut on
the lifted couple.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The screen read would_claim_pc in the simulation itself to skip couples that
would not claim. A reform that gives take-up a formula reading the family's
ESA then closed esa_income -> esa_income_eligible -> would_claim_pc ->
esa_income. pension_credit_follows_model_formulas now returns false when
would_claim_pc has a formula for the period, so the screen reads Pension
Credit without the award for every couple whose claim passes the other
tests. Supplied take-up and parameter reforms keep the shortcut.

Tests: the take-up formula and its control with Pension Credit by another
formula (ESA [0, 5000, 0], Pension Credit [8091.20, 0, 8091.20]), and the
exhaustive guard test's take-up rows (formula drops the shortcut; default
and input keep it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DWP applies the higher earnings disregard while the claimant or partner
receives ESA (SPC Regs 2002 Sch VI para 4(1)(a)(vii)); a member outside the
couple keeps their own claim; the 40-year-old claims in their own right;
nobody owes income tax or primary Class 1 contributions in cases 13 and 20.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mutant that keeps the shortcut then fails on the award it pays, or on the
cycle it closes, rather than on the flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test looped over the model's own list, so a variable dropped from that
list dropped out of the test too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The generators draw tax-credit reports; Core reads a branch's inputs from its
parent branches; the model pays the reported award and applies no Universal
Credit cap in case 20.

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

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Aggregate impact on certified data: pass

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

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

Workflow run and full report

Notes

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

Moves flagged for review

None.

Spending by benefit and service

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

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

Household net income by decile

No household's net income changes in any year.

No change in any year: receipts by tax head; government balance, each component counted once; model totals; household income totals; population; poverty rates; people in poverty; inequality; mean household net income in each side's own deciles; standard reform costings (change in gov_balance); standard reforms: change in each reform's target.

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

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

  • widowed_parents_allowance: pension_credit responds only through other heads in fewer than 10 households (other heads that moved there: carers_allowance; gov_spending; not a failure)
  • widowed_parents_allowance: pension_credit responds only through other heads in fewer than 10 households (other heads that moved there: carers_allowance; household_benefits; not a failure)
Provenance
Side Commit Runtime Environment hash Partition hash Time
base main b8b383cde7 uk 2.176.3 (files ac13f82082c7), core 3.34.1, py 3.13.18 aarch64 ea9029c99333 3bcfb3473960 2555.35s
head #2807 merged into main d12c2a232b uk 2.176.3 (files 3655ea4c9f35), core 3.34.1, py 3.13.18 aarch64 ea9029c99333 3bcfb3473960 2532.58s

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.46bn £315.23bn £335.82bn £351.28bn £366.96bn £385.36bn £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.48bn £132.98bn £136.65bn £140.41bn
Pension Credit £6.21bn £6.46bn £6.68bn £6.82bn £6.89bn £7.07bn £7.22bn
Winter Fuel Payment £1.86bn £1.85bn £1.79bn £1.77bn £1.78bn £1.78bn £1.78bn
Pension Age Winter Heating Payment (Scotland) £0.19bn £0.20bn £0.20bn £0.21bn £0.21bn £0.22bn £0.22bn
Universal Credit £75.70bn £81.81bn £84.29bn £86.34bn £87.51bn £89.06bn £89.70bn
Housing Benefit £8.57bn £8.52bn £8.85bn £9.06bn £9.29bn £9.57bn £9.84bn
Council Tax Reduction £2.19bn £2.32bn £2.44bn £2.58bn £2.72bn £2.87bn £3.02bn
Employment and Support Allowance (income-related) £0.79bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Employment and Support Allowance (contribution-based) £5.20bn £5.37bn £5.56bn £5.69bn £5.82bn £5.95bn £6.09bn
Jobseeker's Allowance (income-based) £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Jobseeker's Allowance (contribution-based) £0.29bn £0.30bn £0.31bn £0.31bn £0.32bn £0.33bn £0.34bn
Income Support £0.11bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Working Tax Credit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Child Tax Credit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Incapacity Benefit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Personal Independence Payment £26.95bn £28.03bn £29.05bn £30.09bn £30.75bn £31.51bn £32.21bn
Disability Living Allowance £7.49bn £7.79bn £8.07bn £8.36bn £8.55bn £8.76bn £8.95bn
Attendance Allowance £8.68bn £9.03bn £9.36bn £9.69bn £9.90bn £10.15bn £10.38bn
Carer's Allowance £4.48bn £4.66bn £4.83bn £5.00bn £5.11bn £5.27bn £5.42bn
Carer Support Payment (Scotland) £0.31bn £0.32bn £0.33bn £0.34bn £0.35bn £0.36bn £0.37bn
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,489k 1,484k 1,476k 1,454k 1,452k 1,457k 1,466k
Winter Fuel Payment 7,844k 7,783k 7,473k 7,375k 7,391k 7,409k 7,427k
Pension Age Winter Heating Payment (Scotland) 803k 802k 800k 797k 799k 801k 802k
Universal Credit 6,343k 6,419k 6,408k 6,369k 6,364k 6,343k 6,295k
Housing Benefit 1,249k 1,182k 1,181k 1,165k 1,164k 1,166k 1,168k
Council Tax Reduction 3,326k 3,344k 3,351k 3,364k 3,376k 3,388k 3,395k
Employment and Support Allowance (income-related) 71k 0k 0k 0k 0k 0k 0k
Employment and Support Allowance (contribution-based) 719k 720k 721k 722k 724k 726k 728k
Jobseeker's Allowance (income-based) 0k 0k 0k 0k 0k 0k 0k
Jobseeker's Allowance (contribution-based) 67k 67k 67k 67k 67k 68k 68k
Income Support 25k 0k 0k 0k 0k 0k 0k
Working Tax Credit 0k 0k 0k 0k 0k 0k 0k
Child Tax Credit 0k 0k 0k 0k 0k 0k 0k
Incapacity Benefit 0k 0k 0k 0k 0k 0k 0k
Personal Independence Payment 3,567k 3,575k 3,579k 3,585k 3,593k 3,602k 3,611k
Disability Living Allowance 1,291k 1,294k 1,296k 1,298k 1,300k 1,304k 1,307k
Attendance Allowance 1,695k 1,698k 1,700k 1,703k 1,707k 1,711k 1,715k
Carer's Allowance 1,034k 1,037k 1,038k 1,040k 1,042k 1,060k 1,062k
Carer Support Payment (Scotland) 71k 71k 71k 72k 72k 72k 72k
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 £892.00bn £934.66bn £980.50bn £1,015.93bn £1,054.24bn £1,091.56bn £1,124.89bn
Government spending £529.29bn † £545.32bn † £550.39bn † £559.10bn † £566.74bn † £575.45bn † £583.06bn †
Government balance, each receipt once £362.71bn † £389.34bn † £430.11bn † £456.82bn † £487.50bn † £516.10bn † £541.83bn †
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 £892.00bn £934.66bn £980.50bn £1,015.93bn £1,054.24bn £1,091.56bn £1,124.89bn
household_tax £491.97bn £520.41bn £551.21bn £576.00bn £601.00bn £626.69bn £648.42bn
pre_budget_change_household_tax £484.15bn £512.28bn £542.82bn £566.95bn £591.72bn £617.16bn £638.61bn
gov_spending £529.29bn † £545.32bn † £550.39bn † £559.10bn † £566.74bn † £575.45bn † £583.06bn †
household_benefits £529.29bn † £545.32bn † £550.39bn † £559.10bn † £566.74bn † £575.45bn † £583.06bn †
gov_balance £362.71bn † £389.34bn † £430.11bn † £456.82bn † £487.50bn † £516.10bn † £541.83bn †
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.60bn † £1,687.48bn † £1,715.08bn † £1,747.09bn † £1,778.23bn † £1,816.52bn † £1,859.15bn †
Household net income (HBAI definition) £1,449.48bn £1,499.49bn £1,530.49bn £1,564.77bn £1,597.43bn £1,635.10bn £1,676.08bn
Population: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
People 69,245k 69,383k 69,473k 69,592k 69,738k 69,912k 70,080k
Benefit units 35,110k 35,181k 35,226k 35,286k 35,360k 35,449k 35,534k
Households 31,046k 31,108k 31,149k 31,201k 31,267k 31,345k 31,420k
Poverty rates: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Absolute poverty, before housing costs: everyone 16.2% 15.4% 16.3% 16.2% 16.4% 16.4% 16.5%
Absolute poverty, before housing costs: children 24.6% 21.9% 23.1% 22.7% 22.9% 23.0% 23.2%
Absolute poverty, before housing costs: working-age adults 14.0% 13.7% 14.6% 14.6% 14.7% 14.7% 14.9%
Absolute poverty, before housing costs: pensioners 13.7% 13.4% 14.0% 13.9% 14.0% 14.0% 13.9%
Absolute poverty, after housing costs: everyone 21.5% 21.0% 22.0% 22.5% 22.8% 23.0% 23.4%
Absolute poverty, after housing costs: children 33.1% 31.2% 32.1% 32.4% 32.8% 33.2% 34.0%
Absolute poverty, after housing costs: working-age adults 19.9% 19.8% 20.8% 21.5% 21.8% 22.0% 22.2%
Absolute poverty, after housing costs: pensioners 12.7% 12.4% 13.2% 13.2% 13.3% 13.4% 13.6%
Relative poverty, before housing costs: everyone 16.9% 15.9% 16.3% 16.2% 16.3% 16.2% 16.5%
Relative poverty, before housing costs: children 25.8% 22.6% 23.1% 22.7% 22.8% 22.6% 23.2%
Relative poverty, before housing costs: working-age adults 14.4% 14.0% 14.6% 14.6% 14.7% 14.6% 14.9%
Relative poverty, before housing costs: pensioners 14.5% 14.1% 14.0% 13.8% 13.8% 13.9% 13.9%
Relative poverty, after housing costs: everyone 21.4% 20.9% 21.2% 21.3% 21.3% 21.5% 21.7%
Relative poverty, after housing costs: children 32.9% 31.0% 31.1% 31.0% 31.1% 31.2% 31.7%
Relative poverty, after housing costs: working-age adults 19.8% 19.7% 20.2% 20.4% 20.5% 20.6% 20.8%
Relative poverty, after housing costs: pensioners 12.5% 12.4% 12.2% 11.8% 11.7% 11.8% 12.1%
People in poverty: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Absolute poverty, before housing costs: everyone 11,251k 10,708k 11,337k 11,302k 11,412k 11,452k 11,588k
Absolute poverty, before housing costs: children 3,677k 3,272k 3,456k 3,407k 3,443k 3,465k 3,511k
Absolute poverty, before housing costs: working-age adults 5,950k 5,860k 6,279k 6,333k 6,390k 6,402k 6,493k
Absolute poverty, before housing costs: pensioners 1,624k 1,576k 1,602k 1,562k 1,579k 1,585k 1,584k
Absolute poverty, after housing costs: everyone 14,871k 14,567k 15,265k 15,691k 15,911k 16,096k 16,374k
Absolute poverty, after housing costs: children 4,941k 4,671k 4,815k 4,873k 4,932k 5,015k 5,146k
Absolute poverty, after housing costs: working-age adults 8,426k 8,436k 8,945k 9,325k 9,475k 9,560k 9,684k
Absolute poverty, after housing costs: pensioners 1,504k 1,460k 1,505k 1,493k 1,505k 1,520k 1,545k
Relative poverty, before housing costs: everyone 11,698k 11,036k 11,323k 11,301k 11,352k 11,354k 11,588k
Relative poverty, before housing costs: children 3,849k 3,391k 3,456k 3,407k 3,429k 3,416k 3,511k
Relative poverty, before housing costs: working-age adults 6,131k 5,993k 6,272k 6,333k 6,359k 6,362k 6,493k
Relative poverty, before housing costs: pensioners 1,719k 1,652k 1,594k 1,561k 1,565k 1,576k 1,584k
Relative poverty, after housing costs: everyone 14,806k 14,520k 14,761k 14,796k 14,871k 15,007k 15,219k
Relative poverty, after housing costs: children 4,918k 4,649k 4,663k 4,653k 4,675k 4,703k 4,798k
Relative poverty, after housing costs: working-age adults 8,399k 8,415k 8,702k 8,816k 8,873k 8,962k 9,052k
Relative poverty, after housing costs: pensioners 1,488k 1,455k 1,395k 1,326k 1,324k 1,342k 1,369k
Inequality: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Gini coefficient, equivalised HBAI income before housing costs 0.3076 0.3049 0.3055 0.3054 0.3059 0.3064 0.3080
Mean household net income in each side's own deciles: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Decile 1: mean household_net_income per household £17,747 † £17,980 † £17,922 † £18,176 † £18,313 † £18,532 † £18,737 †
Decile 2: mean household_net_income per household £28,048 † £29,038 † £28,756 † £28,999 † £29,261 † £29,677 † £30,174 †
Decile 3: mean household_net_income per household £34,219 † £35,425 † £36,052 † £36,547 † £37,155 † £37,773 † £38,102 †
Decile 4: mean household_net_income per household £38,925 † £40,769 † £41,728 † £42,763 † £43,387 † £44,168 † £44,499 †
Decile 5: mean household_net_income per household £43,346 † £45,603 † £46,054 † £47,001 † £47,755 † £48,626 † £50,309 †
Decile 6: mean household_net_income per household £47,936 † £49,179 † £50,883 † £52,036 † £52,926 † £53,947 † £54,723 †
Decile 7: mean household_net_income per household £55,669 † £56,646 † £57,489 † £57,654 † £58,788 † £59,702 † £61,102 †
Decile 8: mean household_net_income per household £68,785 † £70,807 † £71,868 † £73,507 † £73,888 † £75,324 † £77,179 †
Decile 9: mean household_net_income per household £78,889 † £81,012 † £80,859 † £82,645 † £85,463 † £86,945 † £89,508 †
Decile 10: mean household_net_income per household £116,861 † £120,160 † £124,545 † £126,419 † £125,752 † £128,367 † £130,693 †
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.48bn
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.93bn £-3.99bn £-4.05bn £-4.11bn £-4.24bn £-1.95bn
Raise child benefit by 25GBP/week per additional child £-1.99bn £-1.75bn £-1.55bn £-1.38bn £-1.30bn £-1.18bn £-1.07bn
Reduce Universal Credit taper rate to 20% £-20.83bn £-21.38bn £-21.81bn £-22.02bn £-22.37bn £-22.77bn £-22.96bn
Raise Class 1 main employee NICs rate to 10% £10.89bn £11.30bn £11.71bn £12.01bn £12.31bn £12.62bn £13.00bn
Raise VAT standard rate by 2pp £24.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 £3.85bn £4.14bn
Raise the Universal Credit standard allowance by 10% £-3.06bn £-3.23bn £-3.30bn £-3.40bn £-3.48bn £-3.53bn £-3.55bn
Raise the State Pension by 10% £-7.76bn £-7.99bn £-8.22bn £-8.44bn £-8.79bn £-8.52bn £-8.86bn
Standard reforms: change in each reform's target: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise basic rate by 1pp: change in income_tax £6.55bn £6.86bn £6.79bn £7.00bn £7.20bn £7.44bn £7.66bn
Raise higher rate by 1pp: change in income_tax £3.61bn £3.92bn £4.01bn £4.23bn £4.45bn £4.72bn £4.84bn
Raise personal allowance by ~800GBP/year: change in income_tax £-3.93bn £-4.07bn £-4.13bn £-4.18bn £-4.22bn £-4.40bn £-1.75bn
Raise child benefit by 25GBP/week per additional child: change in child_benefit £2.33bn £2.14bn £1.94bn £1.76bn £1.65bn £1.52bn £1.40bn
Reduce Universal Credit taper rate to 20%: change in universal_credit £20.79bn £21.36bn £21.84bn £22.07bn £22.39bn £22.80bn £23.01bn
Raise Class 1 main employee NICs rate to 10%: change in national_insurance £11.15bn £11.63bn £12.00bn £12.29bn £12.61bn £12.94bn £13.31bn
Raise VAT standard rate by 2pp: change in vat £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 £3.86bn £4.12bn
Raise the Universal Credit standard allowance by 10%: change in universal_credit £3.06bn £3.22bn £3.32bn £3.41bn £3.48bn £3.54bn £3.55bn
Raise the State Pension by 10%: change in state_pension £10.68bn £11.17bn £11.28bn £11.60bn £12.03bn £12.46bn £12.96bn

@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 1d6df1a43cfbf7c445d16def69181a02797b088b. One blocker remains.

C1 — Do not make simultaneous Pension Credit and income-related ESA an accepted result. The new gate checks Pension Credit without ESA, B(0). In the new YAML case18, B(0) is zero, but the ESA-related earnings disregard makes actual Pension Credit entitlement positive. Replaying that case without forcing either entitlement returns £5,491.20 ESA and £525.20 Pension Credit, with household net income £15,625.40. WRA2007 Sch1 para6(1)(c)/(d) bars ESA on actual claimant/partner Pension Credit entitlement. The new gate and acceptance oracle therefore leave the overlap this PR targets unresolved.

Please resolve this state using source-supported claim/history treatment and update case18 and its property contract to require mutually exclusive awards. The continued earnings-disregard rule has prior-award/employment conditions, so the evidence here does not establish which single award should take priority.

The restricted comparison substituting the merge-base ESA eligibility gate gives the same outputs. This is an incomplete targeted fix and a newly codified incorrect oracle; the reproduction does not establish a new monetary regression. Relevant locations: esa_income_eligible.py:244–249 and esa_income_pension_age.yaml:594–603.

41 focused checks passed, including all24 new YAML cases, input-record/branch/definition controls and the diagnostic confirming this overlap. Exact-head CI is green; the passing overlap expectation needs correction. Review COMPLETE, one confirmed finding.

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

We've moved this back to draft. Each PR passes on its own, but with #2813 also merged, ten of this PR's tests fail: six cases in esa_income_pension_age.yaml and four properties in test_esa_income_pension_age_properties.py. #2813 models the abolition of income-related ESA from 1 July 2026, and these cases run in 2026. #2813 can land as it is. We're stacking this branch on #2813 with the test fixes, so once we mark it ready it can land after #2813 or bring #2813 with it.

@MaxGhenis
MaxGhenis marked this pull request as draft November 2, 2026 20:26
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Picking this up for the UK hub as brief 1055, which continues brief 1037's branch. Reply here if you're mid-change and I'll stop.


Generated by Claude Code

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

Copy link
Copy Markdown
Collaborator Author

PR_TITLE.txt

Bar income-related ESA at pensionable age and on Pension Credit

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

AXIOM_515.md

## Why

PolicyEngine/policyengine-uk#2565 (branch `fix-2565-esa-pension-credit`) applies Welfare Reform Act 2007 s.1(3)(c) and Sch 1 para 6(1)(c) and (d) to income-related ESA. Under s.1(3)(c) no claimant who has reached pensionable age gets ESA. Under para 6(1)(c) and (d) no claimant gets the income-related allowance while they, or the other member of their couple, have an entitlement to State Pension Credit. rulespec-uk has no module for the Welfare Reform Act 2007 (c. 5); for the ESA Regulations it has `uk/regulations/uksi/2008/794/118.yaml`. Encode the modules below to give the PolicyEngine change its Axiom counterpart.

## Modules

- `uk/statutes/ukpga/2007/5/1.yaml`, with `corpus_citation_path: uk/statute/ukpga/2007/5/1`: s.1(3)(c) and the s.1(6) definition of pensionable age.
- `uk/statutes/ukpga/2007/5/schedule/1/paragraph/6.yaml`, with `corpus_citation_path: uk/statute/ukpga/2007/5/schedule/1/paragraph/6`: para 6(1)(c) and (d).

No statute in the repository has a schedule module yet. These paths join the statute section form (`uk/statutes/ukpga/2002/16/1.yaml`, `corpus_citation_path: uk/statute/ukpga/2002/16/1`) with the regulation schedule form (`uk/regulations/ssi/2012/319/schedule/1/paragraph/2.yaml`, `corpus_citation_path: uk/regulation/ssi/2012/319/schedule/1/paragraph/2`).

The modules read:

- pensionable age, from the Pensions Act 1995 Sch 4 para 1 (`uk/statute/ukpga/1995/26/schedule/4/paragraph/1`);
- entitlement to State Pension Credit, which `uk/statutes/ukpga/2002/16/1.yaml` lists among its deferred outputs (`uk:statutes/ukpga/2002/16/1#state_pension_credit_entitlement`);
- the exclusion of mixed-age couples, SPCA 2002 s.4(1A) (`uk/statute/ukpga/2002/16/4`), and its saving, SI 2019/37 art 4 (`uk/regulation/uksi/2019/37/article/4`).

## Northern Ireland

The Northern Ireland Assembly enacted s.1(3)(c) and Sch 1 para 6(1) word for word in the Welfare Reform Act (Northern Ireland) 2007 (c. 2), and changed two provisions. In s.1(3)(d) the claimant must be "in Northern Ireland", where Parliament wrote "in Great Britain". In s.1(6) the Assembly took pensionable age from the Pensions (Northern Ireland) Order 1995 Sch 2 para 1, where Parliament took it from the Pensions Act 1995 Sch 4 para 1. A person born on a given date reaches pensionable age on the same day under either schedule: their nine rules and four tables of birth dates and pensionable-age dates, 77 rows in all, match line for line. Northern Ireland has the same exclusion of mixed-age couples in the State Pension Credit Act (Northern Ireland) 2002 s.4(1A) and the same saving in SR 2019/4 art 4(1). rulespec-uk has no Northern Ireland modules yet: encode them beside the GB modules, or record that the GB rule covers Northern Ireland.

## Corpus citations

- `uk/statute/ukpga/2007/5/1`: https://www.legislation.gov.uk/ukpga/2007/5/section/1
- `uk/statute/ukpga/2007/5/schedule/1/paragraph/6`: https://www.legislation.gov.uk/ukpga/2007/5/schedule/1/paragraph/6
- `uk/statute/ukpga/1995/26/schedule/4/paragraph/1`: https://www.legislation.gov.uk/ukpga/1995/26/schedule/4/paragraph/1
- `uk/statute/ukpga/2002/16/4`: https://www.legislation.gov.uk/ukpga/2002/16/section/4
- `uk/regulation/uksi/2019/37/article/4`: https://www.legislation.gov.uk/uksi/2019/37/article/4
- `uk/regulation/uksi/2002/1792/schedule/VI/paragraph/4`: https://www.legislation.gov.uk/uksi/2002/1792/schedule/VI/paragraph/4
- `uk/regulation/uksi/2002/1792/schedule/VI/paragraph/5`: https://www.legislation.gov.uk/uksi/2002/1792/schedule/VI/paragraph/5
- `uk/regulation/uksi/2008/794/schedule/7`: https://www.legislation.gov.uk/uksi/2008/794/schedule/7
- `uk/regulation/uksi/2008/794/regulation/45`: https://www.legislation.gov.uk/uksi/2008/794/regulation/45
- Northern Ireland: https://www.legislation.gov.uk/nia/2007/2/section/1, https://www.legislation.gov.uk/nia/2007/2/schedule/1/paragraph/6, https://www.legislation.gov.uk/nisi/1995/3213/schedule/2/paragraph/1, https://www.legislation.gov.uk/nia/2002/14/section/4, https://www.legislation.gov.uk/nisr/2019/4/article/4

## Verbatim law

Welfare Reform Act 2007 s.1, current text:

> (2) Subject to the provisions of this Part, a claimant is entitled to an employment and support allowance if he satisfies the basic conditions and either— (a) the first and the second conditions set out in Part 1 of Schedule 1 (conditions relating to national insurance) or the third condition set out in that Part of that Schedule (condition relating to youth), or (b) the conditions set out in Part 2 of that Schedule (conditions relating to financial position).
>
> (3) The basic conditions are that the claimant— (a) has limited capability for work, (aa) has accepted a claimant commitment, (b) is at least 16 years old, (c) has not reached pensionable age, (d) is in Great Britain, and (e) is not entitled to income support, and (f) is not entitled to a jobseeker's allowance (and is not a member of a couple who are entitled to a joint-claim jobseeker's allowance).
>
> (6) In subsection (3)— ... "pensionable age" has the meaning given by the rules in paragraph 1 of Schedule 4 to the Pensions Act 1995 (c. 26).

Welfare Reform Act 2007 Sch 1 para 6(1):

> The conditions are that the claimant— (a) has an income which does not exceed the applicable amount or has no income; (b) does not have capital which, or a prescribed part of which, exceeds the prescribed amount; (c) is not entitled to state pension credit; (d) is not a member of a couple the other member of which is entitled to an income-related allowance, state pension credit, income support or an income-based jobseeker's allowance; (e) is not engaged in remunerative work; (f) is not a member of a couple the other member of which is engaged in remunerative work; (g) is not receiving education.

State Pension Credit Act 2002 s.4:

> (1) A claimant is not entitled to state pension credit if he is a member of a couple the other member of which is entitled to state pension credit.
>
> (1A) A claimant is not entitled to state pension credit if he is a member of a couple the other member of which has not attained the qualifying age.

SI 2019/37 art 4(1):

> Subject to paragraph (2), the 2002 Act shall have effect as though section 4(1A) (exclusion of mixed-age couples from state pension credit) had not come into force in relation to a member of a mixed-age couple who, on the day before the appointed day and as part of that couple, is entitled to— (a) state pension credit; (b) housing benefit; or (c) state pension credit and housing benefit.

State Pension Credit Regulations 2002 (SI 2002/1792) Sch VI para 4(1)(a)(vii), (2) and (4), latest revised text, with line breaks turned into spaces, a space after each list label, and [...] where we leave words out ("disegard" and "other a break" appear in the text on legislation.gov.uk):

> 4.—(1) £20 is disregarded if the claimant or, if he has a partner, his partner— (a) is in receipt of— [...] (vii) employment and support allowance; [...]
>
> (2) Subject to sub-paragraph (4), £20 is disregarded if the claimant or, if he has a partner, his partner has, within a period of 8 weeks ending on the day in respect of which the claimant first satisfies the conditions for entitlement to state pension credit, had an award of income support, income-based jobseeker's allowance or income-related employment and support allowance and— (a) £20 was disregarded in respect of earnings taken into account in that award; (b) the person whose earnings qualified for the disegard continues in employment after the termination of that award.
>
> (4) The disregard of £20 specified in sub-paragraphs (2) and (3) applies so long as there is no break, other a break which does not exceed 8 weeks,— (a) in a case to which sub-paragraph (2) refers, in a person's entitlement to state pension credit or in employment following the first day in respect of which state pension credit is awarded; or [...]

State Pension Credit Regulations 2002 (SI 2002/1792) Sch VI para 5, latest revised text, with line breaks turned into spaces and a space after each list label:

> 5. Except where the claimant or his partner qualifies for a £20 disregard under the preceding provisions of this Schedule— (a) £5 shall be disregarded if a claimant who has no partner has earnings; (b) £10 shall be disregarded if a claimant who has a partner has earnings.

Employment and Support Allowance Regulations 2008 (SI 2008/794) reg 45(3), (4) and (9A), latest revised text, with the amendment markers removed, line breaks turned into spaces, a space after each list label, and [...] in place of the words legislation.gov.uk marks as repealed at the end of para (4):

> (3) Work for which the earnings in any week do not exceed 16 x National Minimum Wage, subject to paragraph (9A), and which— (a) is part of the claimant's treatment programme and is done under medical supervision while the claimant is an in-patient, or is regularly attending as an out-patient, of a hospital or similar institution; or (b) is supervised by a person employed by a public or local authority or by a voluntary organisation or community interest company engaged in the provision or procurement of work for persons who have disabilities.
>
> (4) Work which is done for less than 16 hours a week, for which earnings in any week do not exceed 16 x National Minimum Wage, subject to paragraph (9A), [...]
>
> (9A) Where the amount determined by the calculation in paragraphs (3) and (4) would, but for this paragraph, include an amount of– (a) less than 50p, that amount shall be rounded up to the nearest 50p; or (b) less than £1 but more than 50p, that amount shall be rounded up to the nearest £1.

We quote the current revised texts and haven't seen the wording in force on 6 April 2016. We haven't confirmed that in 2016-17 reg 45(4) read "16 x National Minimum Wage", that reg 45(9A) applied, or what the words now repealed at the end of reg 45(4) required. PolicyEngine records that before 3 April 2017 such work was exempt for 52 weeks at most unless the claimant had limited capability for work-related activity, so the man of test 8 has the support component. For 2016-17, PolicyEngine takes the limit from the national living wage of £7.20 an hour that the Secretary of State set from 1 April 2016 in the National Minimum Wage (Amendment) Regulations 2016 (SI 2016/68) reg 3 (`gov.hmrc.minimum_wage.non_apprentice`), times 16 hours (`gov.dwp.ESA.exempt_work.higher_earnings_limit_minimum_wage_hours`), £115.20, and rounds the 20p up to the nearest 50p under reg 45(9A)(a): £115.50 (`esa_exempt_work_higher_earnings_limit`).

Employment and Support Allowance Regulations 2008 (SI 2008/794) Sch 7, latest revised text, with line breaks turned into spaces and a space after each list label:

> 5. In the case of a claimant who is undertaking work which falls within one of the categories in regulation 45(2) to (4) any earnings derived from that work which do not exceed the limits specified for that work of £20 in regulation 45(2) or, as the case may be, 16 x National Minimum Wage in regulation 45(3) or (4).
>
> 6. Where regulation 45(2) to (4) applies to the claimant and that claimant's earnings are less than— (a) in a case to which regulation 45(2) applies, £20; (b) in a case to which regulation 45(3) and (4) applies, 16 x National Minimum Wage the earnings of the claimant's partner are to be disregarded to the extent that the claimant's earnings are less than £20 or, as the case may be, 16 x National Minimum Wage, but only up to a maximum of £20.

Para 7(4):

> (4) This paragraph applies, in a case where the claimant's partner is in part-time employment and paragraph 6 does not apply.

The amount DWP disregards where para 7 applies (para 7(1)):

> 7.—(1) In a case to which this paragraph applies, £20; but notwithstanding regulation 83 (calculation of income and capital of members of claimant's family and of a polygamous marriage), if this paragraph applies to a claimant it will not apply to the claimant's partner except where, and to the extent that, the earnings of the claimant which are to be disregarded under this paragraph are less than £20.

## Required outputs

- `esa_basic_condition_pensionable_age` (per person): true where the person has not reached pensionable age. It reads the person's date of birth under the Pensions Act 1995 Sch 4 para 1 (rule 1: a man with a birth date before 6 December 1953 reaches it at 65).
- `esa_income_related_not_barred_by_state_pension_credit` (per claim): true where neither the claimant nor the other member of the couple has an entitlement to State Pension Credit. It takes entitlement as an input, and that entitlement reflects the State Pension Credit means test as well as age and a claim.
- `esa_income_related_entitlement_condition`, which combines both with the other Part 2 conditions that rulespec-uk encodes or stubs.

### review_finding (paste as-is)

> Encode Welfare Reform Act 2007 s.1(3)(c), with the s.1(6) definition of pensionable age (corpus uk/statute/ukpga/2007/5/1), as its own module at uk/statutes/ukpga/2007/5/1.yaml, and Sch 1 para 6(1)(c) and (d) (corpus uk/statute/ukpga/2007/5/schedule/1/paragraph/6) as its own module at uk/statutes/ukpga/2007/5/schedule/1/paragraph/6.yaml. rulespec-uk has no module for the Act. Read pensionable age from the Pensions Act 1995 Sch 4 para 1 (corpus uk/statute/ukpga/1995/26/schedule/4/paragraph/1) and entitlement to State Pension Credit from uk:statutes/ukpga/2002/16/1#state_pension_credit_entitlement, and declare each as a deferred dependency until it has a signed module. In para 6(1)(c) and (d) Parliament named only the claimant and the other member of the claimant's couple, so a member outside the couple who claims in their own right keeps that claim whatever the couple's State Pension Credit.
>
> Required outputs, each with verbatim proof excerpts from the corpus provision:
>
> 1. `esa_basic_condition_pensionable_age` (Boolean, per person): true where the person "has not reached pensionable age" (s.1(3)(c)), with pensionable age as s.1(6) defines it.
> 2. `esa_income_related_not_barred_by_state_pension_credit` (Boolean, per claim): true where the claimant "is not entitled to state pension credit" (para 6(1)(c)) and "is not a member of a couple the other member of which is entitled to ... state pension credit" (para 6(1)(d)). Take State Pension Credit entitlement as an input for the claimant and, where applicable, the other member of their couple; declare `uk:statutes/ukpga/2002/16/1#state_pension_credit_entitlement` as its deferred dependency until the signed module supplies it. That entitlement must reflect the means test, the qualifying age, the exclusion of mixed-age couples (SPCA 2002 s.4(1A)) with its saving (SI 2019/37 art 4), and a claim.
> 3. `esa_income_related_entitlement_condition` (Boolean, per claim): outputs 1 and 2 with the other Part 2 conditions that rulespec-uk encodes or stubs.
> 4. Companion tests 1 to 8 below, with the expected values they state.

## What holds in PolicyEngine

PolicyEngine works out the State Pension Credit the couple would get with no income-related ESA in the family, and refuses the couple's claim where the couple would get some. Where the couple would get none, PolicyEngine pays the award and no Pension Credit, so the couple never gets both. DWP disregards £20 a week of earnings while the claimant or partner receives ESA (SPC Regs 2002 Sch VI para 4(1)(a)(vii)), so some couples would get Pension Credit only with the award. With Pension Credit in payment the award would stop under para 6(1)(c) and (d), and DWP then keeps disregarding the £20 only where "£20 was disregarded in respect of earnings taken into account in that award" (para 4(2)(a)). In the award, DWP disregards all of a claimant's permitted-work earnings up to the limit (ESA Regs 2008 Sch 7 para 5), so on PolicyEngine's reading of para 4(2)(a) such a claimant's Pension Credit would fall to nil and the award would come back (companion test 8, which also gives the other reading). PolicyEngine doesn't apply para 4(2), so it also pays the award and no Pension Credit to a couple in whose award DWP disregarded £20 of earnings (ESA Regs 2008 Sch 7 para 6 or 7), though that couple could stay on Pension Credit with no ESA under para 4(2). A member outside the couple keeps their own income-related ESA beside the couple's Pension Credit: in para 6(1)(d) Parliament named only the other member of the claimant's couple.

## Companion tests (dates of birth fix pensionable age; tests 1 and 3 take their rates from SI 2026/148, and tests 5, 6 and 8 take the 2016-17 minimum guarantee of £155.60 a week from PolicyEngine's parameter `gov.dwp.pension_credit.guarantee_credit.minimum_guarantee`, for which this record cites no instrument)

1. 2026-27, a single claimant with a birth date in 1956 (pensionable age 66, Sch 4 para 1 rule 6) reports income-related ESA. The claimant has reached pensionable age, so fails s.1(3)(c) and gets no ESA, and gets a guarantee credit of £238.00 a week (SPC Regs 2002 reg 6(1)(b), at the rate the Secretary of State set in SI 2026/148 art 29(2)(b)).
2. 2026-27, a couple with birth dates in 1956 and 1958, the younger claiming: both members have reached pensionable age, so each fails s.1(3)(c) and neither gets ESA.
3. 2026-27, a couple aged 73 and 60, the older a man with a birth date of 1 March 1953. The couple had State Pension Credit on 14 May 2019 and has had it ever since (SI 2019/37 art 4). He reached the State Pension Credit qualifying age on 6 January 2016 (Sch 4 para 1 rule 3, table 1, via SPCA 2002 s.1(6)), so the couple could have it on 14 May 2019. With no income, the couple gets £363.25 a week (SI 2026/148 art 29(2)(a)), so the 60-year-old fails para 6(1)(d) and gets no income-related allowance.
4. The same couple without the saving has no entitlement to State Pension Credit (SPCA 2002 s.4(1A)), so the 60-year-old meets para 6(1)(d).
5. 2016-17, a man with a birth date of 1 March 1953, over the qualifying age and under his pensionable age of 65 (rule 1, on 1 March 2018). With no income and a claim, he gets the £155.60 minimum guarantee, so he fails para 6(1)(c) and gets no income-related allowance. If he makes no claim to State Pension Credit (SSAA 1992 s.1), he meets para 6(1)(c).
6. Give the same man £115.50 a week of earnings and a £61 weekly pension. Even with the £20 earnings disregard (SPC Regs 2002 Sch VI para 4(1)(a)(vii)), his income of £156.50 a week exceeds the £155.60 minimum guarantee, and below 65 he gets no savings credit (SPCA 2002 s.3(1)). He has no entitlement to State Pension Credit, so he meets para 6(1)(c).
7. 2016-17, a woman with the same birth date reaches pensionable age on 6 January 2016 (rule 3, table 1), so she fails s.1(3)(c).
8. 2016-17, the man of test 5, with an award of income-related ESA that includes the support component, works 15 hours a week in permitted work for £115.50 a week, and has a £50 weekly pension. His work falls under ESA Regs 2008 reg 45(4) (quoted above; see the note under reg 45): he works under 16 hours, and £115.50 doesn't exceed the 2016-17 limit, 16 × £7.20 = £115.20, which DWP rounds up to £115.50 under reg 45(9A)(a). The Secretary of State set the £7.20 national living wage from 1 April 2016 (National Minimum Wage (Amendment) Regulations 2016, SI 2016/68, reg 3). While he receives ESA, DWP disregards £20 of his earnings (Sch VI para 4(1)(a)(vii)): 115.50 + 50 − 20 = £145.50 a week, £10.10 under the £155.60 minimum guarantee. With Pension Credit in payment he would fail para 6(1)(c) and his ESA would stop. In that award DWP disregarded all of his permitted-work earnings under ESA Regs 2008 Sch 7 para 5, not a fixed £20. PolicyEngine's result agrees with the reading under which "£20 was disregarded" means the £20 disregard of Sch 7 paras 6 and 7(1), as under para 5 DWP disregards "any earnings ... which do not exceed the limits", not a fixed £20. On that reading he fails para 4(2)(a) and DWP disregards only £5 (Sch VI para 5(a), quoted above): 115.50 + 50 − 5 = £160.50 a week, over £155.60, so nil, and below 65 no savings credit (SPCA 2002 s.3(1)). He then has no lasting entitlement to State Pension Credit, so `esa_income_related_not_barred_by_state_pension_credit` is true, `esa_basic_condition_pensionable_age` is true, and his award continues with no State Pension Credit. The expected values in this test follow that reading. On another reading, para 4(2)(a) holds wherever DWP disregarded at least £20 of earnings in the award, and while he stays in employment (para 4(2)(b)) he would then keep £10.10 a week of Pension Credit, £525.20 over 52 weeks, with no ESA.

## Blocker

The corpus holds no source text for the provisions these modules cite. On 1 November 2026 at 04:51 UTC, the corpus serving view (`corpus.current_provisions`) returned no row for `uk/statute/ukpga/2007/5/1`, `uk/statute/ukpga/2007/5/schedule/1/paragraph/6` or `uk/statute/ukpga/1995/26/schedule/4/paragraph/1`, and one row for the control `uk/statute/ukpga/2002/16/1`. rulespec-uk pins the signed release `uk-rulespec-2026-10-25-pe-child-definitions` in `.axiom/toolchain.toml`, and its statute scopes hold none of the three either. Clear the same prerequisites as rulespec-uk#395 and TheAxiomFoundation/axiom-corpus#810, in order:

1. Ingest WRA 2007 s.1, WRA 2007 Sch 1 para 6 and Pensions Act 1995 Sch 4 para 1 into axiom-corpus, with the Northern Ireland provisions above if the encoding covers them.
2. Publish a signed corpus release that contains them.
3. Rebind rulespec-uk: move `.axiom/toolchain.toml` to that release.
4. Run the signed encode. Each billed encoder run needs separate approval.

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

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

PR_BODY.md

Fixes #2565

## Stacked on #2813

This branch contains #2813 (`fix-2562-esa-abolition` at `dc69f3ada`), which models the abolition of income-related ESA from 1 July 2026 (1 October 2026 in Northern Ireland). Land #2813 first: this PR's diff then shows only #2807's change, the six appointee savings in `esa_income_pension_age.yaml` and the test changes below. If this PR lands first, it takes #2813 with it.

With both in force, the abolition reaches an unsaved award reported in 2026-27, and a family that would claim Universal Credit on the move ends it for the year, so several of this PR's 2026 tests lost the award they test. Under the claimant's appointee saving DWP keeps the award in payment (SI 2025/1148 art. 3A(2), `esa_income_appointee_saving`), and the model then pays the claim for the whole of 2026-27 (`esa_income_payable_share`). Each changed test now saves the 2026 awards it tests, so it still tests the pension-age bar and the Pension Credit condition:

- `esa_income_pension_age.yaml` cases 4, 5, 6, 12, 13 and 14: each ESA reporter has the saving. Each case asserts a full-year award in 2026-27, which the abolition would otherwise cut to 86/365 or close.
- `test_couple_esa_follows_pension_credit_worked_out_without_it`: every adult drawn in 2026 has the saving. Without it, a couple aged 58 and 65 reached the reference's abolition rule, which covers no age from 65 to 69.
- `legacy_award_work_reference.esa_abolition_claim`: reads the saving as `esa_income_payable_share` does. Where a member who reports the couple's award has the saving, DWP keeps the whole claim in payment, and the model ignores a saving held only by a member who reports nothing. No caller without the saving changes.
- `test_a_parameter_reform_keeps_the_age_and_take_up_conditions`: the working-age claimant and the younger member of the couple aged 70 and 60 have the saving. Unsaved, each claims Universal Credit when the abolition reaches the award, and gets no ESA whatever the reform.
- `test_the_screen_reads_no_pension_credit_condition_without_a_couple_claim`: every reporter has the saving. Unsaved, the claimant aged 45 out of work claims Universal Credit, so no couple's claim reaches the Pension Credit conditions, and the claimant in remunerative work fails on a closed report and not the work test.
- `test_no_branch_opens_without_a_couple_that_could_be_on_pension_credit`: every reporter has the saving. Unsaved, the couple's awards end in a Universal Credit claim and the member outside the couple gets 86/365 of £5,000.

The abolition's own rules stay with #2813's tests, `test_esa_income_abolition_properties.py` among them.

### Overlap with #2813

`a8f46e3d8` merges #2813's head `dc69f3ada` into `d0ffe7fd3`, and git merged it with no conflict: `git show --remerge-diff a8f46e3d8` lists no file. Of #2813's three commits since `7fe7360fa`, two change a formula, both of them `would_claim_housing_benefit`'s. `84935ccef` gave a hand-entered family in specified or temporary accommodation Housing Benefit take-up, and `dc69f3ada` takes that rule out again, so a hand-entered family claims Housing Benefit under `claims_all_entitled_benefits` or a Housing Benefit report, as before `84935ccef`. `3c9890a19` rewords one comment. The rest of those commits change documentation, comments, #2813's changelog fragment, the `awards_payable.yaml` descriptions and #2813's tests, and none of this PR's own files. `in_specified_or_temporary_accommodation` defaults to false and has no formula, and none of this PR's files sets it. So for every family this PR's tests build, `would_claim_housing_benefit` returns the same value at `dc69f3ada` as at `3c9890a19` and `7fe7360fa`, and no result this PR asserts moves. Every case and property this PR asserts passes at `3f7735fd6` (Test plan).

## What and why

On `origin/main` (2da212abc), `esa_income_eligible` screens a reported income-related ESA award on capital and remunerative work alone, so the model pays it beside State Pension Credit and to claimants past pensionable age. With £5,000 of reported income-related ESA in 2026 and no Universal Credit claim, a single person aged 70 gets £5,000 of ESA and £12,376 of Pension Credit. A couple aged 70 and 68 gets £5,000 and £18,889, and so does a mixed-age couple aged 73 and 60 that has kept Pension Credit since May 2019.

Parliament bars both overlaps in the Welfare Reform Act 2007:

- Under s.1(3)(c), a claimant who has reached pensionable age gets no ESA. In s.1(6) Parliament took pensionable age from the Pensions Act 1995 Sch 4 para 1: the claimant's own State Pension age, which the model holds as `is_SP_age`.
- In Sch 1 para 6(1)(c) and (d) Parliament set two more conditions for the income-related allowance: the claimant "is not entitled to state pension credit", and "is not a member of a couple the other member of which is entitled to ... state pension credit".

This PR applies both:

- `esa_income_eligible` refuses a candidate claimant who has reached pensionable age.
- It reads the State Pension Credit the couple would get with no income-related ESA in the family, B(0) below, and refuses the couple's claim where B(0) exceeds nil. It reads `pension_credit`, so the claim (`would_claim_pc`) and an entered award count.
- The model's own formulas pay Pension Credit only to a couple that meets the Pension Credit age conditions and would claim, so where they calculate it, the screen reads B(0) only for such a couple. A user can instead enter `pension_credit`, `pension_credit_entitlement`, `is_pension_credit_eligible`, `guarantee_credit` or `savings_credit` for the year. A reform can change anything Core calculates one of those five from: a formula, the default a family outside `defined_for` gets, `defined_for` itself or what the entitlement sums. It can also freeze Pension Credit at the baseline's award (`gov.contrib.freeze_pension_credit`). The screen then reads B(0) for every couple whose claim passes the other tests (`pension_credit_follows_model_formulas`). Under a parameter reform the model keeps its formulas, and `meets_pension_credit_age_conditions` reads the reform's parameters.
- The screen also reads B(0) for every couple whose claim passes the other tests when a reform gives `would_claim_pc` a formula, since that formula could read the couple's ESA. With take-up entered as an input, it still reads B(0) only for a couple that meets the age conditions and would claim. When `would_claim_pc` has no formula, the screen reads it in the simulation itself, so a reform that makes take-up read the couple's ESA through `defined_for` or `adds` stops the calculation with `CycleError`.
- The model also refuses an own-right claim from a member outside the couple once they reach pensionable age, in `esa_income` and `is_on_income_related_esa`. That member keeps their claim whatever the couple's Pension Credit: in para 6(1)(d) Parliament named only the other member of the claimant's couple.
- `pension_credit` pays nothing to a family while the claimant or partner has income-related ESA (`claimant_or_partner_esa_income` above nil), so the couple never gets both awards. It reads the couple's award and not `esa_income`, so a member outside the couple keeps their own award beside the couple's Pension Credit. Under the freeze (`gov.contrib.freeze_pension_credit`) the formula still returns the baseline's award, and an entered `pension_credit` still replaces the formula.
- A member past pensionable age can't be the couple's claimant, so `is_on_income_related_esa` and the Housing Benefit and Universal Credit benefit cap readers pass `can_claim` to `is_payee_of_couple_award`, as `is_on_pension_credit` does. `own_report_is_paid` takes `barred` in place of `works`, and `is_on_income_based_jsa` passes the same work test as before.

With the fix, the three families above get no ESA and the same Pension Credit: £12,376 = 52 × £238.00 and £18,889 = 52 × £363.25, the weekly rates the Secretary of State set in SPC Regs 2002 reg 6(1) by SI 2026/148 art 29(2). The same 73 and 60 couple without Pension Credit since May 2019 gets no Pension Credit (SPCA 2002 s.4(1A)), and keeps its £5,000 where DWP keeps the award in payment through 2026-27 under an appointee saving (case 4). A man aged 63 in 2016 who reports £5,000 of income-related ESA and has £2,000 of Pension Credit as an input gets the £2,000 and no ESA, whether or not he would claim Pension Credit; so does a man aged 40 whose Pension Credit entitlement or eligibility an input sets. Under a reform that gives a family outside the entitlement's `defined_for` £2,000 of it, a man aged 40 in 2016 who reports £5,000 of income-related ESA and would claim Pension Credit gets the £2,000 and no ESA. Under a reform in which a family claims Pension Credit while it gets no income-related ESA, two men aged 63 each get £8,091.20 of Pension Credit and no ESA, and a man aged 40 keeps his £5,000 and gets no Pension Credit.

`replaces_definition`, in `utils/supplied_inputs.py`, compares the simulation's variable with a fresh instance of the model's own class. It compares everything Core calculates the variable from: the formula for the period, by its code, the default, `defined_for`, `adds` and `subtracts`, the period, the end date and neutralization. It ignores the label, documentation, reference, unit, metadata and the other descriptive attributes. It also counts an input for the year, and the five Pension Credit variables join `SUPPLIED_INPUT_VARIABLES` so that it can ask about inputs.

### Pension Credit without the award

The model's Pension Credit reads the couple's award five ways, so a screen that read `pension_credit` directly would stop the engine with `CycleError`:

- `pension_credit` itself pays nothing while the couple has the award (`claimant_or_partner_esa_income`), so that no couple gets both, since Parliament allows the award only where neither member of the couple is entitled to State Pension Credit (WRA 2007 Sch 1 para 6(1)(c) and (d));
- `pension_credit_earnings_disregard` reads `claimant_or_partner_esa_income`, from the list `gov.dwp.pension_credit.earnings_disregard.higher.disability_benefit_unit_benefits`, for the higher earnings disregard (SPC Regs 2002 Sch VI para 4(1)(a)(vii));
- `pension_credit_income` counts working tax credit, and the tax credit income test reads the award (TCA 2002 s.7(2), `tax_credits_applicable_income`); `savings_credit_income` takes working tax credit back out, and `pension_credit_earnings_disregard` reads working tax credit's disability elements;
- that test also reads Income Support, whose gate (`income_support_eligible`) reads the award;
- Pension Credit's route test (`left_pension_route_at_legacy_closure`) reads Universal Credit entitlement, whose work capability test reads `esa_income` (`has_own_esa_award`).

`pension_credit_without_income_related_esa` follows `pension_credit_with_passported_tax_credits`. It opens a throwaway branch under a name neither the simulation nor its branches use, and sets `esa_income`, `claimant_or_partner_esa_income` and `esa_income_eligible` to nil there. It then reads `pension_credit` through the branch's population and pops the branch in `finally`. Core reads an input that a parent branch sets. So in a tax credit year the passport branch inside it reads the award as nil, and an ESA branch inside a passport branch keeps the passport's input.

The screen returns before it reads any Pension Credit condition when no couple's claim passes the age, work and capital tests. Otherwise it opens the branch only where some couple's claim passes them and, while the model's own formulas calculate Pension Credit, that couple meets the age conditions and would claim.

Write B(e) for the family's Pension Credit with the couple's award e. The model pays:

| B(0) | B(e) | The model pays | YAML case |
|---|---|---|---|
| more than nil | more than nil | B(0), no ESA | 15 |
| nil | nil | ESA, no Pension Credit | 16, 17, 20 |
| more than nil | nil | B(0), no ESA | 19 |
| nil | more than nil | ESA, no Pension Credit | 18, 25, 26 |

In the third row, working tax credit decides the result. With the award, HMRC pays working tax credit without its income test, and the full credit puts the couple's income over the minimum guarantee; without the award, the income-tested credit leaves the couple some Pension Credit. Both states hold. The model takes the one with Pension Credit and no income-related ESA, since Parliament made the allowance depend on the absence of Pension Credit (para 6(1)(c) and (d)) and not the reverse.

In the fourth row the couple gets Pension Credit only with the award, through the higher earnings disregard, and the model pays the award and no Pension Credit. Parliament allows the award only while the claimant "is not entitled to state pension credit" and "is not a member of a couple the other member of which is entitled to ... state pension credit" (para 6(1)(c) and (d)). With Pension Credit in payment the couple's ESA would stop, so the £20 that DWP disregards for a claimant or partner "in receipt of ... employment and support allowance" (SPC Regs 2002 Sch VI para 4(1)(a)(vii)) would stop with it. Where the claimant or partner had an award of income-related ESA in the 8 weeks before Pension Credit began, DWP keeps disregarding the £20 under para 4(2) only where:

> (a) £20 was disregarded in respect of earnings taken into account in that award;
> (b) the person whose earnings qualified for the disegard continues in employment after the termination of that award.

For a claimant in permitted work, DWP disregards "any earnings derived from that work which do not exceed the limits specified for that work" (ESA Regs 2008 Sch 7 para 5): all of those earnings up to the limit, not a fixed £20. Case 18's man works 15 hours a week for £115.50, which the 2016-17 limit of £115.50 covers: 16 hours at the £7.20 national living wage comes to £115.20, and `esa_exempt_work_higher_earnings_limit` rounds that up to the next 50p, citing ESA Regs 2008 reg 45(9A). No formula applies para 4(2) (`pension_credit_earnings_disregard` lists it as not modelled), so the model's result agrees with one reading of it and not the other. PolicyEngine's result agrees with the reading under which "£20 was disregarded" means the £20 disregard of Sch 7 paras 6 and 7(1), as under para 5 DWP disregards "any earnings ... which do not exceed the limits", not a fixed £20. On that reading he fails para 4(2)(a): his Pension Credit would fall to B(0), nil, and his ESA would come back, so ESA with no Pension Credit holds over time, and Pension Credit with no ESA doesn't. On another reading, para 4(2)(a) holds wherever DWP disregarded at least £20 of earnings in the award, and while he stays in employment (para 4(2)(b)) he would then keep £525.20 of Pension Credit with no ESA.

A family lands in this row only with earnings, an open ESA report, Pension Credit's age conditions and no other route to the higher disregard. Its income must also exceed the minimum guarantee after the standard disregard by less than the gap between the two disregards: £15 a week, or £10 for a couple. Case 18's man has £115.50 a week of earnings and a £50 pension against a £155.60 minimum guarantee: £160.50 of income after the £5 disregard, and £145.50 after the £20 one. So B(0) is nil and B(e) comes to 52 × £10.10 = £525.20, and the model pays the £5,491.20 award and no Pension Credit.

The model applies this rule where DWP might keep a couple on Pension Credit. DWP disregards £20 of earnings in an ESA award where the claimant's partner is in part-time employment and paragraph 6 doesn't apply (Sch 7 para 7(4)), where the claimant's permitted-work earnings fall £20 or more short of the limit (from the partner's earnings, Sch 7 para 6), and where one of the regulations Sch 7 para 7(2) lists applies to the claimant (subject to para 7(3)). Where the person whose earnings qualified stays in work, DWP could keep such a couple on Pension Credit at B(e), with no ESA, once it claimed (para 4(2)). The model holds no record of how an award started, and `pension_credit_earnings_disregard` doesn't apply para 4(2), so the model pays such a couple the award and no Pension Credit too.

- In case 25 (2016), a man aged 63 reports £5,491.20 of income-related ESA and has a £135 weekly pension, and his partner, a woman aged 60, earns £115.50 a week for 15 hours, so DWP disregards £20 of her earnings in the award under para 7(4) if that work is part-time employment. B(0) is nil (£250.50 − £10 = £240.50, over the £237.55 couple minimum guarantee), B(e) comes to 52 × £7.05 = £366.60, and the model pays the award and no Pension Credit.
- In case 26 (2016), a man aged 63 reports £5,491.20 of income-related ESA, earns £90 a week in 15 hours and has a £130 weekly pension, and his partner, a woman aged 60, earns £30 a week in 5 hours. His £90 falls more than £20 short of 16 × £7.20 = £115.20, so DWP disregards £20 of her £30 in the award under para 6. B(0) is nil (£250 − £10 = £240, over £237.55), B(e) comes to 52 × (£237.55 − £230) = £392.60, and the model pays the award and no Pension Credit. Neither member owes income tax: the model takes the couple to be married (`is_married`) and to claim the marriage allowance (`would_claim_marriage_allowance`), so it transfers £1,100 of her unused personal allowance to him (`marriage_allowance`), which covers his £440 over the £11,000 personal allowance.

### What this PR does not change

- Para 6(1)(g) (not receiving education): the screen reads no education test, as before.
- The other limbs of para 6(1)(d). The model makes the couple's reports one award, so the other member never holds a separate entitlement to an income-related allowance. `income_support_eligible` already refuses Income Support while the couple has income-related ESA (SSCBA 1992 s.124(1)(h)), and a screen that read Income Support would close another cycle. The screen doesn't test the other member's income-based JSA.
- `income_support_eligible` keeps its own reading of s.124(1)(g): the couple meets the Pension Credit age conditions and would claim.
- s.1(3)(c) for contributory ESA: `esa_contrib` adds up the reported amounts, with no age test.
- s.1(3)(e) and (f) (not entitled to Income Support or JSA) for the claimant.
- The abolition of income-related ESA (#2562): #2813 models it, and this branch carries #2813's code unchanged.

### Northern Ireland

The Northern Ireland Assembly enacted s.1(3)(c) and Sch 1 para 6(1) word for word in the Welfare Reform Act (Northern Ireland) 2007, and changed two provisions. In s.1(3)(d) the claimant must be "in Northern Ireland". In s.1(6) the Assembly took pensionable age from the Pensions (Northern Ireland) Order 1995 Sch 2 para 1, where Parliament took it from the Pensions Act 1995 Sch 4 para 1. A person born on a given date reaches pensionable age on the same day under either schedule: their nine rules and four tables, 77 rows in all, match line for line. Northern Ireland has the same exclusion of mixed-age couples in the State Pension Credit Act (Northern Ireland) 2002 s.4(1A) and the same saving in SR 2019/4 art 4(1). So the model applies one rule across the UK.

### Years this reaches

Income-related ESA has no `active` parameter, so the screen runs in every model year, and the model applies s.1(3)(c) in every year. With #2813 in force, the model pays an unsaved award for the share of 2026-27 before the abolition (`esa_income_payable_share`: 86/365 in Great Britain, 178/365 in Northern Ireland) and closes its report from 2027-28 (`esa_income_report_closed`), so from 2027-28 only an award under an appointee saving reaches the screen. Only a man with a birth date before 6 December 1953 can fail para 6(1)(c) while still under pensionable age: past the State Pension Credit qualifying age but not his pensionable age of 65. Such men appear only in model years up to 2018. A mixed-age couple that reports no Universal Credit can fail para 6(1)(d) up to model year 2019, since the model reads that year's parameters at 30 April, before the 15 May 2019 exclusion. From then on only a couple that keeps the SI 2019/37 art 4 saving can fail it on the model's formulas. In each case the couple must also get Pension Credit without the award. Where a user enters or reforms Pension Credit, a couple fails para 6(1)(c) or (d) in any year and at any age wherever that Pension Credit exceeds nil.

## Invariants

For every input:

1. Take a couple with an open report whose claim passes the age, work and capital tests. Where B(0) exceeds nil, the model pays the couple no income-related ESA, and the family's Pension Credit equals B(0). This holds whether the model works out Pension Credit or a user enters or reforms it.
2. Where B(0) is nil, the couple's take-up of Pension Credit changes neither the couple's income-related ESA nor the family's.
3. Claiming Pension Credit never raises income-related ESA.
4. Nobody who has reached pensionable age receives income-related ESA (`is_on_income_related_esa`), and a family whose every report comes from such a member gets none.
5. `esa_income_eligible` equals the family-by-family reading of the law in `legacy_award_work_reference.esa_screen`, given `is_SP_age` and B(0).
6. On the model's own formulas, reading B(0) for every couple whose claim passes the other tests gives every family the same ESA, Pension Credit and income as reading it only for a couple that meets the age conditions and would claim.
7. Without a couple's claim that passes the age, work and capital tests, the screen reads neither the Pension Credit age conditions nor the claim. A population in which no such couple meets the age conditions and would claim opens no branch while the model's formulas calculate Pension Credit, a parameter reform included, and no branch outlives the calculation.
8. For families to which the model applies neither new refusal, and to which `origin/main` doesn't pay both Pension Credit and the couple's income-related ESA, every output equals `origin/main`'s.
9. A reform that changes anything Core calculates a Pension Credit variable from, or gives `would_claim_pc` a formula, makes the screen read B(0) for every couple whose claim passes the other tests. A change to a variable's label, documentation, reference, unit or metadata alone, a take-up input and a parameter reform keep the age and take-up conditions.
10. Wherever the model's own formulas calculate Pension Credit, no family has both `pension_credit` and `claimant_or_partner_esa_income` above nil.

A family can get both awards in one way: a member outside the couple keeps their own income-related ESA beside the couple's Pension Credit (case 12).

## Cost

The branch costs a second Pension Credit calculation for the whole population in each year in which some couple could fail the Pension Credit condition. The screen also opens another inside each branch that recomputes Pension Credit: the tax credit passport and the non-dependant Pension Credit test (`uc_non_dep_receives_pension_credit`). A Microsimulation pays this in each year up to 2026-27. From 2027-28 the model closes every unsaved report (`esa_income_report_closed`), so in a dataset run, where nobody has the appointee saving, the screen returns before it opens a branch. At `741054b16`, a couple aged 74 and 61 with the mixed-age saving, in which the younger member reports £5,000, opens 2 ESA branches in 2026 and in 2027 with the appointee saving, and none without it (`esa_income_report_closed` [True, True]).

On a synthetic population of 480 families and 889 people (seed 256502), with the same inputs on each tree, the model opens these branches, ESA branches in brackets:

| Year | `origin/main` (2da212abc) | `0d157f078` | `b6ddfcf0d` |
|---|---|---|---|
| 2016 | 8 (0) | 28 (5) | 28 (5) |
| 2018 | 8 (0) | 28 (5) | 28 (5) |
| 2024 | 8 (0) | 28 (5) | 28 (5) |
| 2025 | 2 (0) | 8 (2) | 8 (2) |
| 2026 | 2 (0) | 11 (3) | 11 (3) |

`b6ddfcf0d`, the first commit with the entered-award test and the early return, gives every family the same 24 outputs as `0d157f078` in each of the five years. Every year of this population has some couple whose claim passes the age, work and capital tests, so the early return skips nothing there; it saves the reads of the age conditions and the claim only in a population with no such couple. An entered or reformed Pension Credit, or a take-up formula, costs more: the screen then reads Pension Credit for every couple whose claim passes the other tests, and opens the branch wherever one does. These counts come from commits before the branch took in #2813, and we haven't timed the early return.

At `d37e42de9`, on a separate population of 480 families (785 to 810 people, seed 20261031), each year ran in its own process, with `origin/main` and this PR side by side on a shared host, twice. The table gives the seconds each run took to build the simulation, then to calculate the outputs:

| Year | `origin/main`, run 1 | This PR, run 1 | `origin/main`, run 2 | This PR, run 2 |
|---|---|---|---|---|
| 2016 | 20.1, 4.1 | 55.0, 12.4 | 78.0, 24.6 | 79.4, 44.8 |
| 2018 | 30.2, 9.4 | 42.2, 33.1 | 48.3, 13.5 | 29.4, 21.0 |
| 2026 | 61.0, 10.3 | 68.8, 9.1 | 117.6, 16.1 | 128.5, 25.0 |

The calculation took 1.6 to 3.5 times as long in 2016 and 2018, years with tax credits, and between 0.9 and 1.6 times as long in 2026. Host load moved between runs, so the branch counts measure the cost more steadily than the clock.

## Test plan

CI runs the suite at the head, `3f7735fd6`. At that commit the local runs below cover every test file this PR or #2813 changes against `main` (`git diff --name-only origin/main...HEAD -- policyengine_uk/tests`), this PR's two Income Support property files among them. Earlier sections name each target they ran.

### Local runs at `3f7735fd6`

`3f7735fd6` adds these commits to `d0ffe7fd3`:

- `a8f46e3d8` merges #2813's head `dc69f3ada`, with no conflict (Overlap with #2813).
- `6cd70815a` adds case 26's couple (`COUPLE_ROW`) beside case 18's man to every generated 2016 example of `examples()`, and an explicit 2018 example (`ROWS_2018`: case 19's working tax credit couple and case 20's couple) to the combined property and the differential.
- `0822532e4` changes text only: case names in which DWP, HMRC or the couple acts, case 18's and 26's comments, the docstring of `pension_credit_without_income_related_esa`, `esa_abolition_claim`'s docstring and the appointee-saving comments.
- `44ea4c161` changes text only: case 18's comment says which reading its expected values agree with, and `pension_credit`'s guard comment gives the formula the verb.
- `9327cec76` merges `main` at `95dfc62f8`, with no conflict.
- `741054b16` changes text only: comments and docstrings in which DWP or the formula acts.
- `3f7735fd6` merges `main` at `47a2817e2`, with no conflict.

Each file ran alone, with no survey data, after `uv sync --frozen --extra dev`.

YAML (`uv run policyengine-core test <file> -c policyengine_uk`), every case passed:

- `esa_income/esa_income_abolition.yaml` 41, `esa_income/esa_income_capital.yaml` 10, `esa_income/esa_income_pension_age.yaml` 26, `esa_income/esa_income_remunerative_work.yaml` 47, `housing_benefit/housing_benefit_eligible.yaml` 15, `housing_benefit/housing_benefit_universal_credit_passport.yaml` 9, `housing_benefit/would_claim_housing_benefit.yaml` 6, `legacy_award_readers/council_tax_reduction_passports.yaml` 11, `legacy_award_readers/housing_benefit_pension_age_regulations.yaml` 4, `legacy_award_readers/pension_credit_earnings_disregard.yaml` 4, `universal_credit/forgoes_universal_credit_for_tax_free_childcare.yaml` 10, `universal_credit/housing_costs_element/uc_housing_costs_element.yaml` 8, `universal_credit/work_allowance/uc_work_allowance.yaml` 5, `gov/dwp/benefit_cap_by_scheme.yaml` 22, `gov/dwp/pension_credit/mixed_age_couples.yaml` 26 and `gov/dwp/universal_credit/person_legacy_benefits_closed.yaml` 3.

Python (`uv run pytest -q -o xfail_strict=true -n 4 <file>`, 30-minute cap; none stopped), every test passed:

- `test_benefit_cap_uc_and_hb_properties.py` 1 (58.10 s), `test_disable_simulated_benefits.py` 49 (149.82 s), `test_esa_income_abolition_properties.py` 31 (76.56 s), `test_esa_income_held_outside_award.py` 3 (17.76 s), `test_esa_income_pension_age_properties.py` 19 (75.21 s), `test_esa_income_universal_credit_claim_properties.py` 2 (92.58 s), `test_esa_permitted_work_nlw_reforms.py` 5 (33.27 s), `test_housing_benefit_accommodation_dates_properties.py` 12 (18.05 s), `test_housing_benefit_accommodation_take_up_data_built.py` 8 (16.32 s), `test_housing_benefit_universal_credit_passport_properties.py` 6 (377.40 s), `test_income_support_eligibility_properties.py` 4 (120.77 s), `test_income_support_gate_reads_claimant_or_partner_awards.py` 13 (239.57 s), `test_legacy_award_readers_properties.py` 20 (194.52 s), `test_legacy_award_work_properties.py` 4 (102.51 s), `test_legacy_closure_scope.py` 21 (31.95 s), `test_mixed_age_couple_properties.py` 1 (762.86 s) and `test_uc_legacy_closure_properties.py` 4 (137.97 s).
- `policyengine_uk/tests/code_health`: 3,166 passed in 223.79 s.

Hypothesis 6.170.11 statistics at `3f7735fd6` (`--hypothesis-show-statistics`, generated examples only): the combined property draws 1 example in 2016 and 3 in 2026, and "Pension Credit pays only with the award" appears in 25% of them, the 2016 one, which carries case 18's man and case 26's couple. The differential draws 1 in 2016 and 1 in 2026. Neither property draws a 2018 example, so the explicit `ROWS_2018` example gives each a 2018 family on every run. `6cd70815a` appends the couple after the draws. `legacy_award_work_reference.py`, the one other file the list names, holds the shared test reference and no tests; five of the property files above import it.

We ran mutant M5 as one change to `pension_credit.py` in a copy of `741054b16`: the guard skips couples (`where((couple_esa > 0) & ~is_couple, 0, entitlement * would_claim)`).

| Variant of the property file | `test_esa_income_pension_age_properties.py` under M5 |
|---|---|
| As at `741054b16` | **Killed**, 1 failed and 18 passed in 86.29 s, first on the explicit 2018 example at line 428: case 19's couple gets no Pension Credit against B(0) of 955.40 |
| `ROWS_2018` example removed from the combined property | **Killed** on the combined property's generated 2016 example, which carries case 26's couple: `assert not ((couple_esa > 0) & (pension_credit > 0)).any()` fails (that node alone, run without `-n`) |
| `ROWS_2018` example removed and `COUPLE_ROW` out of `examples()` | Survives: the combined property passes (1 passed in 44.23 s, that node alone, run without `-n`) |

In 2018 case 19's couple would get £7.60 of Pension Credit with the award. Under M5 the tax credit passport branch then pays them Pension Credit, HMRC pays working tax credit without its income test, and the couple gets neither Pension Credit nor ESA. In `esa_income_pension_age.yaml`, M5 fails cases 25 and 26 (2 failed, 24 passed); case 19 runs in 2016, when that couple gets no Pension Credit with the award.

Other checks:

- [x] `.github/check_fix_pr.py` from `main` at `47a2817e2`, on this title and body, as `pr_fix_evidence.yaml` runs it, with no labels, against the changes from the merge base (`47a2817e2`) to `3f7735fd6`: PASS, 33 changed executable tests.
- [x] `uvx ruff@0.17.2 format --check .` (2,235 files already formatted), `uvx ruff@0.17.2 check .` and `uvx ruff@0.17.2 check --select F811,F821 policyengine_uk`: all passed. `git diff --check d0ffe7fd3..3f7735fd6`: clean.
- [x] `git merge-tree --write-tree --name-only 3f7735fd6 origin/main` (`47a2817e2`): clean, tree `49b8bbbe3`.

### Local runs at `d0ffe7fd3`

`d0ffe7fd3` adds four commits to `254669a0d`:

- `562d067ab` merges #2813's head `3c9890a19`, with no conflict.
- `419bccb44` adds case 26 (the para 6 couple), names Sch 7 paras 6 and 7 in the docstring of `pension_credit_without_income_related_esa` and in case 25, states the other reading of para 4(2)(a) in case 18, gives DWP or the formula the verb in comments in `esa_income_eligible.py` and cases 18 and 25 and in `pension_credit`'s `documentation` string, and rewraps the YAML header.
- `d6bf2f657` adds case 18's man to every generated 2016 example of `examples()` in `test_esa_income_pension_age_properties.py`. The explicit `@example((ROWS, 2016))` stays.
- `d0ffe7fd3` rewords the appointee-saving comments of `esa_income_pension_age.yaml`, the property file's docstring and `examples()`'s comment, `legacy_award_work_reference.esa_abolition_claim`, case 19's savings-credit comment and case 18's "as under para 5 DWP disregards".

Each file ran alone, with no survey data, after `uv sync --frozen --extra dev`.

YAML (`uv run policyengine-core test <target> -c policyengine_uk`), every case passed:

- `esa_income/esa_income_pension_age.yaml`: 26 passed.
- `esa_income/esa_income_abolition.yaml`: 40 passed.
- `housing_benefit/housing_benefit_eligible.yaml`: 14 passed.
- The directory `baseline/finance/benefit/family/esa_income`: 132 passed.
- The directory `baseline/gov/dwp/pension_credit`: 320 passed.

Python (`uv run pytest -q -o xfail_strict=true -n 4 <target>`, 30-minute cap; none stopped), every test passed:

- `test_esa_income_pension_age_properties.py`: 19 passed in 92.25 s.
- `policyengine_uk/tests/code_health`: 3,093 passed in 277.74 s.

We worked case 26 from the 2016-17 parameter files (a couple minimum guarantee of £237.55 a week, disregards of £10 and £20), and the model gives the same figures: `pension_credit_entitlement` 392.60, `pension_credit_income` 11,960 (£13,000 less 52 × £20), `pension_credit_employment_income_tax` 0 and `pension_credit` 0. It pays the reported award, `esa_income` 5,491.20.

We ran each mutant as one change to a copy of `d6bf2f657`:

| Mutation | What fails |
|---|---|
| Guard removed from `pension_credit` | YAML cases 18, 25 and 26 (3 failed, 23 passed) |
| The same, with the explicit `@example((ROWS, 2016))` removed from the combined property | The combined property, on a generated example: `assert not ((couple_esa > 0) & (pension_credit > 0)).any()` (1 failed in 118.03 s). Case 18's man, whom `examples()` now adds to every 2016 example, gets the award beside Pension Credit |

At `a1771bb2e` the second mutant survived, because none of the 4 generated examples reached the fourth row.

Other checks:

- [x] `.github/check_fix_pr.py` on this title and body, as `pr_fix_evidence.yaml` runs it, against the changes from the `main` merge base (`0080274d7`) to `d0ffe7fd3`: PASS, 33 changed executable tests.
- [x] `uvx ruff@0.17.2 format --check .` (2,178 files already formatted), `uvx ruff@0.17.2 check .` and `uvx ruff@0.17.2 check --select F811,F821 policyengine_uk`: all passed. `git diff --check 254669a0d..d0ffe7fd3`: clean.
- [x] `git merge-tree --write-tree --name-only d0ffe7fd3 origin/main` (`52b30809f`): clean.

### Local runs at `a1771bb2e`

`a1771bb2e` adds two commits to `71ba27523`, and `git merge-tree` of it against `main` at `0080274d7` is clean. `70563bbd` gives `pension_credit` the guard, changes case 18, adds case 25, adds invariant 10 to the combined property and updates the docstrings and the changelog fragment. `a1771bb2` checks the table's outside-reporter row in that property. Each file ran alone, with no survey data.

YAML (`uv run policyengine-core test <file> -c policyengine_uk`), every case passed:

- `esa_income/esa_income_pension_age.yaml`: 25 passed.
- #2813's YAML test files: `esa_income_abolition.yaml` 36, `esa_income_capital.yaml` 10, `esa_income_remunerative_work.yaml` 47, `housing_benefit_eligible.yaml` 6, `council_tax_reduction_passports.yaml` 11, `housing_benefit_pension_age_regulations.yaml` 4, `legacy_award_readers/pension_credit_earnings_disregard.yaml` 4, `forgoes_universal_credit_for_tax_free_childcare.yaml` 10, `mixed_age_couples.yaml` 26 and `person_legacy_benefits_closed.yaml` 3.
- Files whose cases set the couple's ESA and read Pension Credit: `income_support_claimant_partner_gates.yaml` 27, `legacy_award_readers/passports.yaml` 20, `claims_universal_credit.yaml` 22, `housing_benefit_pension_age_means_test.yaml` 5, `gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml` 33, `relevant_benefit_receipt.yaml` 40, `winter_fuel_payment_charge.yaml` 41, `warm_home_discount.yaml` 17, `warm_home_discount_receipt.yaml` 7, `pawhp.yaml` 27, `scottish_child_payment.yaml` 21 and `winter_fuel_allowance/taxable_income_test.yaml` 18.
- Whole directories: `baseline/finance/benefit` 1,839 passed in 110.98 s and `baseline/gov/dwp` 804 passed in 49.04 s.

No existing case outside `esa_income_pension_age.yaml` needed a change.

Python (`uv run pytest -q -o xfail_strict=true -n 4 <file>`, 30-minute cap; none stopped), every test passed:

- `test_esa_income_pension_age_properties.py`: 19 passed in 56.24 s.
- `test_council_tax_reduction_carer_income_properties.py` 4 (167.82 s), `test_council_tax_reduction_claimant_family_scheme_properties.py` 3 (214.34 s), `test_council_tax_reduction_pension_credit_properties.py` 9 (370.53 s), `test_hbai_property_income_properties.py` 1 (28.27 s), `test_income_support_eligibility_properties.py` 4 (108.70 s), `test_income_support_gate_reads_claimant_or_partner_awards.py` 13 (191.23 s), `test_legacy_award_readers_properties.py` 18 (182.13 s), `test_legacy_closure_scope.py` 21 (25.66 s), `test_pension_credit_earnings_disregard_properties.py` 5 (309.12 s), `test_targeted_childcare_guarantee_credit_properties.py` 7 (123.28 s), `test_trading_loss_properties.py` 12 (148.58 s), `test_uc_legacy_closure_properties.py` 4 (104.67 s), `test_wales_childcare_offer_income_sources.py` 84 (10.08 s), `test_winter_fuel_payment_charge_means_tests_properties.py` 1 (84.17 s) and `test_winter_heating_payment_properties.py` 4 (51.24 s).
- `test_disable_simulated_benefits.py` 49 (128.20 s), `test_esa_income_abolition_properties.py` 31 (60.66 s), `test_esa_income_held_outside_award.py` 3 (15.10 s), `test_esa_income_universal_credit_claim_properties.py` 2 (68.35 s), `test_esa_permitted_work_nlw_reforms.py` 5 (26.93 s), `test_legacy_award_work_properties.py` 4 (78.98 s) and `test_mixed_age_couple_properties.py` 1 (503.51 s).
- `policyengine_uk/tests/code_health`: 3,093 passed in 176.31 s.

We ran each mutant as one change to a copy of `a1771bb2e`:

| Mutation | What fails |
|---|---|
| Guard removed from `pension_credit` | YAML cases 18 and 25 (2 failed, 23 passed); the combined property on its explicit table example, at the new assertion: the third row, case 18's man, gets £5,491.20 of ESA beside £525.20 of Pension Credit |
| The same, with the explicit table example removed | The 4 generated examples (1 in 2016 and 3 in 2026) pass: none reaches the fourth row |
| `esa_income` read in place of `claimant_or_partner_esa_income` | YAML case 12 (1 failed, 24 passed); the combined property on the outside-reporter row (Pension Credit 0, expected 8,091.20) |

Differential against `71ba27523`: a generator outside the repository draws 480 families a year from seed 20261031 (825, 840 and 834 people in 2016, 2018 and 2026), with case 18's earnings, pensions and awards among the draws, and runs each year in its own process on each tree. At `71ba27523`, 3, 1 and 0 families have both `pension_credit` and `claimant_or_partner_esa_income` above nil. Every other family gives the same `pension_credit`, `esa_income`, `claimant_or_partner_esa_income`, `household_net_income` and `household_benefits` on both trees. Each of the 4 families with both loses its Pension Credit and nothing else, and household net income falls by the same amount:

| Year | Family | Couple's ESA | Pension Credit, `71ba27523` → `a1771bb2e` | Household net income, `71ba27523` → `a1771bb2e` |
|---|---|---|---|---|
| 2016 | 99 | 5,075.20 | 525.20 → 0 | 15,209.40 → 14,684.20 |
| 2016 | 145 | 5,755.20 | 29.54 → 0 | 21,607.74 → 21,578.20 |
| 2016 | 261 | 7,500.00 | 366.60 → 0 | 21,895.60 → 21,529.00 |
| 2018 | 237 | 7,419.20 | 120.02 → 0 | 24,164.71 → 24,044.70 |

On the same populations B(0) (`pension_credit_without_income_related_esa`) gives the same value for every family on both trees, no branch outlives the calculation, and each tree opens the same branches: 10 in 2016 and 2018 (2 ESA branches) and 8 in 2026 (3 ESA branches). The guard reads `claimant_or_partner_esa_income`, which an ESA branch sets as an input, so the branches read the same variables.

Other checks:

- [x] `.github/check_fix_pr.py` on this title and body, as `pr_fix_evidence.yaml` runs it, against the changes from the `main` merge base (`0080274d7`) to `a1771bb2e`: PASS, 24 changed executable tests.
- [x] `uvx ruff@0.17.2 format --check .` (2,177 files already formatted), `uvx ruff@0.17.2 check .` and `uvx ruff@0.17.2 check --select F811,F821 policyengine_uk`: all passed. `git diff --check 71ba27523..HEAD`: clean.

### Local runs at `71ba27523`

`71ba27523` rewraps one docstring of `d2ce8b54a`, and a comparison of the parsed code shows no other change. At `71ba27523`, `test_esa_income_pension_age_properties.py` 19 passed in 77.67 s, and the ruff checks, `git diff --check` and `check_fix_pr.py` pass. Every other run below ran at `d2ce8b54a`.

`71ba27523` merges #2813's head `5a5ac5fb1` and then `main` at `0080274d7` into `1d6df1a43`, with no conflict. `git merge-tree` of `71ba27523` against `main` at `0080274d7` and against `5a5ac5fb1` is clean, since it contains both. Each file ran alone, with no survey data.

YAML (`uv run policyengine-core test <file> -c policyengine_uk`), every case passed:

- `esa_income/esa_income_pension_age.yaml`: 24 passed. At the merge before the appointee savings (`15b61ba2`), cases 4, 5, 6, 12, 13 and 14 fail and 18 pass.
- The YAML test files #2813 adds or changes: `esa_income/esa_income_abolition.yaml` 36, `esa_income/esa_income_capital.yaml` 10, `esa_income/esa_income_remunerative_work.yaml` 47, `housing_benefit/housing_benefit_eligible.yaml` 6, `legacy_award_readers/council_tax_reduction_passports.yaml` 11, `legacy_award_readers/housing_benefit_pension_age_regulations.yaml` 4, `legacy_award_readers/pension_credit_earnings_disregard.yaml` 4, `universal_credit/forgoes_universal_credit_for_tax_free_childcare.yaml` 10, `gov/dwp/pension_credit/mixed_age_couples.yaml` 26 and `gov/dwp/universal_credit/person_legacy_benefits_closed.yaml` 3.

Python (`uv run pytest -q -o xfail_strict=true -n 4 <file>`, 30-minute cap; none stopped), every test passed:

- `test_esa_income_pension_age_properties.py`: 19 passed in 80.68 s.
- `test_income_support_eligibility_properties.py`: 4 passed in 142.54 s.
- `test_income_support_gate_reads_claimant_or_partner_awards.py`: 13 passed in 292.40 s.
- `test_disable_simulated_benefits.py`: 49 passed in 171.75 s.
- `test_esa_income_abolition_properties.py`: 31 passed in 115.09 s.
- `test_esa_income_held_outside_award.py`: 3 passed in 20.57 s.
- `test_esa_income_universal_credit_claim_properties.py`: 2 passed in 107.66 s.
- `test_esa_permitted_work_nlw_reforms.py`: 5 passed in 37.92 s.
- `test_legacy_award_readers_properties.py`: 18 passed in 249.39 s.
- `test_legacy_award_work_properties.py`: 4 passed in 112.84 s.
- `test_legacy_closure_scope.py`: 21 passed in 34.44 s.
- `test_uc_legacy_closure_properties.py`: 4 passed in 147.61 s.
- `test_mixed_age_couple_properties.py`: 1 passed in 895.28 s.
- `policyengine_uk/tests/code_health`: 3,093 passed in 244.70 s.

The four changed property nodes, at the merge before the change: each fails as on the merged train (4 failed in 439.44 s), at line 691 (`[0.0] == [5000]`), line 1141 (`[False, False, True, False]`), `legacy_award_work_reference.py:254` (a couple aged 58 and 65) and line 469. At `d2ce8b54a`: 4 passed in 58.75 s. In the combined property, 1 of the 4 generated examples falls in 2026.

We ran each mutant as one change to a copy of `d2ce8b54a`, against the four changed nodes:

| Mutation | Nodes that fail |
|---|---|
| s.1(3)(c) dropped from `esa_income_eligible` and from the own-right claim in `esa_income` | combined, no branch, screen reads |
| The Pension Credit condition passes every couple | combined, parameter reform |
| Age and take-up conditions dropped for the model's formulas | no branch, screen reads, parameter reform |
| Early return removed | screen reads |

Other checks:

- [x] `.github/check_fix_pr.py` on this title and body, as `pr_fix_evidence.yaml` runs it: PASS, 24 changed executable tests.
- [x] `uvx ruff@0.17.2 format --check .` (2,177 files already formatted), `uvx ruff@0.17.2 check .` and `uvx ruff@0.17.2 check --select F811,F821 policyengine_uk`: all passed. `git diff --check 1d6df1a43..HEAD`: clean.
- [x] Against #2813 merged with `main`, `71ba27523` changes the same 14 files that #2807 changes against `origin/main` (2da212abc).

### Earlier runs

After `b6ddfcf0d`, two commits change the screen's code: `09e0c0872` compares whole Pension Credit definitions, and `3fc1e1d78` checks take-up for a formula. The rest change tests, comments, the changelog fragment and the Housing Benefit file, which returns to `origin/main`'s. On the model's own formulas and under a parameter reform, `pension_credit_follows_model_formulas` returns true at `abdcede2d` wherever it did at `cef744bc2`. In a fresh system, a clone and a simulation's system, each of the five variables matches a fresh instance of its class in 2016 and 2026 (30 of 30 comparisons), and `would_claim_pc` has no formula. Results at earlier commits below cover that unchanged path. Each ran before this branch contained #2813.

- [x] YAML cases from the legislation: `esa_income_pension_age.yaml`, 24 cases, each with its arithmetic. All 24 pass at `eec9f6480` in 42.91 s. On `origin/main` 14 fail (cases 1, 2, 3, 7, 9, 10, 11, 13, 14, 15, 19, 21, 22 and 23) and 10 pass; on `0d157f078` cases 21, 22 and 23 fail. Cases 15 to 19 take the four pairings of B(0) and B(e) in the table above. Case 16 takes a man aged 63 in 2016 with £115.50 a week of permitted earnings and a £61 pension: he gets no Pension Credit and keeps £4,919.20 of ESA, and case 17 gives him no Pension Credit claim and the same ESA. In case 20 a couple aged 66 and 58 in 2018 has a £200 weekly pension and £124 a week of permitted earnings, and keeps £2,537.60 of support-group ESA and both benefit cap exceptions, so no Universal Credit cap applies. Cases 3 to 5 build the saving from facts: the older member's birth date, 1 March 1953, and the couple's Pension Credit report. Case 14 gives only the older member of an unsaved couple aged 70 and 60 the support component, so neither cap exception applies. Cases 21 to 24 enter Pension Credit or a part of it: £2,000 of `pension_credit` for a man aged 63 who would not claim (no ESA), £2,000 of `pension_credit_entitlement` for a man aged 40 (no ESA), `is_pension_credit_eligible` for a man aged 40 with no income (the £8,091.20 minimum guarantee and no ESA), and £2,000 of `guarantee_credit` for the man aged 63 who would not claim (no Pension Credit, so he keeps £5,000 of ESA).
- [x] Property and example tests: `test_esa_income_pension_age_properties.py`, 19 tests, all passed at `eec9f6480` in 1,554.74 s. On `origin/main` the file stops at import (`NO_INCOME_RELATED_ESA_BRANCH`), and on `0d157f078` it stops at import too (`pension_credit_follows_model_formulas`).
  - At `eec9f6480` the combined property checked invariants 1 to 5 against a second simulation of the same people in which nobody reports ESA. It drew earnings, pensions, savings and working tax credit over 2016, 2018 and 2026, in 4 generated examples of 4 to 12 families, and an explicit example added the four table cases and an outside reporter.
  - At `eec9f6480` the differential (invariant 6) ran 2 generated examples and the explicit one through a reform that calculates the model's Pension Credit by a formula of its own, and compared 6 outputs with the model's.
  - Example tests for invariants 1 and 7, each with its own families:
    - £2,000 of entered Pension Credit for the man aged 63 who would claim, the same man who would not and a man aged 40: no ESA for any of them.
    - Under a reform in which every family gets £2,000 of Pension Credit: no ESA for the same three.
    - Under a reform with the mixed-age exclusion in force in 2018 and Pension Credit frozen at the baseline's award: a couple aged 70 and 60 keeps the £12,937.60 baseline award and gets no ESA.
    - Under a parameter reform without the 2026 exclusion: the same couple gets £18,889 and no ESA, and a working-age claimant alone opens no branch.
    - An input for each of the five Pension Credit variables, one at a time.
    - In a 2026 population with no couple's claim that passes the age, work and capital tests, the screen reads neither the age conditions nor the claim; add a claimant who passes, and it reads both.
  - Under each of five reforms, a family outside a Pension Credit variable's `defined_for` gets something other than the model gives it, in 2016:
    - £2,000 of entitlement by default: the man aged 40 gets £2,000 of Pension Credit and no ESA;
    - the same, with Pension Credit calculated by a formula of the reform's: the same results;
    - £2,000 of Pension Credit by default for a family not eligible for it: £2,000 and no ESA;
    - eligibility by default for a family that fails the age conditions: the £8,091.20 minimum guarantee and no ESA;
    - the guarantee credit and entitlement for every family with income within the minimum guarantee, a change of coverage alone: £8,091.20 and no ESA.

    In each, the man aged 63 who would not claim keeps his £5,000, and the man aged 63 who would claim gets £8,091.20 and no ESA. With the model's own five definitions registered again, the screen keeps the age and take-up conditions, and the man aged 40 keeps his £5,000.
  - Under a reform in which a family claims Pension Credit while it gets no income-related ESA, with Pension Credit by the model's formula and by a formula of the reform's: ESA [0, 5,000, 0], Pension Credit [8,091.20, 0, 8,091.20] and take-up [true, false, true], with no `CycleError`.
  - Exhaustive over the five variables and twelve attributes: each of seven changes to what Core calculates a variable from (default, `defined_for`, neutralization, formula, `adds`, period and end date) drops the age and take-up conditions, and each of five descriptive changes (label, documentation, reference, unit and metadata) keeps them. A take-up formula drops them; a take-up default or input keeps them.
- [x] Mutations of the entered-award test and the early return, each caught. Each mutant changes one line or block in a copy of a commit from `b6ddfcf0d` to `cef744bc2`, which share the screen's code, and runs against the tests the table names:

| Mutation | YAML cases that fail | Python tests that fail |
|---|---|---|
| Age and take-up conditions kept for entered and reformed Pension Credit | 21, 22, 23 | entered award, reform for all |
| Age and take-up conditions dropped for the model's formulas too | not run | parameter reform, no branch |
| Inputs not counted | 21, 23 | entered award, each entered part |
| Replaced formulas not counted | not run | reform for all |
| Freeze not checked | not run | frozen award |
| Entitlement not checked | 22 | each entered part |
| `is_pension_credit_eligible` not checked | 23 | each entered part |
| Guarantee and savings credits not checked | not run | each entered part |
| Early return removed | not run | screen reads |
| Early return keyed to any claim, an outside member's included | not run | screen reads |
| Entered Pension Credit read only for a couple that would claim | 21 | entered award |
| Entered Pension Credit read only for a couple that meets the age conditions | 22, 23 | entered award |

- [x] Mutations of the definition and take-up checks, each caught. Each mutant changes one line of an export of `abdcede2d`, and runs at a 2 GB cap against the tests the table names. The entitlement row ran on `eec9f6480`, whose tests match `abdcede2d`'s and whose code differs only in docstrings:

| Mutation | Python tests that fail, and how |
|---|---|
| Default not compared | entitlement default (ESA [5,000, 5,000, 0]); every change |
| `defined_for` not compared | guarantee credit coverage (ESA [5,000, 5,000, 0]); every change |
| Take-up formula not checked | take-up formula (`CycleError`); every change |
| Entitlement left out of the compared variables | entitlement default (ESA [5,000, 5,000, 0]); every change |
| Inputs not counted | entered award (ESA [0, 5,000, 5,000]); each entered part |
| Formula not compared | reform for all (ESA [0, 5,000, 5,000]); every change |

- [x] `1d6df1a43` rewords three docstrings from `abdcede2d`. A comparison of the parsed code shows no other change, so the runs named at `abdcede2d` hold for it.
- [x] A matrix of 23 input and reform cases at `abdcede2d`, run against the same fixtures as at `cef744bc2`. The three default reforms give the man aged 40 no ESA (ESA [5,000, 0, 0], where `cef744bc2` paid [5,000, 5,000, 0]), and take-up that reads ESA gives ESA [0, 5,000, 0] and Pension Credit [8,091.20, 0, 8,091.20], where `cef744bc2` raised `CycleError`. Two take-up formulas that return a constant now drop the age and take-up conditions and give the same awards. The other 16 cases give the same ESA, Pension Credit and shortcut as at `cef744bc2`, among them the 2026 parameter reform (ESA [0], Pension Credit [18,889], age and take-up conditions kept) and the entered and replaced Pension Credit cases. In a population of the man aged 40 alone, the entitlement default reform gives him no Pension Credit, so he keeps his £5,000.
- [x] The two-copy entered-award check at `abdcede2d`: two men aged 63 with £2,000 of entered Pension Credit and opposite take-up get ESA [0, 0] and Pension Credit [2,000, 2,000] (1 passed in 217.22 s), and its YAML form passes (1 passed in 25.30 s).
- [x] Hand-built datasets through the aggregate-impact guard at `abdcede2d`: a household already paid Pension Credit and three families with none give the same 92 baseline fiscal heads, perturbations, changed heads and counted-once verdicts as at `cef744bc2`, and the same baseline heads and verdicts as at `0d157f078`. Pension Credit passes the counted-once check, and resetting the baseline reproduces it. In the second fixture the synthetic £1 of Pension Credit also takes the middle family's £5,000 of ESA (ESA change [0, -5,000, 0]).
- [x] Mutations of the bar at `d37e42de9`, each caught:

| Mutation | YAML cases that fail | Property tests that fail |
|---|---|---|
| Bar on the age conditions and the claim alone | 16, 18, 20 | combined, branch |
| Bar on Pension Credit with the award, B(e) | 18, 19 | combined, branch |
| Bar on entitlement without the claim | 5, 8 | combined |
| Bar reaches a member outside the couple | 12 | combined, no-branch |
| Branch left in `simulation.branches` | none | branch |
| Branch opens whatever the population | none | no-branch |
| Both benefit cap payee masks removed | 14 | none |
| s.1(3)(c) dropped from the screen | 9, 11 | stopped at the 1,500 s limit |
| `is_on_income_related_esa` as on `origin/main` | 11, 13, 14 | stopped at the 1,500 s limit |

- [x] Differential against `origin/main` (invariant 8), at `d37e42de9`: 480 synthetic families a year (785, 794 and 810 people) in 2016, 2018 and 2026, from seed 20261031, through both trees. All 243, 262 and 303 families to which the model applies neither new refusal give the same values as `origin/main` on 16 benefit-unit outputs, `is_on_income_related_esa`, `household_net_income` and `household_benefits`. No family gains income-related ESA. Some families have an open couple report, the Pension Credit age conditions and a claim, and no report from anyone past pensionable age: 23, 9 and 3 of them. Of these, 5, 2 and 0 lose the couple's award, 11, 2 and 0 keep it at `origin/main`'s amount, and 1, 0 and 0 get both awards. On `origin/main` 49, 35 and 36 families got both. The population in the cost section gives the same 24 outputs at `b6ddfcf0d` as at `0d157f078` in all five years.
- [x] Existing YAML files whose cases report ESA and enter Pension Credit or a part of it, each alone at `2ee68e799`: 232 cases, all passed (`relevant_benefit_receipt.yaml` 40, `warm_home_discount.yaml` 17, `pawhp.yaml` 27, `warm_home_discount_receipt.yaml` 7, `housing_benefit_pension_age_means_test.yaml` 5, `legacy_award_readers/passports.yaml` 20, `winter_fuel_allowance/taxable_income_test.yaml` 18, `housing_benefit_specified_or_temporary_accommodation_disregard.yaml` 50, `housing_benefit/housing_benefit_income_related_benefit_passport.yaml` 7 and `winter_fuel_payment_charge.yaml` 41). At `d37e42de9`, `baseline/finance/benefit` 1,793 passed and `baseline/gov/dwp` 804 passed.
- [x] Python files that read the input register, at `2ee68e799`: `aggregate_impact/test_aggregate_impact_guard.py` 39 passed and `test_input_records.py` 11 passed.
- [ ] Python files that read the screen, each alone with a 3,000 s limit, one at a time, at `b6ddfcf0d`. None failed; CI runs each in full:
  - `test_income_support_gate_reads_claimant_or_partner_awards.py`: 7 tests passed, then the limit.
  - `test_income_support_eligibility_properties.py`: 1 test passed, then the limit.
  - `test_income_support_esa_entered_directly.py`: 18 tests passed, then the limit.
  - `test_income_support_direct_inputs.py`: 17 passed in 1,801 s.
  - `test_legacy_award_work_properties.py`: 1 test passed in the 24 minutes we ran it; CI must complete this file.
  - `test_esa_income_universal_credit_claim_properties.py`: 2 passed in 2,410.51 s at `cef744bc2`, with a 3,000 s limit.
- [x] At `e7f5b0a58` and `d37e42de9`: `test_claimant_or_partner_awards_entered_directly.py` 25 passed, `test_entered_directly_lifecycle.py` 16, `test_reported_legacy_award_screening.py` 18 and `test_esa_permitted_work_nlw_reforms.py` 5.
- [x] `ruff format --check`, `ruff check` and `ruff check --select F811,F821 policyengine_uk` on the changed Python files, and `git diff --check`.
- [x] `git merge-tree --write-tree --name-only` at `eec9f6480` against `origin/main` (2da212abc), #1969 (3a3241cf6), #2756 (c34662736), #2763 (6ca062e68), #2776 (1d4a3e475) and the #2562 branch (68ccc0856): no conflict.
- [x] Changelog fragment `changelog.d/2565.fixed.md`.

The shared test reference takes the couple's entitlement to Pension Credit: `legacy_award_work_reference.esa_screen` gains an `on_pension_credit` argument. The Income Support property tests pass it the couple's claim and the model's `pension_credit`, and their own s.124(1)(g) reading keeps the age conditions and the claim; the gate test reads Pension Credit only for its ESA reference. No YAML expected value outside the new file changed.

## Reproducer

Reproducer: `policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_pension_age.yaml`; `policyengine_uk/tests/test_esa_income_pension_age_properties.py::test_couple_esa_follows_pension_credit_worked_out_without_it`; `policyengine_uk/tests/test_esa_income_pension_age_properties.py::test_an_entered_pension_credit_award_bars_the_award_whatever_the_age_and_take_up`; `policyengine_uk/tests/test_esa_income_pension_age_properties.py::test_a_reform_that_pays_outside_the_age_conditions_bars_the_award`; `policyengine_uk/tests/test_esa_income_pension_age_properties.py::test_a_take_up_formula_makes_the_model_read_pension_credit_for_every_couple`

axiom: TheAxiomFoundation/rulespec-uk#515 queued

🤖 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: 3f7735fd6ad7bc4eb2bc52e4773dacf95b66c118 on claude/pr-2807-review-2813-ddonjt (no PR; CI not started). It is 741054b16 plus one merge of main at 47a2817e2. I added no commit of my own to the repository files: the six inherited commits already hold every repository item, and the rest of the job was in the three hub documents, posted above (PR_TITLE.txt 6651005552, AXIOM_515.md 6651005823, PR_BODY.md 6651798003). Work record: 6650882169.

The six commits above d0ffe7fd3

Commit Items Holds?
a8f46e3d8 merges dc69f3ada 1 Yes. Parents d0ffe7fd3 and dc69f3ada; its tree equals git merge-tree --write-tree d0ffe7fd3 dc69f3ada (16155d7a1), so no hand edit. I wrote "Overlap with #2813" myself (below).
6cd70815a couple and 2018 example 3 (P3-1), P3-9 "2018 cover" Yes. COUPLE_ROW and the examples() change are the review's text word for word. ROWS_2018 = case 19's couple (ROWS[3]) and case 20's couple, added as @example((ROWS_2018, 2018)) to the combined property and the differential. I checked its comment by simulation: in 2018 case 19's couple gets B(0) 955.40 and B(e) 7.60 (Pension Credit with or without the award), and case 20's couple gets B(0) nil (its YAML works B(e) nil).
0822532e4 text 4, 5 (P3-2, P3-3 case 18 and docstring, P3-4 case 18, P3-6 all YAML parts, P3-7, P3-8 all three, P3-12 docstring) Yes. Case 26's name is P3-2's text; case 25 takes the optional name. Case 26 now reads "more than £20 short of 16 x £7.20 = £115.20", "£392.60", the review's tax comment and "WRA 2007 Sch 1 para 6(1)(f)" (cases 25 and 26). Case 19 takes P3-8's text; esa_abolition_claim takes P3-8's text; the eight comments say "the claimant's appointee saving". The docstring of pension_credit_without_income_related_esa names five reads, pension_credit's own first (P3-12's last point), and says which reading the model's result agrees with (item 4) in place of the review's "on the model's reading". Older names in cases 12, 15, 16, 18, 19 and 20 now give DWP, HMRC or the couple the verb.
44ea4c161 text 4, P3-12 item 1 Yes. Case 18 says "The expected values agree with the reading under which …" (item 4, not the review's "The model takes"). pension_credit's guard comment takes P3-12's direction: the formula pays nothing so no couple gets both, since Parliament allows the award only without Pension Credit. Case 25's "£366.60" gains its pound sign.
9327cec76 merges main at 95dfc62f8 — Yes. Tree equals the merge-tree (d3ada7f82).
741054b16 text P3-7, P3-8 Yes, with one judgment call: three docstrings (:556-561, :791-796, :1227-1231) say "the model keeps each report open for the whole of 2026-27 (esa_income_payable_share is 1)" instead of P3-7's "DWP keeps each award in payment …". That describes the code (esa_income_payable_share.py:59-64 gives a saved claim 1), so I kept it. The docstring of pension_credit_without_income_related_esa gains para 4(2)(b)'s condition for the other reading.

The case 13 and 14 comments on the older member ("The model applies this member's appointee saving to the couple's whole claim (esa_income_payable_share), as it does the claimant's below") match esa_income_payable_share's formula.

Findings

  • P2-1 (A2). Settled in PR_BODY.md, Test plan: the first paragraph now says the local runs at 3f7735fd6 cover every test file this PR or Model the 2026 income-related ESA abolition and its UC claims #2813 changes against main, and "Local runs at 3f7735fd6" lists all 33 with counts (below). No commit; the runs are at 3f7735fd6.
  • P3-1 (A3). 6cd70815a. M5 below.
  • P3-2. 0822532e4 (esa_income_pension_age.yaml case 26 name; case 25's optional name too).
  • P3-3. Case 18 44ea4c161 (esa_income_pension_age.yaml:651-663); docstring 0822532e4 and 741054b16 (esa_income_eligible.py:90-115); the record's "What holds in PolicyEngine" takes the review's sentence. Item 4 in the body and the record: "PolicyEngine's result agrees with the reading under which …", and the body adds "No formula applies para 4(2) (pension_credit_earnings_disregard lists it as not modelled)", which I read at pension_credit_earnings_disregard.py:27-31.
  • P3-4. "not a fixed £20": case 18 (0822532e4), body (twice), record test 8 (twice).
  • P3-5. Body: "is in part-time employment", "one of the regulations Sch 7 para 7(2) lists applies to the claimant (subject to para 7(3))", case 25's "if that work is part-time employment", and "while he stays in employment (para 4(2)(b))".
  • P3-6. YAML in 0822532e4; body case 26 bullet "more than £20 short of 16 × £7.20 = £115.20", and the bullet also names is_married and would_claim_marriage_allowance (I read both: is_married returns is_couple, would_claim_marriage_allowance defaults to true).
  • P3-7. 0822532e4 and 741054b16, including the older names (cases 15, 16, 18, 19; case 20's "Pension Credit refuses"; case 12's "does not bar"). Left: the Hypothesis event() labels in the combined property ("Pension Credit pays without the award", "Pension Credit refuses a couple with a report", "Pension Credit pays only with the award", :412-415). They are strings in the test body, from which Hypothesis seeds its derandomised draws, so rewording them would move every generated example; I left them for a pass that can rerun the mutants.
  • P3-8. All three in 0822532e4 (YAML case 19, legacy_award_work_reference.py:232-237, the eight comments); body bullet now ends "and the model ignores a saving held only by a member who reports nothing".
  • P3-9. Body: "It pays the reported award, esa_income 5,491.20."; the 419bccb44 bullet names the docstring of pension_credit_without_income_related_esa, pension_credit's documentation string and "gives DWP or the formula the verb"; the d0ffe7fd3 bullet adds examples()'s comment and case 18's "as under para 5 DWP disregards"; the a1771bb2e mutant row reads "(1 in 2016 and 3 in 2026)"; "Earlier runs" puts the combined property and the differential in the past tense at eec9f6480. 2018 cover: ROWS_2018 (rows: case 19's couple, ROWS[3], and case 20's couple). Statistics at 3f7735fd6: the combined property draws 1 example in 2016 and 3 in 2026; the differential 1 and 1; neither draws 2018, so the explicit example supplies it.
  • P3-10. Cost sentence replaced with the review's text, plus my run at 741054b16: a couple aged 74 and 61 (mixed-age saving set as an input, younger member reports £5,000) opens 2 ESA branches in 2026 and in 2027 with the appointee saving, and 0 without it, where esa_income_report_closed is [True, True]. Overlap rewritten for dc69f3ada (below).
  • P3-11. AXIOM_515.md: test 8 adds "that includes the support component", "(quoted above; see the note under reg 45)" after reg 45(4) and "quoted above" after Sch VI para 5(a); the reg 45 note takes the review's two sentences; units "£10.10 a week of Pension Credit, £525.20 over 52 weeks"; heading "The amount DWP disregards where para 7 applies (para 7(1)):"; para 4(1) gains "[...]" after "(vii) employment and support allowance;", para 4(4)(a) ends "awarded; or [...]"; each excerpt heading now says it adds a space after each list label and turns line breaks into spaces; "The model takes" is gone. £7.20: cited as the National Minimum Wage (Amendment) Regulations 2016 (SI 2016/68) reg 3. I couldn't check the number on legislation.gov.uk (it doesn't resolve here); SI 2016/68 is the number the repository's parameter (non_apprentice.yaml:517-521) links to. £155.60: no instrument found (the repository cites none for 2016-17 and I can't reach legislation.gov.uk), so the companion tests' heading now claims only what the record sources: tests 1 and 3 take rates from SI 2026/148, and tests 5, 6 and 8 take £155.60 from PolicyEngine's parameter, "for which this record cites no instrument".
  • P3-12 (A4 with P3-2, P3-3, P3-4, P3-8). Body items 1 to 3 take the review's text; item 3 also names savings_credit_income (I read excluded_income.yaml:16, which lists working_tax_credit) and pension_credit_earnings_disregard's read of the disability elements (:66-75). Screen docstring: 0822532e4.
  • A2 = P2-1: settled by the Test plan at 3f7735fd6. A3 = P3-1: settled by 6cd70815a. A4 = P3-2 (0822532e4), P3-3 (0822532e4, 44ea4c161, 741054b16, body, record), P3-4 (0822532e4, body, record) and P3-8 (0822532e4, body).

Overlap with #2813 (item 1)

Of #2813's three commits since 7fe7360fa, two change a formula, both would_claim_housing_benefit's: 84935ccef added the accommodation status to a hand-entered family's take-up, and dc69f3ada takes it out again, back to claims_all_entitled_benefits | reported_hb. 3c9890a19 rewords a comment. in_specified_or_temporary_accommodation defaults to false with no formula, and none of this PR's 15 files names it (grep), so for every family this PR's tests build the take-up is the same at dc69f3ada as before 84935ccef. No result this PR asserts moves; every case and property passes at 3f7735fd6.

Tests at 3f7735fd6

No survey data; uv sync --frozen --extra dev; Python with -n 4.

  • YAML (16 files): esa_income_abolition 41, esa_income_capital 10, esa_income_pension_age 26, esa_income_remunerative_work 47, housing_benefit_eligible 15, housing_benefit_universal_credit_passport 9, would_claim_housing_benefit 6, council_tax_reduction_passports 11, housing_benefit_pension_age_regulations 4, legacy_award_readers/pension_credit_earnings_disregard 4, forgoes_universal_credit_for_tax_free_childcare 10, uc_housing_costs_element 8, uc_work_allowance 5, benefit_cap_by_scheme 22, mixed_age_couples 26, person_legacy_benefits_closed 3. All passed.
  • Python (17 files): test_benefit_cap_uc_and_hb_properties 1, test_disable_simulated_benefits 49, test_esa_income_abolition_properties 31, test_esa_income_held_outside_award 3, test_esa_income_pension_age_properties 19 (75.21 s), test_esa_income_universal_credit_claim_properties 2, test_esa_permitted_work_nlw_reforms 5, test_housing_benefit_accommodation_dates_properties 12, test_housing_benefit_accommodation_take_up_data_built 8, test_housing_benefit_universal_credit_passport_properties 6, test_income_support_eligibility_properties 4, test_income_support_gate_reads_claimant_or_partner_awards 13, test_legacy_award_readers_properties 20, test_legacy_award_work_properties 4, test_legacy_closure_scope 21, test_mixed_age_couple_properties 1 (762.86 s), test_uc_legacy_closure_properties 4. All passed; timings in the body.
  • policyengine_uk/tests/code_health: 3,166 passed in 223.79 s.
  • check_fix_pr.py from main at 47a2817e2, on the posted title and body, no labels, merge base 47a2817e2 to 3f7735fd6: PASS, 33 changed executable tests.
  • uvx ruff@0.17.2 format --check . 2,235 files already formatted; ruff check . and --select F811,F821 policyengine_uk: all passed. git diff --check d0ffe7fd3..HEAD: clean.
  • git merge-tree --write-tree --name-only HEAD origin/main (47a2817e2): clean, tree 49b8bbbe3.

Mutation M5 (guard skips couples), scratch copies of 741054b16

  • YAML: 2 failed, 24 passed (cases 25 and 26).
  • Property file as committed: killed, 1 failed, 18 passed in 86.29 s, first on the explicit 2018 example at line 428: case 19's couple gets Pension Credit 0 against B(0) 955.40. In 2018 that couple gets £7.60 with the award, so under M5 the tax credit passport branch pays it Pension Credit, which passports full working tax credit (3,970) and leaves it neither Pension Credit nor ESA. In 2016 (YAML case 19) B(e) is nil, so the YAML doesn't see this.
  • Without the 2018 example, couple kept (combined node alone, no -n): killed at assert not ((couple_esa > 0) & (pension_credit > 0)).any() on the generated 2016 example that carries case 26's couple.
  • Without both: survives (1 passed in 44.23 s).
  • M5 with only the couple taken out (2018 example kept): 1 failed, 18 passed in 71.15 s.

So the couple and the 2018 example each kill M5.

Skipped

  • Survey-data tests (no access).
  • legislation.gov.uk checks for SI 2016/68 and the 2016-17 up-rating instrument (no network to it); handled as above.
  • rulespec-uk#515 itself (outside this session's repository scope); AXIOM_515.md is posted here for the hub.
  • No new branch counts or timings for the Cost tables beyond the one couple; the tables stay labelled by commit.

Judgment calls

  • Kept 741054b16's model-as-actor wording in three docstrings (above).
  • Left the Hypothesis event labels unchanged (above).
  • Item 4 wording ("agrees with the reading") over the review's "on the model's reading" in the docstring and body, as the brief asks; the record's "What holds" keeps the review's sentence ("on PolicyEngine's reading of para 4(2)(a)"), which item 6 names.
  • Body P3-12 item 3: took the optional extra reads, each checked in code.
  • The Cost section names the mixed-age saving in the couple aged 74 and 61; without it the couple fails the age conditions in 2026 and the run shows nothing.

Generated by Claude Code

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Incremental review at 3f7735fd6ad7bc4eb2bc52e4773dacf95b66c118 — COMPLETE; all prior findings resolved, with no new findings or material evidence gaps.

All 172 focused checks passed: 99 distinct YAML cases and 73 Python cases, covering ESA age/abolition rules and affected Housing Benefit, Council Tax Reduction and final-income paths. The current locked Core 3.34.1 environment was verified. Current source explanations resolve A4; the head-specific validation record resolves A2, and guaranteed couple/2018 cases resolve A3. C1 and A1 remain resolved. Retained parent and historical evidence is counted separately.

The PR remains draft, and its stated merge-first parent #2813 is still open at dc69f3ada6b6bee045ce4db88406311ba5701fcd. Those delivery conditions remain; this comment does not approve either PR.

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

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review status: COMPLETE at eab8ebba327f21004d5c7653376dbb4867400dfc. All prior findings remain resolved; no new findings or material evidence gaps.

The 15 authored feature files match the previously reviewed 3f7735fd6 byte-for-byte. I reviewed the current main integration and updated description, retaining the valid independent policy/172-check evidence with its original qualifications. The description now dates the earlier runs accurately and recognises #2813's integration into main.

27/27 current-head checks passed on lock-matched Core 3.34.1: 26 pension-age YAML cases and the generated/differential couple property through final ESA, Pension Credit and household income. These are separate from the retained historical 172 checks. The appointee-saving fixtures, paid-PC distinction, annual abolition proxies and other documented limits remain explicit. Current #515/#518 source records remain valid queued contracts.

No approval is issued while the PR remains a draft. Hosted CI was pending at the last capture and can be checked later without repeating this completed review.

@MaxGhenis
MaxGhenis marked this pull request as ready for review November 5, 2026 12:52
@MaxGhenis
MaxGhenis requested a review from juaristi22 November 5, 2026 12:52

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review status: COMPLETE — approved at eab8ebba327f21004d5c7653376dbb4867400dfc. No findings or material evidence gaps remain.

The code head is unchanged from the completed review. I verified the retained code/source evidence and resolved findings, and assessed the current description and its primary CI provenance. No tests or model calculations were repeated for this readiness change; the historical validation and documented annual/reporting limitations retain their original qualifications.

The PR is now ready, and applicable required checks pass at this exact head. The head, description, review activity, source guards and checks are refreshed again before this approval.

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

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Completed incremental review at e2615e80b58452a1034180eaeb1141f2b9cb9309: no open findings or material evidence gaps; all 118 targeted tests passed. Approval is deferred until pending exact-head CI completes.

Program review

Base repository: PolicyEngine/policyengine-uk
PR number: 2807
Reviewed head SHA: e2615e8
Merge base SHA: b8b383c
Mode: incremental from eab8ebb
Scope: Main-merge resolution in supplied-input imports and affected ESA/Pension Credit/stored-input integration; reuse unchanged 14 feature paths and existing official evidence.
Source manifest: /privateretained review artifacts/pr-2807-sweep20261109-1359-review-sources.json
Review status: COMPLETE

Source Documents

Critical

C1 — Do not accept simultaneous Pension Credit and income-related ESA (RESOLVED)

C1 remains RESOLVED. The ESA/PC screen and final-award formulas are unchanged. The current run passes all 26 pension-age/PC YAML cases and 19 ESA property/regression calls, covering actual final ESA and Pension Credit, reported PC inputs, separate-simulation B(0), mixed-age cases, appointee-saved 2026 reports and the bar against simultaneous couple ESA and PC. The receipt-dependent disregard case remains qualified by the independently retained source interpretation.

A1–A4 also remain RESOLVED. The body SHA is still d557496979efa4a80d6b0c386344928abf0b4de72b6332069da825e9ca93e42e: the corrected stack/saved-fixture description, coverage qualifications and earnings/saving explanations have not changed. The property file retaining the fourth-row couple is byte-identical and its current checks pass. The prior sections remain the assembly selectors for these unchanged resolutions.

Coordinator assessment: Current independent incremental checks retain this prior resolution; see current validation and completed role reports.

Should Address

A2 — Qualify the PR Test plan's local-coverage claim (RESOLVED)

Prior verified resolution retained at identical code. Source guards and documented scenario/annual/input limits remain qualified. No new contradictory evidence.

Coordinator assessment: Current independent incremental checks retain this prior resolution; see current validation and completed role reports.

A3 — Guarantee a fourth-row couple in the property examples (RESOLVED)

Prior verified resolution retained at identical code. Source guards and documented scenario/annual/input limits remain qualified. No new contradictory evidence.

Coordinator assessment: Current independent incremental checks retain this prior resolution; see current validation and completed role reports.

A1 — Update the actual PR description for the abolition stack and saved fixtures (RESOLVED)

Prior verified resolution retained at identical code. Source guards and documented scenario/annual/input limits remain qualified. No new contradictory evidence.

Coordinator assessment: Current independent incremental checks retain this prior resolution; see current validation and completed role reports.

A4 — Correct policy explanations of earnings disregards and ignored savings (RESOLVED)

Prior verified resolution retained at identical code. Source guards and documented scenario/annual/input limits remain qualified. No new contradictory evidence.

All131 technicalmembers and37 source originals/each boundextract were hash-verified and rehomed undercurrentR. No newacquisition. Current20latest-nameCIchecks allsuccess andPRReady; onlyjuaristi22requested, noassignee/newsubstantivehumanfeedback. Root owns fresh applicable rules/head/body/activity/sourceguard and approval publication. No state/config/GitHub write.

DONE 2026-11-05T13:24:33.943080+00:00; originalheartbeat elapsed763.64s from13:11:50.300UTC, noreset.

Coordinator assessment: Current independent incremental checks retain this prior resolution; see current validation and completed role reports.

Suggestions

None.

Evidence Gaps

No material gaps reported.

Notes

  • Exact captured head and merge-base are recorded. Imported main behavior outside affected dependencies reuses prior evidence.
  • Approval deferred while exact-head hosted Python CI remains pending; the completed code/source review can be reused for a later CI-only approval gate.
  • Current local execution is the locked Python3.13/Core3.34.1 environment only; no all-platform qualification is claimed.
  • Appointee-saved2026 reported awards remain conditional model inputs, not dataset incidence or blanket statutory entitlement certification.
  • The complete historical27-pass and older172-pass counts are dated evidence; they are not added to the new118.
  • C1 and A1–A4 remain resolved; the sole merge conflict preserves cache and Tuple imports in supplied_inputs. Other14 feature paths and the PR body match the prior approved head.

Validation Summary

118 current targeted tests passed; no failures, skips or expected failures in the focused campaign. 37 retained originals and 37 extracts validated; no new source acquisition or rendering. 277 immutable technical receipt members SHA-256 checked before cleanup. Current ESA/Pension Credit final-award YAML, appointee-saving pension-age/property cases and stored-input/IS-gate regressions passed. No new native model diagnostic or population run.

Timing

setup seconds: 156.60s; consolidation cleanup seconds: 79.42s; elapsed seconds: 828.82s

Review Severity

APPROVE. Open findings: 0 critical, 0 should address, 0 suggestions.

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approve at e2615e80b58452a1034180eaeb1141f2b9cb9309. The pending-CI gate from the completed incremental review has cleared: all 20 latest exact-head contexts passed, including Lint and Test / Result. The completed review has no open findings or material gaps; its 118 targeted passes are retained, with no repeated code review or model execution.

The current head, description, human activity, source dependencies, readiness and applicable required checks are refreshed before this approval.

@juaristi22
juaristi22 added this pull request to the merge queue Nov 10, 2026
Merged via the queue into main with commit 96a0341 Nov 10, 2026
20 checks passed
@juaristi22
juaristi22 deleted the fix-2565-esa-pension-credit branch November 10, 2026 12:18
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.

Income-related ESA is paid alongside State Pension Credit (WRA 2007 s.1(3)(c), Sch 1 para 6(1)(c)-(d))

3 participants