Repository navigation
Model the 2026 income-related ESA abolition and its UC claims - #2813
Conversation
Add gov.dwp.ESA.income.awards_payable, a 1/0 flag per jurisdiction with fiscal_year_blend, so income-related ESA awards end on 1 July 2026 in Great Britain and 1 October 2026 in Northern Ireland. esa_income pays each award for esa_income_payable_share of the year, and esa_income_report_closed ends the report once none of the year is payable. esa_income_appointee_saving keeps an award saved under art. 3A(2). own_report_is_paid reads the same closure as the award. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a UC claim Hand-worked YAML cases for 2025-26, 2026-27 (86/365 in Great Britain, 178/365 in Northern Ireland) and 2027-28, the appointee saving, a saved award with a Universal Credit claim, contributory-only and mixed awards, a family the legacy closure already moved and a non-dependent adult's own award. forgoes_universal_credit_for_tax_free_childcare counts an ESA report as a legacy award a claim would end only while some of the year is payable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Thirteen YAML cases in 2026-27 and 2027-28 test capital, work, passports, Housing Benefit regulations and the Pension Credit disregard on an income-related ESA award. The abolition now ends or shortens those awards, so each reporter gets esa_income_appointee_saving (SI 2025/1148 art. 3A(2)), which keeps the award payable all year; expected values are unchanged. The UC-claim property test saves every award, so 2027 still tests art. 3A(3). Add the abolition property tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bolition Python tests that read income-related ESA in 2026-27 or 2027-28 for another rule give their reporters esa_income_appointee_saving (SI 2025/1148 art. 3A(2)): the NLW permitted-work reform, the Merton non-dependant properties and the legacy-closure scope cases. Under disable_simulated_benefits an outside adult's own income-related ESA now ends with the abolition, as income-based JSA ends with its scheme, so that test expects it. The property test runs each population once with three copies (saving as drawn, every award saved, none saved) and checks that a reform keeping the parameter at 1 equals saving every award. Add the changelog fragment and a paragraph on the abolition to docs/engineering/esa-jsa-report-screening.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mber's status Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ordinary parameters take their 30 April value; the flag's fiscal_year_blend day-weights each fiscal year. Drop a sentence about dated reform keys that the current reform routes no longer follow, and say plainly that the appointee saving is off in dataset runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lose it from 2027-28 - esa_income_abolition_reached: the claimant's or partner's unsaved income-related ESA claim is payable for less than the whole year; it joins legacy_benefits_closed in claims_uc_at_legacy_closure. - legacy_award_is_closed closes an ESA report whose claim has nothing payable, under the Income Support and JSA Pension Credit guard. - esa_income_payable_share is the claim's share: a couple's claim is saved when either member who reports it is saved. - own_report_is_paid lets a stored award, not the abolition closure, decide an outside member's status, so disable_simulated_benefits keeps them on the award it pays; the reform holds the new claim term. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… claim - esa_income_abolition.yaml: share cases opt out of the claim; new cases for the claim (GB, NI, couples, a protected mixed-age couple), the 2027-28 move off legacy benefits, saved claims, a saved partner who reports nothing, and the Pension Credit age guard. - Property tests: an independent reading of each award from the regulations, one share per couple, saving a non-reporter changes nothing, and the claim path never gives income-related ESA. - The UC closure and work-screen references model the claim; families in tests about other rules set would_claim_uc_at_legacy_closure false or the saving. - disable_simulated_benefits keeps an outside adult on the ESA it pays. - own_report_is_paid reads the benefit unit population for the award. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aid's signature Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a legacy closure A family that supplies in_specified_or_temporary_accommodation keeps an existing Housing Benefit award when DWP moves it off a closed legacy benefit: SI 2014/1230 reg 46(2) (NI: SR 2016/226 reg 47(2)) keeps such an award at the deadline whichever legacy benefit closed, and SI 2025/1148 art. 7(4)(a) reaches the same cases through reg 6A(2). A Universal Credit claim still ends the award, because the model's Universal Credit pays a housing costs element for these homes. Tests cover the ESA closure and the tax credit closure, each with a control that leaves the status unset, and a family with the status that claims. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… amounts are the formula's own_report_is_paid now applies the abolition's report closure to an outside member's status only when the stored esa_income and the claimant's and partner's part (claimant_or_partner_esa_income) both match the formula on open reports (holds_formula_award). Under disable_simulated_benefits, an entered award or a replacing reform, a held couple part of £5,000 against a formula's £5,200 identifies the outside adult's held £200, so their status no longer depends on whether the claimant's own report happens to give the held total. The new test holds the awards three ways and varies the claimant's 2027-28 report across £5,199, £5,200 and £5,201. Invariant 12 checks that without a reform both amounts match the formula for every generated benefit unit, so the extra comparison changes no baseline result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e who acts in its documentation esa_income_abolition_reached lists every input it reads: the claimant's and partner's ESA and Pension Credit reports, the household's country, the saving, the Pension Credit age conditions and mixed-age couple status. would_claim_uc_at_legacy_closure says the certified data (1.58.1) carry no draw and that PolicyEngine/policyengine-uk-data#534 plans one. Documentation and comments introduce the Department for Communities by name and give the model, not a share or a year, the paying verbs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gelog Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in active voice Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Name the benefit cap and the nil claim where the docs say Housing Benefit beside a claim would pay the rent twice, and state that the benefit unit's held award decides an outside member's status. Words only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Aggregate impact on certified data: passBase Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 44 (thresholds in New warnings
Notes
Moves flagged for review
4 more flags are in the report artifact. Spending by benefit and service
Caseloads
Government balance, each component counted once
Model totals
Household income totals
Poverty rates
People in poverty
Inequality
Mean household net income in each side's own deciles
Standard reform costings (change in gov_balance)
The report suppresses a reform's costing and target change in these years, for the reasons below (
Standard reforms: change in each reform's target
Household net income by decileMean change in household net income per household, and as a share of the decile's base income; households stay in the base run's deciles.
No change in any year: receipts by tax head; population. Checks failing on both base and head (1)
Counted-once checkEach of 92 components was raised by £1 per unit in 2026-27, and every head of its totals was read household by household: its own head must rise by exactly £1, no other tax head may move, no other benefit may rise, and the model's own total must move by what its heads moved. A head that breaks a rule is measured again twice: with every other head held at its baseline value, and with every other head held at its value after the first £1 while the component rises again. It fails where either still shows the response. 0 failed; 0 were skipped.
Provenance
Dataset: Receipts by tax head: head levels
Spending by benefit and service: head levels
Caseloads: head levels
Government balance, each component counted once: head levels
Model totals: head levels
Household income totals: head levels
Population: head levels
Poverty rates: head levels
People in poverty: head levels
Inequality: head levels
Mean household net income in each side's own deciles: head levels
Standard reform costings (change in gov_balance): head levels
Standard reforms: change in each reform's target: head levels
|
A member who reports nothing has no ESA report to close, so esa_income_report_closed can differ from person_legacy_benefits_closed for them with no effect on any award. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
juaristi22
left a comment
There was a problem hiding this comment.
Reviewed 5a5ac5fb1c7547b08a0b6dc042c4e8e253050abf. Two blockers remain.
C1 — Preserve temporary-accommodation Housing Benefit on the new UC migration path. The new ESA-abolition trigger induces a UC claim, but housing_benefit_eligible.py:108 still removes HB even for qualifying temporary accommodation. UC then includes that rent and caps it. In a native 2026 case—a40-year-old London WRAG claimant in qualifying council-arranged homelessness accommodation, £30,000 eligible annual rent, £5,000 reported ESA, no earnings/capital/other cap exemption, would_claim_uc=false and migration take-up=true—the head pays HB£0 and UC£16,967. Final household income is £15,387.
TPreg8(3) preserves this HB after a UC claim; UCSch1 paras3(i)/3B excludes this rent from UC, and HBreg75F(1)(g) exempts HB while UC is received. An otherwise identical, explicitly source-backed control changing only those three routing intermediates calculates HB£30,000 plus UC£5,098.80, and household income £33,518.80: £18,131.80 more. Please compose the new migration trigger with both housing rules and cover the capped combined final award. The underlying limitation predates this PR, but this PR adds the affected claim path and a new test accepting HB's loss; the documented dependency on #1910 leaves this path incorrect.
C2 — Include the new no-claim HB saving in the queued parity record. The changed HB continuation implements GBreg46(2) and NIreg47(2). Queue518 quotes only the run-on limbs46(1)/47(1), expressly excludes wider migration, and contains no HB-saving output or companion case/addendum. Those provisions are absent at the pinned rulespec main. docs/engineering/skills/github-prs.md:141–157 requires the changed provision's dispatch-ready module/corpus target, operative quotation, outputs and same-source tests. Add the HB savings/cases to the issue or link a complete companion record. Completing a billed encoder run is not required for this correction.
Review COMPLETE.82 focused tests/diagnostics passed; two expensive properties were deliberately excluded locally, with the broader frozen suite green at this head. The temporary-accommodation diagnostic confirms the counterexample; passing the new HB-loss expectation does not resolve it.
|
Picking this up for the UK hub. Reply here if you're mid-change and I'll stop. Generated by Claude Code |
…Universal Credit Universal Credit leaves that rent out of its housing costs element (UC Regs 2013 Sch 1 paras 3(h), 3(i), 3A and 3B; SR 2016/216 Sch 1 paras 3(e), 3(ea), 4 and 4A). Housing Benefit for it survives a Universal Credit claim (SI 2014/1230 reg 8(3); SR 2016/226 reg 6(3)) and a migration deadline (reg 46(2); reg 47(2)), and a family there can make a new claim (reg 6A(2); reg 4A(2)). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6JY861guYKTfTZajLq
|
Generated by Claude Code |
|
Generated by Claude Code |
|
Ready for hub review Head: C1: route the rent the way the law does
Cases and figures (each worked by hand in its YAML comment from the parameter files)
Properties (
|
| Mutant | Result |
|---|---|
| Element not nil for the input | killed: 8 cases, R1 |
| Reg 8(3) route dropped | killed: 5 cases, R2 |
| Reg 46(2) route dropped | killed: 2 cases (2027-28 move, tax credit closure), R2 |
| New-claim route dropped | killed: 2 cases, R2 |
| Take-up guard removed | not applicable: I added no guard, and would_claim_housing_benefit still decides take-up |
At 5a5ac5fb1 the 8 new routing cases fail and the 3 controls pass. R1 and R2 fail there and R3 passes, because the cap needed no change.
Tests at 7fe7360fa
- YAML:
esa_income_abolition.yaml39;housing_benefit_eligible.yaml10;housing_benefit_residential_capital.yaml7;…specified_or_temporary_accommodation_disregard.yaml50;gov/dwp/benefit_cap116;gov/dwp/pension_credit320;gov/dwp/universal_credit16; all ofgov/dwp804 (benefit_cap.yaml18 andbenefit_cap_by_scheme.yaml22 included);universal_credit/housing_costs_element21;universal_credit/work_allowance17;housing_benefit_universal_credit_passport.yaml9;UC_maximum_amount.yaml1;integration/uc_child_responsibility_housing_contribution.yaml12. All passed. - Python,
-n 4:test_housing_benefit_accommodation_dates.py38;…_dates_properties.py9;test_housing_benefit_specified_accommodation_disregard_properties.py11;test_esa_income_abolition_properties.py31;test_esa_income_universal_credit_claim_properties.py2;test_uc_legacy_closure_properties.py4;test_legacy_closure_scope.py21;test_benefit_cap_uc_and_hb_properties.py1;test_benefit_cap_lha_scheme_split_properties.py7;test_housing_benefit_pension_age_properties.py4;code_health3,091. All passed. check_fix_pr.pyon the posted title and body: PASS (27 changed executable tests).ruff@0.17.2 format --check,check .and--select F811,F821: clean.git diff --check 5a5ac5fb1..HEAD: clean.- Merges: clean with
main(0080274d7) and with Bar income-related ESA at pensionable age and on Pension Credit #2807's current head71ba2752. On the Bar income-related ESA at pensionable age and on Pension Credit #2807 merge,esa_income,housing_benefit,universal_credit/housing_costs_elementanduniversal_credit/work_allowancepassed 757 of 757. Abolish working-age Housing Benefit from 1 July 2026 (NI 1 October 2026) #1910 (2d0e4941) now also conflicts inuc_housing_costs_element.py. It makes the same change there, and the PR body says how the merge resolves it.
C2: rulespec-uk#518
AXIOM_518.md adds 11 module rows and extends the reg 46 and 47 rows to paragraph (2). It adds source URLs and operative excerpts quoted only from the law file. It adds five required outputs, companion cases 12 to 20 worked from the law, the matching review_finding additions, and a new paragraph in "What PolicyEngine models". The pinned target and blocker section stays as it was.
Judgment calls and limits
- In the AXIOM record I didn't check the release's corpus locks for SI 2013/376, SI 2006/213, SR 2016/216 or SR 2016/226; I can't reach axiom-corpus from here. The record says so and asks the corpus update to check them. It also has no captured-original hashes for the new excerpts and says that too. It leaves the reg 2 definitions and the housing costs element provisions unquoted and names them as bindings the encoder still needs. It quotes no Northern Ireland benefit cap exception, because legislation.gov.uk has no reg 75F in SR 2006/405.
- With earnings, a family in this accommodation can now have lower net income than without the status. In the worked household with £10,000 of earnings, the claim brings no Universal Credit, so the Housing Benefit means test (65% taper) applies: £11,835.83 against £13,218.80 in 2026-27. That follows the law's routing, and the PR body states it.
- The change applies to every route to Universal Credit, not only the ESA abolition (the PR body's "What else changes" says so). The input defaults to false and no dataset sets it, so the aggregate impact should not move.
- I skipped nothing that needs survey data. No test here uses it.
- The live PR body had one passive phrase ("an appointment was likely to be needed"), which I rewrote. I also added "Local runs at
7fe7360fa" and labelled the older list "ata3f8b1104".
Generated by Claude Code
We answered both findings at 7fe7360fa. C1: a family in specified or temporary accommodation keeps Housing Benefit through a Universal Credit claim and the move off legacy benefits, and can claim afresh (SI 2014/1230 regs 6A(2), 8(3) and 46(2); SR 2016/226 regs 4A(2), 6(3) and 47(2)), and Universal Credit leaves that rent out. Your London tenant now gets Housing Benefit of £30,000 and Universal Credit of £5,098.80, for household income of £33,518.80; esa_income_abolition.yaml holds that case, its control and a capped combined award. C2: TheAxiomFoundation/rulespec-uk#518 now carries regs 46(2) and 47(2), the other routing provisions, their outputs and companion cases 12 to 20.
juaristi22
left a comment
There was a problem hiding this comment.
Re-reviewed 7fe7360fa9008437670055067761db88d8bfc94e. Review COMPLETE, with no open findings or material evidence gaps.
- C1 RESOLVED: the statutory specified/temporary-accommodation route now preserves new and continuing Housing Benefit claims alongside UC and after legacy closure, with tenure, capital and take-up checks retained. The original London case now pays £30,000 HB plus £5,098.80 UC, zero UC housing element and no cap reduction, giving £33,518.80 household net income. The ordinary-accommodation control, GB/NI new claims, continuation, joint cap and work-allowance cases pass.
- C2 RESOLVED: the actual rulespec-uk#518 record now includes the GB/NI continuation and new-claim rules, operative text, outputs, companion cases and execution blocker.
109 repository tests and two valid native diagnostics passed. The accommodation input is the statutory status for the assessment period. Current-head Python/aggregate CI is still running, so approval is deferred until the required checks pass; the completed code/source review does not need repeating for that CI-only step.
|
Ready for hub review Verdict: REQUEST_CHANGES at This review covers the delta Previous round (review 5740395595)
P11. Setting the input without a Housing Benefit report removes all help with the rent
I ran it with
With the input false, both households stay at £15,387 and £13,518.80 at the head. A user who supplies the status to get the law's routing gets a result £11,868.20 (or £10,000) lower than without it, and lower than the base. Under reg 8(3) and Sch 1 para 3(i), the rent moves to Housing Benefit; it doesn't disappear. My R4 population with the input true shows the same pattern: 35 to 40 families a year lose net income because they are eligible for Housing Benefit, have no Housing Benefit take-up, and lose the element ( Fix I tried in a scratch copy: in the hand-entered branch of accommodation = benunit("in_specified_or_temporary_accommodation", period)
return benunit("claims_all_entitled_benefits", period) | reported_hb | accommodationThe data branch doesn't change, and no dataset sets the input, so R4 and the aggregate report stay as they are. With the fix, both rows above give £33,518.80 and £13,518.80, the second matching the control. P22. rulespec-uk#518 companion case 13: the Northern Ireland deadline date conflicts with art. 3ACase 13 gives a Northern Ireland claimant with an unsaved income-related ESA award a deadline day of 22 June 2027, and says reg 47(1)(a) ends the award on 5 July 2027. Under SR 2025/176 art. 3A(1), as quoted in the issue, the amending provisions come into operation for an unsaved award on 1 October 2026, nine months before that deadline. The case states no saving, so the encoder would find no award left for reg 47(1)(a) to end. Its own arithmetic is right: 22 June plus 13 days is 5 July. Replacement for case 13:
The P3
Checked and sound
The formula has no Northern Ireland branch, so no mutant separates reg 47(2), and I added no take-up guard mutant, since none exists.
CI at the head
What I couldn't check
Generated by Claude Code |
|
Picking this up for the UK hub. Reply here if you're mid-change and I'll stop. Generated by Claude Code |
…using Benefit take-up A hand-entered family with in_specified_or_temporary_accommodation now claims Housing Benefit whatever else it reports, since Universal Credit leaves that rent out of its housing costs element. Data-built simulations keep their reporters-only rule. Adds YAML cases for take-up, the tenure and capital tests on the route, properties R5 and R6, a data-built test, and names an actor in the branch's comments that made a regulation act. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6KjKZVLNEzacp8jh34
juaristi22
left a comment
There was a problem hiding this comment.
Follow-up at unchanged 7fe7360fa9008437670055067761db88d8bfc94e, after assessing the review received just after my previous comment. Review COMPLETE; original C1 and C2 remain RESOLVED. One nonblocking correction remains before approval:
A1 — Explain the HB take-up default beside the accommodation input. For hand-entered situations, the model treats reported benefits as the complete claimed set. Reporting another benefit without HB therefore makes would_claim_housing_benefit false. The statutory accommodation input removes the UC housing element regardless of whether HB is claimed. Please explain that this combination leaves rent unsupported unless the user also sets housing_benefit_reported or would_claim_housing_benefit, and show the explicit take-up override. Automatically claiming HB would be a different take-up convention; the lower result compared with the former UC-rent route alone does not establish a statutory routing error.
A2 RESOLVED before publication: the actual rulespec-uk#518 case13 now uses the 22 September 2026 deadline / 5 October 2026 termination and art.3A(1)(b)'s pending-run-on exception. Case12's rent-payee premise is also explicit. The updated record was checked against the original operative sources.
The routing repair and GB/NI HB-saving coverage remain validated. No model tests were repeated for these contract/source clarifications. Approval remains on hold for A1 and current-head CI.
… comment Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6KjKZVLNEzacp8jh34
|
Generated by Claude Code |
|
Generated by Claude Code |
|
Ready for hub review Head: Findings and how I resolved themP1 item 1, take-up (C1). I made the code fix.
P3 item 8, tenure and capital (C2). Two new cases in
M5 and M6 now each fail one YAML case. P3 item 7, writing (C3). I applied the five replacements as written:
I then read every comment, YAML name and documentation string the branch adds against
The P3 items 3, 4 and 5 (body).
Items 2 and 6. I left these alone, as the brief asked. Tests at
|
| Target | Passed |
|---|---|
housing_benefit_eligible.yaml |
14 |
would_claim_housing_benefit.yaml |
5 |
esa_income_abolition.yaml |
40 |
finance/benefit/family/housing_benefit |
596 |
universal_credit/work_allowance |
17 |
universal_credit/housing_costs_element |
21 |
housing_benefit_universal_credit_passport.yaml |
9 |
benefit_cap_by_scheme.yaml |
22 |
Extra YAML runs:
finance/benefit/family/universal_credit: 569 passed.finance/benefit/family/esa_income: 106.gov/dwp: 804. All passed, none failed.- No
integrationdirectory exists at this head, so I couldn't rerun the body's older 2,588-case figure.
Python (-n 4):
test_housing_benefit_accommodation_dates_properties.py,test_benefit_cap_uc_and_hb_properties.py,test_housing_benefit_universal_credit_passport_properties.pyandtest_housing_benefit_accommodation_take_up_data_built.pytogether: 23 passed (11 + 1 + 6 + 5) in 486 s.policyengine_uk/tests/code_health: 3,091 passed.
Other checks:
.github/check_fix_pr.pyon the final title and body, with the changes from the merge base withmainto the head, run aspr_fix_evidence.yamlruns it: PASS, 29 changed executable tests.uvx ruff@0.17.2 format --check .: 2,170 files already formatted.uvx ruff@0.17.2 check .andcheck --select F811,F821 policyengine_uk: all checks passed.git diff --check 7fe7360fa..HEAD: clean.- The Bar income-related ESA at pensionable age and on Pension Credit #2807 merge (
3c9890a19+254669a0d):esa_income131,housing_benefit596,gov/dwp/pension_credit320, for 1,047 of 1,047.
Mutations
Each mutant ran at 3c9890a19 in a scratch worktree, with PYTHONPATH pointing there. Each ran against the three YAML files, R1–R3, R5, R6 and the data-built file. All eight were killed.
| Mutant | YAML failures | Python failures |
|---|---|---|
Status dropped from would_claim_housing_benefit |
3 (one in each file) | R5, R6, the hand-entered test |
Status moved above the built_from_data branch |
0 | both data-built no-report tests |
| M5: the route skips capital | 1 (£20,000 capital) | R2 |
| M6: the route skips tenure | 1 (owner occupier) | R2 |
| The element ignores the input | 9 | R1, both data-built no-report tests |
| reg 8(3) route dropped | 4 | R2, both data-built report tests |
| reg 46(2) route dropped | 2 | R2 |
| New-claim route dropped | 4 | R2, both data-built no-report tests, the hand-entered test |
The second mutant fails only the data-built test, because no YAML case is data-built. The regression tests also fail without the change: the first mutant restores the pre-change take-up rule.
Differential (R4)
diff_run.py is my own population: 600 families a year in 2025, 2026 and 2027, built as the body describes, with 25 outputs. It ran on worktrees at 5a5ac5fb1, 7fe7360fa and 3c9890a19.
The input unset or false (half unset, half false): 58,740 values, 0 differ between 3c9890a19 and 7fe7360fa, and 0 between 3c9890a19 and 5a5ac5fb1.
The input true for every family, against 5a5ac5fb1:
| 2025 | 2026 | 2027 | |
|---|---|---|---|
7fe7360fa: net income falls |
72 | 84 | 71 |
| …of which no HB take-up | 70 | 78 | 67 |
3c9890a19: net income falls |
7 | 17 | 11 |
3c9890a19: net income rises |
173 | 182 | 184 |
Why each family at 3c9890a19 falls:
- HB's taper without a UC passport: 3, 8 and 2.
- HB paid weekly beside UC, slightly under the element: 3, 8 and 6. For example £161.10 × 52 = £8,377.20 against an £8,400 element, a loss of £22.80.
- The HB cap for a family without UC: 1, 0 and 2.
- The means test takes HB to nil: 0, 1 and 1.
None of these families lacks take-up. My population has more non-take-up losers than the review's 35–40, because it draws more benefit reports, but every one of them stops falling.
Also with the input true: council_tax_reduction rises for 1, 3 and 4 families, and housing_benefit_on_passporting_benefit turns false for 4, 10 and 7.
Judgment calls
- I put both of the review's rows in YAML: row 1 in
esa_income_abolition.yaml, row 2 and its control inhousing_benefit_eligible.yaml. - I numbered the new properties R5 and R6, because the body's R4 is the scratch differential. In the body, R4 now sits before them.
- I didn't cite HB Regs 2006 reg 43 in the capital case, because the law file doesn't quote it. The comment names the parameter the formula reads.
- In the body, the What-and-why bullet also states the take-up change, and the changelog fragment gains one clause.
- I left the body's older "Local runs" sections, and the "Add overseas and other real estate to property wealth and capital #2688, Fix structural reforms for their requested periods #2670…" Overlap line, as they stood. I didn't rerun those other PRs.
Skipped
- No survey-data test, and no aggregate report. The Python shards are left to CI.
- I didn't edit rulespec-uk#518.
Generated by Claude Code
|
Picking this up for the UK hub. Reply here if you're mid-change and I'll stop. Generated by Claude Code |
…emporary accommodation would_claim_housing_benefit no longer reads in_specified_or_temporary_accommodation: a hand-entered family claims when claims_all_entitled_benefits is true or it reports Housing Benefit, and a data-built one when it reports Housing Benefit. The documentation of both variables now says that a hand-entered family with the status that reports another means-tested benefit and no Housing Benefit gets no help with the rent, and that setting housing_benefit_reported or would_claim_housing_benefit models the claim. The cases and properties R5 to R7 pin both results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6KQ9PJBdyEd9CqHdqK
|
Generated by Claude Code |
|
Generated by Claude Code |
|
Ready for hub review Head: Findings and how I resolved themTake-up (juaristi22's A1; this replaces the earlier review's P1 item 1 fix).
Cases. I worked the arithmetic from the parameter files, and it's in each case's comment.
Properties (
Data-built test (
Body. I changed these parts:
The title is unchanged. Tests at
|
| Target | Passed |
|---|---|
housing_benefit_eligible.yaml |
15 |
would_claim_housing_benefit.yaml |
6 |
esa_income_abolition.yaml |
41 |
finance/benefit/family/housing_benefit |
598 |
finance/benefit/family/esa_income |
107 |
finance/benefit/family/universal_credit |
569 |
gov/dwp |
804 |
None failed.
Python, -n 4 each:
| File | Passed |
|---|---|
test_housing_benefit_accommodation_dates_properties.py |
12 |
test_housing_benefit_accommodation_take_up_data_built.py |
8 |
test_benefit_cap_uc_and_hb_properties.py |
1 |
test_housing_benefit_universal_credit_passport_properties.py |
6 |
policyengine_uk/tests/code_health |
3,091 |
None failed. No renamed test sits in shard_durations.toml.
Other checks:
.github/check_fix_pr.pyon the final title and body, with changes from the merge base withmain, run aspr_fix_evidence.yamlruns it: PASS, 29 changed executable tests.uvx ruff@0.17.2 format --check .: 2,170 files already formatted.ruff check .and--select F811,F821 policyengine_uk: pass.git diff --check 3c9890a19..HEAD: clean.
Merges (git merge-tree --write-tree --name-only):
- with
origin/main(52b30809f): clean. - with Bar income-related ESA at pensionable age and on Pension Credit #2807's head
254669a0d(read frompulls/2807): clean. - with Abolish working-age Housing Benefit from 1 July 2026 (NI 1 October 2026) #1910's head
2d0e49417: the same 7 conflicts as before. I didn't rerun tests on any merge.
Mutations
Each mutant ran in its own scratch worktree at dc69f3ada, with PYTHONPATH set to that worktree. Each ran against the three YAML files, R1 to R3, R5 to R7 and the data-built file. All 7 were killed.
| Mutant | YAML failures | Python failures |
|---|---|---|
Accommodation term put back into would_claim_housing_benefit |
3 (one per file) | R5, R7, hand-entered no-claim |
| M5: route skips capital | 1 (£20,000 capital) | R2 |
| M6: route skips tenure | 1 (owner occupier) | R2 |
| Element ignores the input | 11 | R1, R7, both data-built no-report, hand-entered no-claim |
| reg 8(3) route dropped | 4 | R2, both data-built with a report |
| reg 46(2) route dropped | 2 | R2 |
| New-claim route dropped | 6 | R2, both data-built no-report, both data-built override, both hand-entered |
handoff-690.md's "status moved above the data-built branch" mutant no longer applies, because the formula has no status term, so I didn't run it. R6 doesn't kill the take-up mutant, as expected: it sets take-up as an input.
Differential
I didn't have handoff-690.md's diff_run.py. I built a population to the body's R4 description and ran it on worktrees at 5a5ac5fb1, 7fe7360fa, 3c9890a19 and dc69f3ada:
- 600 families a year in 2025, 2026 and 2027;
- one or two adults aged 20 to 75, 0 to 4 children;
- six regions including Northern Ireland, five tenures;
- rent of £0 to £30,000, savings of £0 to £17,000, earnings, LCWRA and appointee savings;
- reports of the six legacy benefits plus Universal Credit and Pension Credit;
- random claim flags, with
claims_all_entitled_benefitsset for half the families and left to the formula for the rest; - the same 25 outputs.
The body calls it the third population.
- Input unset, false or true against
7fe7360fa: 58,419 values in each mode across the three years, 0 differ. - Input true plus
would_claim_housing_benefittrue at head, against3c9890a19with the input true: 58,419 values, 0 differ. - Net income with the input true, against
5a5ac5fb1:
| Run at head | 2025 | 2026 | 2027 |
|---|---|---|---|
| Convention: falls | 69 | 57 | 58 |
| Convention: rises | 32 | 43 | 42 |
| Override: falls | 9 | 10 | 9 |
| Override: rises | 147 | 129 | 135 |
The convention run matches 7fe7360fa exactly, and the override run matches 3c9890a19 exactly.
- Why the convention falls happen: 65, 55 and 52 of them don't take Housing Benefit up and get neither Housing Benefit nor an element.
- Why the override falls happen (my own classification):
- Housing Benefit's taper without a Universal Credit passport: 3, 5 and 1;
- Housing Benefit beside Universal Credit a little under the element, for example £12,864.80 against £12,900: 3, 5 and 6;
- the Housing Benefit cap without Universal Credit: 0, 0 and 1;
- the means test taking Housing Benefit to nil: 3, 0 and 1.
- In both runs,
council_tax_reductionrises for 1, 2 and 0 families, andhousing_benefit_on_passporting_benefitturns false for 7, 5 and 4.
Judgment calls
- The differential population is my rebuild, not
handoff-690.md's. Its totals (58,419) differ from the 58,740 in the body's second population, so I kept both in R4 and labelled each by commit. - I replaced the body's "Local runs at
3c9890a19" section with the head section. I kept a short3c9890a19paragraph for the runs I didn't repeat:work_allowance,housing_costs_element, the passport YAML,benefit_cap_by_scheme.yaml, and the Bar income-related ESA at pensionable age and on Pension Credit #2807 merge's 1,047. housing_benefit_eligible.yaml's "The same family that doesn't take Housing Benefit up gets no help with the rent" still setswould_claim_housing_benefit: false, as3c9890a19left it. Its outputs are the same either way.- R7 forces an income-related ESA report where the draw gave neither ESA nor Working Tax Credit, so every family reports one of the seven benefits.
Skipped
- Survey-data tests and the aggregate report: I had no dataset access.
- Tests on the Bar income-related ESA at pensionable age and on Pension Credit #2807 and Abolish working-age Housing Benefit from 1 July 2026 (NI 1 October 2026) #1910 merges: I checked those merges with merge-tree only.
- I didn't edit rulespec-uk#518.
Generated by Claude Code
juaristi22
left a comment
There was a problem hiding this comment.
Re-reviewed the newly adopted head dc69f3ada6b6bee045ce4db88406311ba5701fcd. Review COMPLETE: C1, C2, A1 and A2 are resolved, with no open findings or material evidence gaps.
The actual description and accommodation input now explain the report-driven HB take-up convention, the no-HB-report default and how to model an explicit HB claim. The protected reported-HB award still reaches final HB, UC and household net income correctly. The current rulespec-uk#518 handoff remains consistent.
All 69 focused current-head cases passed. Unchanged validated behavior from the preceding review was retained without repeating its full suite. Exact-head hosted CI is still running, so approval is deferred until the required checks pass.
juaristi22
left a comment
There was a problem hiding this comment.
Approved at exact head dc69f3ada6b6bee045ce4db88406311ba5701fcd. The required checks now pass at this commit. The completed independent code/source review is retained, all prior findings remain resolved, and no material evidence gaps remain. No local test rerun was needed for this CI-only follow-up.
Fixes #2562
What and why
The Secretary of State appointed 1 July 2026 for the remaining old style income-related ESA awards in Great Britain to end under Welfare Reform Act 2012 s.33(1)(b) (SI 2025/1148 art. 3A, inserted by SI 2026/409). The Department for Communities appointed 1 October 2026 in Northern Ireland (SR 2025/176 art. 3A, inserted by SR 2026/115). Under each article, an appointee saving keeps an award in payment, and an award already due to end after a two-week run-on ends on that schedule. DWP had sent migration notices to 879,000 households on income-related ESA by December 2025, and most of them had claimed Universal Credit (LA Welfare Direct 6/2026, paras 51 to 54). At
main(2da212a) the model pays an award in full every year unless the family claims Universal Credit or DWP closes another of its legacy benefits, and nothing in the model prompts a claim on the award's appointed day.This PR:
gov.dwp.ESA.income.awards_payable.{great_britain,northern_ireland}, a 1/0 flag for each jurisdiction withfiscal_year_blend: true, so the model day-weights the date across each fiscal year from 6 April to 5 April;esa_income_appointee_saving(person input, default false) for the art. 3A(2) saving. The saving applies when someone holds an appointment under reg 57 of the Claims and Payments Regulations 2013 immediately before the date, or the Secretary of State determined in the preceding six months that someone would likely need to hold one. In Northern Ireland, the Department for Communities makes that determination and reg 52 of the 2016 Regulations governs the appointment. The claimant and partner share one award, so when either of them reports the award and has the saving, the model keeps the whole claim payable; it ignores a saving for a member who reports nothing;esa_income_payable_share(person), the share of the year the person's claim stays payable: 1 for a saved claim, otherwise the household's jurisdiction value.esa_incomepays each claim for its share, one share for the couple's claim and one for each member outside it;esa_income_abolition_reached(benefit unit): the claimant or partner reports an unsaved award with less than the whole year payable, from 2026-27.claims_uc_at_legacy_closurenow reads it besidelegacy_benefits_closed, so the family claims Universal Credit when the abolition reaches its award ifwould_claim_uc_at_legacy_closure. The claim ends the income-related ESA award (esa_income_report_closed) and, outside specified or temporary accommodation, the Housing Benefit award (housing_benefit_eligible), through the rulesmainalready has;legacy_award_is_closedclose an income-related ESA report once none of its claim stays payable, from 2027-28, under the same Pension Credit and pension-age conditions as Income Support and income-based JSA. DWP's move then ends the family's other legacy awards whether or not it claims, apart from the Housing Benefit exception below. The model neither prompts a claim for a saved award nor moves its family;in_specified_or_temporary_accommodationfor the family.uc_housing_costs_elementreturns nil: the Secretary of State excludes these payments from rent payments (UC Regs 2013 Sch 1 paras 3(h), 3(i), 3A and 3B; Northern Ireland: SR 2016/216 Sch 1 paras 3(e), 3(ea), 4 and 4A).housing_benefit_eligiblekeeps an existing award through a Universal Credit claim (SI 2014/1230 reg 8(3); Northern Ireland: SR 2016/226 reg 6(3)) and through the move off legacy benefits, whichever legacy benefit closed (SI 2014/1230 reg 46(2); Northern Ireland: SR 2016/226 reg 47(2)), and lets a family with no award make a new claim (SI 2014/1230 reg 6A(2); Northern Ireland: SR 2016/226 reg 4A(2)). The tenure and capital tests still apply. The input doesn't change take-up (would_claim_housing_benefit): in a data-built simulation, families that report Housing Benefit claim it, and in a hand-entered one, a family also claims whenclaims_all_entitled_benefitsis true. With no Universal Credit towards this rent, a family gets help with it only through Housing Benefit. A hand-entered family that reports another of the seven means-tested benefits and no Housing Benefit claims only what it reports and gets no help with the rent; to model the claim, a user setshousing_benefit_reportedorwould_claim_housing_benefit. The 1 July 2026 abolition of Housing Benefit doesn't reach these awards (SI 2025/1148 art. 7(4)(a), through reg 6A(2)). The route reads the input, not an award, so it adds no cycle through the benefit cap. Without the element,uc_work_allowancegives the higher work allowance (UC Regs 2013 reg 22(2)(a)). The benefit cap needs no formula change: DWP counts Housing Benefit as nil in the Universal Credit cap (uc_benefit_cap_reduction; reg 80(2A)), and the council doesn't apply the Housing Benefit cap to a claimant who receives Universal Credit (is_housing_benefit_benefit_cap_exempt_specified_benefit; HB Regs 2006 reg 75F(1)(g)).uc_non_dep_receives_pension_creditalready fixes the element at nil in its branch, so the change leaves it alone;left_pension_route_at_legacy_closurecount a protected mixed-age couple whose award the abolition reaches, so a couple that claims and has a Universal Credit award stops getting pension-age Housing Benefit beside it;own_report_is_paidapply the abolition's report closure to a member outside the couple only when both held amounts match the formula on open reports:esa_incomeand the claimant's and partner's part of it,claimant_or_partner_esa_income(holds_formula_award). Either amount can differ when a user entersesa_income, a reform replaces it, ordisable_simulated_benefitsholds it;own_report_is_paidthen applies only the person's legacy closure and reads the benefit unit's held award. That award covers the whole unit, so an outside member with an open report stays paid while the unit's award stays positive, even if it leaves them nothing. A member outside the couple at pension age has no legacy closure, so a reform that replacesesa_incomekeeps that member on income-related ESA.holds_formula_awardcan't separate two or more outside reporters in one benefit unit when the held total and the couple's part both equal the formula's:own_report_is_paidthen applies each one's report closure;disable_simulated_benefitsholdesa_income_abolition_reachedfalse with the other closures, so the reform prompts no claim.Contributory ESA continues as new style ESA, and
esa_contribkeeps its formula.Which years change
In 2026-27 a family whose award the abolition reaches claims Universal Credit if
would_claim_uc_at_legacy_closure, and the claim ends the award for the year. A family that doesn't claim keeps £5,000 x 86/365 = £1,178.08 of a £5,000 award in Great Britain (£2,438.36 in Northern Ireland) and its Housing Benefit. From 2027-28 every unsaved award has ended, and the model moves each affected family off its legacy benefits. Scotland and Wales follow the Great Britain date.Under art. 3A(1)(b), an award already due to end at the end of a two-week run-on ends on that schedule. A claimant whose migration notice deadline falls on 22 June 2026 and who makes no claim keeps the award until 5 July, the last day of the two weeks beginning with the deadline day (SI 2014/1230 reg 46(1)(a)). The model has no deadline or claim dates, so it pays every unsaved award to 30 June (30 September in Northern Ireland).
Worked household
A single claimant aged 40 in a London council tenancy (rent £5,200 a year, council tax £1,500) reports £5,000 a year of income-related ESA and £4,000 of Housing Benefit, and
would_claim_ucis false. The Housing Benefit means test pays £5,200 from the rent. Onmainevery row below pays £5,000 of ESA and £5,200 of Housing Benefit, and no Universal Credit or Council Tax Reduction.The ESA abolition doesn't end Housing Benefit by itself. The Secretary of State appointed 1 July 2026 for the remaining working-age Housing Benefit awards to end without a Universal Credit claim (SI 2025/1148 art. 7), and the Department for Communities appointed 1 October 2026 in Northern Ireland (SR 2025/176 art. 7), except in specified or temporary accommodation. So under the law this tenant has neither benefit in 2027-28 without a claim, as the model gives. The model doesn't apply art. 7's date: in 2026-27 it pays a family that doesn't claim a full year of Housing Benefit, an approximation that #1910 replaces.
Each cell gives the amount here and, in brackets, the change from
main, in pounds. Council Tax Reduction stays £0 in every row.would_claim_uc_at_legacy_closureWith the flag at its default, true, the model gives the true rows' amounts, and every output matches
mainin 2025-26. Within_specified_or_temporary_accommodation, the tenant who doesn't claim keeps their £5,200 of Housing Benefit in 2027-28, for £3,520.00 of net income. The tenant without earnings who claims keeps the same £5,200 of Housing Benefit beside Universal Credit, which leaves the rent out: £5,098.80 in 2026-27 and £5,318.52 in 2027-28, the standard allowance alone. Net income comes to the same £8,718.80 and £8,838.52 as in the table, now split between the two benefits. With £10,000 of earnings, the claim brings no Universal Credit, so Universal Credit no longer passports the Housing Benefit means test, and Housing Benefit's 65% taper leaves £3,415.83 in 2026-27 (net income £11,835.83) and £4,838.89 in 2027-28 (£13,158.89), against £13,218.80 and £13,338.52 in the table. With any Universal Credit entitlement, the council disregards all income in the Housing Benefit means test (housing_benefit_on_passporting_benefitand the provisions it cites), so at £9,200 of earnings, with £38.80 of Universal Credit, the tenant receives the whole £5,200 of Housing Benefit and net income comes to £12,858.80.Data
The certified data (policyengine-uk-data 1.58.1) supply no draw for
would_claim_uc_at_legacy_closure, so the model uses its default, true. The model therefore has each affected family claim Universal Credit in 2026-27. In an affected family the claimant or partner reports an unsaved income-related ESA award, and the family neither reports Pension Credit nor consists of a single claimant, or joint claimants both, over the qualifying age for State Pension Credit. A family whose only report belongs to a member outside the couple makes no claim through this path. PolicyEngine/policyengine-uk-data#534, a draft pull request, would draw the flag at DWP's Move to Universal Credit claim rates. No dataset identifies appointees or specified or temporary accommodation, so the saving, the Housing Benefit routes and the Universal Credit exclusion stay off in dataset runs.What else changes
mainan income-related ESA report counted as an award the claim would end, so the family kept its claim; now it counts only while some of the year stays payable (forgoes_universal_credit_for_tax_free_childcare). With a £21,993 maximum award and a £20,000 income reduction, Universal Credit would pay £1,993 against £2,400 of Tax-Free Childcare, so the family now forgoes it.in_specified_or_temporary_accommodationnow keeps its Housing Benefit when any legacy closure moves it, the tax credit closure from 2025-26 and the Income Support and income-based JSA closures from 2026-27 included. In every year it also keeps the award beside a Universal Credit claim, can claim Housing Benefit afresh, and gets no Universal Credit housing costs element. That changes the model for such a family whatever moved it to Universal Credit, not only for the ESA abolition. Take-up stays as onmain(would_claim_housing_benefit), so a hand-entered family that reports another of the seven means-tested benefits and no Housing Benefit claims only what it reports. It loses the Universal Credit housing costs element it gets onmainand gets no help with the rent; to model the claim, a user setshousing_benefit_reportedorwould_claim_housing_benefit. Universal Credit falls by the element, so the model's Council Tax Reduction, which counts Universal Credit as income, can rise, and a family whose Universal Credit falls to nil loses the Housing Benefit passport.has_own_esa_award, the support component default, the Universal Credit LCWRA default, the mixed-age Pension Credit saving andclaims_all_entitled_benefitsread the raw report as before. Their documentation now treats the report as the award's history: the work capability assessment behind an ended award still stands, and the report still records the family's claiming.What the model doesn't do
in_specified_or_temporary_accommodationdoesn't separate specified from temporary accommodation. Where the Housing Benefit cap applies, to a family without Universal Credit, the council counts Housing Benefit for specified accommodation as nil (HB Regs 2006 reg 75C(2)(a)), butbenefit_cap_welfare_benefitscounts it at its amount before the cap.in_specified_or_temporary_accommodationdoesn't make a family claim Housing Benefit. With no Universal Credit towards this rent, a family gets help with it only through Housing Benefit. A data-built family with the input and no Housing Benefit report, and a hand-entered family that reports another of the seven means-tested benefits and no Housing Benefit, claim only what they report and get no help with the rent. To model the claim, sethousing_benefit_reportedorwould_claim_housing_benefit.Invariants
The property tests (
policyengine_uk/tests/test_esa_income_abolition_properties.py) check these for generated reported-award situations: working-age families of one to three adults, with or without a child, in five regions, with reports of £0 to £6,000, capital around the tariff threshold and limit, and every combination of saving and claim flags, in 2025-26 to 2027-28.awards_payablevalue equals the share of 6 April to 5 April before the statutory date, counted from the two dates (2015 to 2040).esa_income_payable_share<= 1; the claimant and partner share one value; it equals 1 for every member of a saved claim.esa_incomeoresa.would_claim_uc_at_legacy_closureon never raisesesa_income, and a family that claims when the abolition reaches its award has noclaimant_or_partner_esa_incomethat year.esa_income_abolition_reachedandlegacy_benefits_closedstay false for the family, and a member outside the couple with a saved claim keeps their legacy benefits open.esa_income,claims_universal_creditand each outside member's status match amounts worked in the test from the regulations (the £6,000 tariff threshold, £1 a week for each £250 or part above it and the £16,000 limit; ESA Regs 2008 regs 110 and 118), the dates and the saving, without the model's code. The test's reference gives a couple's claim one share and pays a saved claim in full.esa_income,holds_formula_awardis true for every benefit unit in every mode and year, so the comparison of the claimant's and partner's part changes no baseline result.Specified or temporary accommodation
policyengine_uk/tests/test_housing_benefit_accommodation_dates_properties.pychecks R1 to R3 and R5 to R7 for generated families with the input set: one or two adults aged 22 to 70, up to five children, council, housing association and private tenancies and owners, rent of £0 to £30,000, capital around the £16,000 limit, earnings, reported Housing Benefit, income-related ESA and Working Tax Credit, and every combination of claim and take-up flags, in five regions including Northern Ireland, in 2025-26 to 2027-28.uc_housing_costs_elementis nil for every family.housing_benefit_eligibleequals the tenure and capital tests alone, and stays the same when the test setsclaims_universal_creditandlegacy_benefits_closedto each of their four combinations.uc_benefit_cap_reductionequals that of the same family with Housing Benefit set to nil,housing_benefit_benefit_cap_reductionis nil, andhousing_benefitequalshousing_benefit_pre_benefit_cap.7fe7360fawith5a5ac5fb1(housing_benefit_eligible,housing_benefit_pre_benefit_cap,housing_benefit,uc_housing_costs_element,uc_work_allowance,universal_credit_pre_benefit_cap,universal_credit,uc_benefit_cap_reduction,housing_benefit_benefit_cap_reduction,is_housing_benefit_benefit_cap_exempt_specified_benefit,benefit_cap_welfare_benefits,claims_universal_credit,esa_income,household_net_income) match for every family and year. On a second population, with reports of six of the seven legacy benefits, it compares 25 outputs at3c9890a19with both7fe7360faand5a5ac5fb1: the 14 above,would_claim_housing_benefit,pension_credit,working_tax_credit,child_tax_credit,income_support,tax_free_childcare,housing_benefit_on_passporting_benefit,council_tax_reduction,free_school_meals,is_scp_eligibleandhousing_benefit_entitlement. None of the 58,740 values differs from either base. Atdc69f3ada, the differential compares the same 25 outputs on a third population built to the same description (600 families a year, reports of six legacy benefits, Universal Credit and Pension Credit, andclaims_all_entitled_benefitsleft to the formula for half the families). With the input unset, false or true for every family, none of the 58,419 values in each mode differs from7fe7360fa. With the input andwould_claim_housing_benefittrue for every family, none of the 58,419 values differs from3c9890a19with the input true. No dataset sets the input, so CI's aggregate report should show no move from this change.7fe7360fafalls against5a5ac5fb1for 72, 84 and 71 families in the three years, 70, 78 and 67 of them with no Housing Benefit take-up. At3c9890a19it falls for 7, 17 and 11 families and rises for 173, 182 and 184. Each family whose net income falls receives Housing Benefit in place of a larger element, or gets none from the means test: Housing Benefit's taper without a Universal Credit passport (3, 8 and 2), Housing Benefit paid weekly over 52 weeks beside Universal Credit, such as £161.10 × 52 = £8,377.20 against an £8,400 element (3, 8 and 6), the Housing Benefit cap for a family without Universal Credit (1, 0 and 2), and the means test taking Housing Benefit to nil (0, 1 and 1). With the input true,council_tax_reductionrises for 1, 3 and 4 families, andhousing_benefit_on_passporting_benefitturns false for 4, 10 and 7.dc69f3adafalls against5a5ac5fb1for 69, 57 and 58 families in the three years and rises for 32, 43 and 42, the same as at7fe7360fa. Of the families whose net income falls, 65, 55 and 52 get neither Housing Benefit nor an element because they don't take Housing Benefit up. Withwould_claim_housing_benefitalso true, it falls for 9, 10 and 9 families and rises for 147, 129 and 135, the same as at3c9890a19: Housing Benefit's taper without a Universal Credit passport (3, 5 and 1), Housing Benefit beside Universal Credit a little under the element, such as £12,864.80 against a £12,900 element (3, 5 and 6), the Housing Benefit cap for a family without Universal Credit (0, 0 and 1), and the means test taking Housing Benefit to nil (3, 0 and 1). In both runscouncil_tax_reductionrises for 1, 2 and 0 families, andhousing_benefit_on_passporting_benefitturns false for 7, 5 and 4.would_claim_housing_benefit: with the input true, false or unset, it equalsclaims_all_entitled_benefitsor a Housing Benefit report.would_claim_housing_benefittrue, a family that passes the tenure and capital tests (housing_benefit_eligible) always ends up with Housing Benefit or a housing costs element, except where the Housing Benefit means test takes its entitlement to nil (housing_benefit_entitlement). The council caps Housing Benefit no lower than 50 pence a week (housing_benefit_benefit_cap_reduction), so the cap can't take that Housing Benefit to nil.claims_all_entitled_benefitsandwould_claim_housing_benefitleft to their formulas, gets neither Housing Benefit nor a housing costs element.test_keeping_awards_payable_is_saving_every_awardchecks that a reform keepingawards_payableat 1 gives the same outputs as saving every award.Invariant 12 also follows from the code.
esa_incomeappliesincome_related_esa_award_on_reportsto open reports, so the first comparison matches.claimant_or_partner_awardthen finds thatesa_incomeholds the formula's value and takes the same function on the claimant's and partner's open reports, so the second comparison matches unless it sets that part to zero. It does so only where the part, or the stored total that bounds it, falls within half a penny of zero, and then the formula's part also falls within half a penny of zero.is_on_income_related_esaalready readsclaimant_or_partner_esa_incomefor the claimant and partner, so the new read adds no cycle.Test plan
Local runs at
dc69f3adadc69f3ada6b6bee045ce4db88406311ba5701fcdadds one commit to3c9890a19. We ran these with no survey data, in the locked environment (uv sync --frozen --extra dev).YAML policy tests (
policyengine-core test):housing_benefit/housing_benefit_eligible.yaml: 15 passed. "A family in temporary accommodation that reports only Universal Credit claims no Housing Benefit and gets no help with the rent": no Housing Benefit, element nil, Universal Credit £5,098.80, net income £5,098.80 + £100 − £180 − £1,500 = £3,518.80. "The same family gets Housing Benefit for the rent when the user sets would_claim_housing_benefit": Housing Benefit £10,000, Universal Credit £5,098.80, net income £13,518.80, the same as its control outside that accommodation. The capital, owner-occupier and control cases from3c9890a19stay as they were.housing_benefit/would_claim_housing_benefit.yaml: 6 passed. "The accommodation status doesn't make a family claim without a report" (claims_all_entitled_benefitsfalse, no report) gives false; "A family with the accommodation status claims when it claims all entitled benefits" gives true; "Ineligible with the accommodation status set false" stays false.esa_income/esa_income_abolition.yaml: 41 passed. "Great Britain 2026-27, the same London tenant without a Housing Benefit report claims only the benefits it reports and gets no help with the rent": no Housing Benefit, element nil, Universal Credit £5,098.80, net income £5,098.80 + £100 − £180 − £1,500 = £3,518.80. "Great Britain 2026-27, the same London tenant gets Housing Benefit for the rent when the user sets would_claim_housing_benefit": Housing Benefit £30,000, Universal Credit £5,098.80, net income £33,518.80.finance/benefit/family/housing_benefit: 598 passed;finance/benefit/family/esa_income: 107;finance/benefit/family/universal_credit: 569;gov/dwp: 804.Python tests (
pytest -n 4):test_housing_benefit_accommodation_dates_properties.py12 passed (R5 to R7);test_housing_benefit_accommodation_take_up_data_built.py8;test_benefit_cap_uc_and_hb_properties.py1;test_housing_benefit_universal_credit_passport_properties.py6;policyengine_uk/tests/code_health3,091 passed. The data-built file checks that a data-built family with the status and no Housing Benefit report doesn't claim and gets no help with the rent, that one with a report gets the whole rent, that one withwould_claim_housing_benefitin the dataset gets the whole rent, that the same family hand-entered withclaims_all_entitled_benefitsfalse doesn't claim and gets no help with the rent, and that it gets the whole rent withwould_claim_housing_benefitset.Other checks at
dc69f3ada:.github/check_fix_pr.pyon this title and body, with the changes from the merge base withmain, passes with 29 changed executable tests;uvx ruff@0.17.2 format --check .finds 2,170 files formatted;ruff check .andruff check --select F811,F821 policyengine_ukpass; andgit diff --check 3c9890a19..HEADfinds no whitespace errors.We ran each mutant below as one change to a copy of the branch at
dc69f3ada, againsthousing_benefit_eligible.yaml,would_claim_housing_benefit.yaml,esa_income_abolition.yaml, R1 to R3, R5 to R7 and the data-built test. Each one failed at least one of them:dc69f3ada)would_claim_housing_benefit's hand-entered branchThe differential above (R4) compares
dc69f3adawith7fe7360faand3c9890a19.Local runs at
7fe7360faWe ran these with no survey data, in the locked environment (
uv sync --frozen --extra dev).YAML policy tests (
policyengine-core test):esa_income/esa_income_abolition.yaml: 39 passed. The branch adds three cases: a London tenant in council-arranged homelessness accommodation, with £30,000 of rent, whom the abolition moves to Universal Credit (Housing Benefit £30,000, Universal Credit £5,098.80, net income £33,518.80); the same tenant without the status (Housing Benefit nil, Universal Credit capped at £16,967, net income £15,387); and a lone parent of four whose Universal Credit the cap cuts by £2,442.20 to £17,821 while Housing Benefit pays the whole £15,000 rent. The case "a family in specified or temporary accommodation that claims Universal Credit" now keeps its £5,200 of Housing Benefit beside £5,098.80 of Universal Credit, where it had none.housing_benefit/housing_benefit_eligible.yaml: 10 passed. The branch adds four cases: a new claim beside Universal Credit (reg 6A(2)); the same family without take-up gets no help with the rent; Northern Ireland's reg 6(3) with SR 2016/216's exclusion; and its control without the status.universal_credit/work_allowance: 17 passed, two new: the higher work allowance (£8,520) in that accommodation, and the lower one (£5,124) without it.universal_credit/housing_costs_element: 21 passed, one new.housing_benefit/housing_benefit_residential_capital.yaml: 7 passed.housing_benefit/applicable_income/housing_benefit_specified_or_temporary_accommodation_disregard.yaml: 50 passed.housing_benefit/housing_benefit_universal_credit_passport.yaml: 9 passed.gov/dwp/benefit_cap: 116 passed;gov/dwp/pension_credit: 320 passed;gov/dwp/universal_credit: 16 passed; all ofgov/dwp: 804 passed,benefit_cap.yaml(18) andbenefit_cap_by_scheme.yaml(22) included.gov/dwp,finance/benefit/familyandintegrationgave 2,588 passed and 1 failed, the case above that now keeps its Housing Benefit.Python tests (
pytest -n 4):test_housing_benefit_accommodation_dates.py38 passed;test_housing_benefit_accommodation_dates_properties.py9 passed (R1 to R3 new);test_housing_benefit_specified_accommodation_disregard_properties.py11;test_esa_income_abolition_properties.py31;test_esa_income_universal_credit_claim_properties.py2;test_uc_legacy_closure_properties.py4;test_legacy_closure_scope.py21;test_benefit_cap_uc_and_hb_properties.py1;test_benefit_cap_lha_scheme_split_properties.py7;test_housing_benefit_pension_age_properties.py4;policyengine_uk/tests/code_health3,091 passed.At
5a5ac5fb1, the 8 new routing cases fail and the 3 controls pass, and R1 and R2 fail; R3 passes there, as the cap needed no formula change. We ran each mutant below as one change to a copy of the branch, against the four YAML files above and R1 to R3:7fe7360famerged cleanly withmain(0080274d7) and with #2807 at71ba2752; on the #2807 merge,esa_income,housing_benefit,universal_credit/housing_costs_elementanduniversal_credit/work_allowancepassed 757 of 757.Local runs at
a3f8b1104Each file or test ran alone at
a3f8b1104, with-n 0, no survey data and a 4 GB memory cap (2 GB for mutants); the cap stopped any run that exceeded its time limit. Later commits up to5a5ac5fb1change only documentation, comments and the changelog.YAML policy tests (
policyengine-core test):esa_income/esa_income_abolition.yaml: 36 passed. The branch added three cases: the 2027-28 ESA closure keeps Housing Benefit for a family within_specified_or_temporary_accommodation, the same family without the status loses it, and a family with the status that claims Universal Credit, which ata3f8b1104got no Housing Benefit and at7fe7360fakeeps it.housing_benefit/housing_benefit_eligible.yaml: 6 passed. The branch adds two cases: the tax credit closure keeps Housing Benefit with the status and ends it without.Python tests (
pytest):test_esa_income_held_outside_award.py(new): 3 passed, one for each way of holding the award.test_esa_income_abolition_properties.py: the 29 parameter and fixed-case tests passed in one run. CI ran the whole file at5a5ac5fb1, and all 31 tests passed, includingtest_abolition_invariants, which now checks invariant 12, andtest_awards_match_an_independent_reading.test_uc_legacy_closure_properties.py:test_the_closure_flag_only_decides_closure_claimspassed (357 s) andtest_a_closure_claim_pays_the_universal_credit_of_any_claimpassed (617 s), each at full size with a 3,400 s cap.test_disable_simulated_benefits.py: CI ran the file at5a5ac5fb1, and all 89 tests passed.holds_formula_awardat5a5ac5fb1: 9 of 11 passed. The 2 failures reproduce the limits in theown_report_is_paidbullet above: a stored award that leaves an outside member nothing, and a reform that scalesesa_incomewith a pension-age outside member.Fails on
origin/main(2da212a): with the reproducer's inputs (a single claimant aged 40 in London reporting £5,000),mainpays £5,000 of income-related ESA in 2027-28 against £0 here, and £5,000 in 2026-27 against £1,178.08 without a claim.We ran each mutant as one change to a copy of the branch, against
esa_income_abolition.yamlunless the row names another test. The first row ran ata3f8b1104. The rest ran on the branch's earlier code, up to68ccc0856, and the later commits leave their mutated lines unchanged. Thebenunit.sumrow, the row for a saving for a member who reports nothing and the last two rows ran at5a5ac5fb1.holds_formula_awarddrops the claimant's and partner's comparison (test_esa_income_held_outside_award.py)claims_uc_at_legacy_closurelegacy_award_is_closedignores income-related ESAbenunit.sumfor the couple's sharetest_disable_simulated_benefits.py -k no_family_is_moved)continuing_awarddrops the accommodation term (withhousing_benefit_eligible.yaml)own_report_is_paidtreats every held award as the formula's (test_esa_income_held_outside_award.py)Existing tests changed
No expected value in a test on
mainchanges. At7fe7360faone case this branch added changes, as the list above records, and comments inbenefit_cap_by_scheme.yaml,housing_benefit_universal_credit_passport.yaml,test_benefit_cap_uc_and_hb_properties.pyandtest_housing_benefit_universal_credit_passport_properties.pyno longer say the model lacks the route; those cases still supply both awards so that they test the cap or the passport alone. Two property-test reference calculations now include Universal Credit claims prompted by the ESA abolition. Cases about other rules opt out of that claim.test_uc_legacy_closure_properties.py:expected_closurecloses an unsaved income-related ESA report from 2027-28 under the Pension Credit conditions; a newexpected_reachedgives the abolition's reach from 2026-27; a family claims on either; and a claiming family gets no income-related ESA. The flag property and the closure-claim differential treat a reached family as moved.test_legacy_award_work_properties.pyandlegacy_award_work_reference.py: in 2026-27 the couple's ESA reports close when the abolition reaches them, because the family claims withwould_claim_uc_at_legacy_closureat its default.test_disable_simulated_benefits.py: new assertions that the abolition reaches the claimant's own award from 2026-27 without the reform, and that the reform prompts no claim.Cases about other rules set
would_claim_uc_at_legacy_closurefalse, so the family keeps the award they test:esa_income_remunerative_work.yaml, the two 2026-27 limit cases: they test the permitted-work limit, and a claim would close the report.pension_credit/mixed_age_couples.yaml, "HB alongside income-related ESA is working-age HB, so no saving": it tests the saving inference and the continuing Housing Benefit award, which a claim would end.test_mixed_age_couple_properties.py, every generated family: its reference formulas from before the mixed-age couple rules readwould_claim_ucalone.In Case 1 of
universal_credit/person_legacy_benefits_closed.yaml, the claimant getsesa_income_appointee_savinginstead: the case checks that an outside member's closed report leaves the couple open, and from 2027-28 the claimant's own unsaved report would close the couple.Thirteen existing YAML cases and four Python files test another rule on an income-related ESA award in 2026-27 or 2027-28, so each ESA reporter there gets
esa_income_appointee_saving: true, which keeps the award payable for the whole year (inputs only):esa_income/esa_income_capital.yaml: "has no tariff income at the lower capital threshold", "tariff income rounds incomplete capital steps up" and "remains eligible at the upper capital limit" (2026);esa_income/esa_income_remunerative_work.yaml: "Issue 2395 outside closed IS report leaves the couple ESA award open" (2027);legacy_award_readers/council_tax_reduction_passports.yaml: the five non-dependant cases (2027);legacy_award_readers/housing_benefit_pension_age_regulations.yaml: the two ESA cases (2026);legacy_award_readers/pension_credit_earnings_disregard.yaml: "Another member's income-related ESA leaves a single pensioner with the £5 earnings disregard" and "The partner's income-related ESA gives the couple the £20 earnings disregard" (2026);test_esa_permitted_work_nlw_reforms.py(2026), the two 2027 Merton non-dependant tests intest_legacy_award_readers_properties.py, the 2027 cases oftest_legacy_closure_scope.py, and every award intest_esa_income_universal_credit_claim_properties.py, whose 2027 cases test the Universal Credit claim under art. 3A(3).Overlap
dc69f3adamerges cleanly withmainat52b30809f(git merge-tree --write-tree --name-only).Bar income-related ESA at pensionable age and on Pension Credit #2807 (
fix-2565-esa-pension-credit, headd0ffe7fd3) already merges3c9890a19, anddc69f3adamerges cleanly with it. No case or property in Bar income-related ESA at pensionable age and on Pension Credit #2807 setsin_specified_or_temporary_accommodation, and without itdc69f3ada's take-up rule gives the same result as3c9890a19's, so the change moves none of Bar income-related ESA at pensionable age and on Pension Credit #2807's results. Bar income-related ESA at pensionable age and on Pension Credit #2807's head already gives the ESA reporters of Cases 4, 5, 6, 12, 13 and 14 ofesa_income_pension_age.yamlesa_income_appointee_saving: true, because each asserts a full-year income-related ESA award in 2026-27.Abolish working-age Housing Benefit from 1 July 2026 (NI 1 October 2026) #1910 (
abolish-working-age-housing-benefit, head2d0e49417) conflicts withmainat52b30809finhousing_benefit_eligible.pyandhousing_benefit/takeup.yaml. Withdc69f3adait conflicts in those two files and inclaims_uc_at_legacy_closure.py,mixed_age_couples.yamlanduc_housing_costs_element.py. Abolish working-age Housing Benefit from 1 July 2026 (NI 1 October 2026) #1910 adds the same kind of term for Housing Benefit toclaims_uc_at_legacy_closure, and the same flag line to themixed_age_couples.yamlcase; a merge ORs both abolition terms and keeps one copy of the flag line. The conflicts inhousing_benefit_eligible.pyanduc_housing_costs_element.pycome from Abolish working-age Housing Benefit from 1 July 2026 (NI 1 October 2026) #1910 making the same two routing changes as this branch. Inuc_housing_costs_element.pyboth return nil forin_specified_or_temporary_accommodation, and a merge keeps either text. Inhousing_benefit_eligible.pyboth OR the input into the routes before the tenure and capital tests and drop it from the continuing award; a merge keeps Abolish working-age Housing Benefit from 1 July 2026 (NI 1 October 2026) #1910's continuing award, which addshousing_benefit_payable_share, and one accommodation term. Under SI 2025/1148 art. 7(4)(b) and SR 2025/176 art. 7(4)(b), an appointee-saved income-related ESA claimant keeps Housing Benefit, andhousing_benefit_payable_shareat Abolish working-age Housing Benefit from 1 July 2026 (NI 1 October 2026) #1910's head has no input for that saving. Whichever PR lands second makes it readesa_income_appointee_saving, or the merged model ends that Housing Benefit on the appointed day. Abolish working-age Housing Benefit from 1 July 2026 (NI 1 October 2026) #1910 doesn't changewould_claim_housing_benefit, so a merge keeps the hand-entered take-up rule.Test PC, CB and CTC QYP exclusion of young Income Support recipients #2302 and Add CB and CTC qualifying young person routes for school leavers #2301 conflict with this branch only where they already conflict with
main, intest_child_and_adult_definitions_properties.py. Add overseas and other real estate to property wealth and capital #2688, Fix structural reforms for their requested periods #2670, Place the UC and Pension Credit QYP terminal date from the birth date #2300, Count 16-year-olds outside education as UC and Pension Credit qualifying young persons until the 1 September after their 16th birthday #2279 and Value means-tested capital less 10% sale expenses and secured debt #1969 merge cleanly with it. Fix HB and national CTR non-dependant deductions #2007 (headfde435bd3) and this branch both rewrite the comment aboveCLAIMANT_OR_PARTNER_BENEFITSinbenefit_cap_welfare_benefits.py, so they conflict there; a merge keeps Fix HB and national CTR non-dependant deductions #2007's account of the Universal Credit cap and this branch's sentences on specified or temporary accommodation.For policy changes: YAML tests whose expected values come from an external source:
esa_income_abolition.yamlworks each amount by hand from art. 3A's dates and ESA Regs 2008 reg 118, and the Housing Benefit cases from SI 2014/1230 regs 6A(2), 8(3) and 46(2), SR 2016/226 reg 6(3), the Universal Credit rates and caps in the parameter files, and the means test with no income.make formatequivalent on the changed Python files:ruff format --check,ruff check,ruff check --select F811,F821 policyengine_uk,git diff --check(ruff 0.17.2).Changelog fragment
changelog.d/2562.fixed.md.Reproducer
Reproducer: policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_abolition.yaml::Great Britain 2027-28 pays no income-related ESA
axiom: TheAxiomFoundation/rulespec-uk#518 queued
🤖 Generated with Claude Code