Skip to content

Disregard WTC before the Housing Benefit childcare deduction - #2776

Open
MaxGhenis wants to merge 32 commits into
mainfrom
fix-2730-hb-wtc-disregard
Open

MaxGhenis wants to merge 32 commits into
mainfrom
fix-2730-hb-wtc-disregard

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2730

Also fixes #2777.

What and why

The model now disregards up to £17.10 a week of Working Tax Credit (WTC) in Housing Benefit before it deducts childcare. Under Schedule 5 paragraph 56 of the Housing Benefit Regulations 2006, the authority administering Housing Benefit disregards that amount for a family that meets a paragraph 17(2) work condition when its earnings can't fund the additional earnings disregard. The new housing_benefit_wtc_disregard takes the lesser of WTC and £889.20 a year (£1,929.20 in 2020-21). housing_benefit_childcare_earnings_limit lets childcare draw only on the WTC left after it, and housing_benefit_applicable_income deducts it alongside the earnings disregard. The existing gov.dwp.tax_credits.active parameter switches it off from model year 2025, when tax credits end.

In the issue's example, a lone parent earns £5,200 a year, has a £1,300 standard disregard, pays £4,160 for childcare and gets £100 of WTC. The model now disregards the £100 and deducts £3,900 for childcare, where main deducts £4,000.

The model also settles the WTC disregard in each accommodation segment of the calculation that #2234 added to housing_benefit_entitlement. Where a reform adds a defined_for condition to an amount the segments rebuild and keeps the model's formula, each segment now applies the condition as the annual output does (#2777).

The regulations

Under paragraph 56, the authority disregards up to £17.10 of WTC, "any amount of working tax credit up to £17.10", except "in a case which falls under sub-paragraph (1) of paragraph 17 of Schedule 4". Under paragraph 17(1), it raises the earnings disregard by £17.10 where "his net earnings equal or exceed the total" of the other earnings disregards, "the amount of child care charges calculated as deductible under regulation 27(1)(c)" and £17.10. Under regulation 40(2), it leaves out of other income "any sum, where applicable, specified in Schedule 5". Under regulation 27(1)(c), it then deducts childcare from earnings and, where earnings fall short and the family gets a tax credit, from "those earnings plus whichever credit" applies. In its BW2 guidance, DWP tells authorities to make that deduction "as the final step in the earnings calculation" (W2.131), to offset the rest against WTC or CTC "in addition to the existing disregards" (W2.181), and never to offset it against other income (W2.182).

The authority follows the same order under the pension-age and Northern Ireland regulations, and the model applies one calculation to all four:

Regulations Additional earnings test WTC disregard Childcare deduction
SI 2006/213 (working age) Sch 4 para 17 Sch 5 para 56 reg 27(1)(c)
SI 2006/214 (pension age) Sch 4 para 9 Sch 5 para 21 reg 30(1)(c)
SR 2006/405 (NI working age) Sch 5 para 17 Sch 6 para 58 reg 24(1)(c)
SR 2006/406 (NI pension age) Sch 5 para 9 Sch 6 para 22 reg 28(1)(c)

Under SI 2020/371 reg 5 and SR 2020/53 reg 5, which the Secretary of State made, the authority read £37.10 for £17.10 in both disregards, in all four sets of regulations, from 6 April 2020 to the end of 4 April 2021. That period spans the 52 weeks from Monday 6 April 2020 to Sunday 4 April 2021. The model's fiscal year 2020 runs to 5 April 2021, and the model takes the £1,929.20 for those 52 weeks from the dated worker-disregard parameter it already has.

Where a claimant gets only the savings credit of Pension Credit, the authority may modify the Secretary of State's income figure under SI 2006/214 reg 27(4) "only in so far as necessary to take into account" the items in its list. The list includes "childcare charges taken into account under regulation 30(1)(c)" and the Schedule 4 paragraph 9(1) additional earnings disregard, but not the Schedule 5 paragraph 21 WTC disregard, and SR 2006/406 reg 25(4) has the same list. So on that route the model corrects the childcare charge for the disregarded WTC and deducts no WTC disregard from income.

Limit, deduction, WTC disregard

The paragraph 17 total includes the childcare deduction, and under regulation 27(2) that deduction draws on the WTC left after the paragraph 56 disregard, so each decision needs the other. The model settles them in one direction:

  1. housing_benefit_childcare_earnings_limit keeps back the WTC that the authority would disregard under paragraph 56 if childcare took the whole capped charge. It reads neither the deduction nor the housing_benefit_wtc_disregard formula.
  2. housing_benefit_applicable_income_childcare_element takes the lesser of the capped charge and that limit.
  3. housing_benefit_wtc_disregard and the additional earnings disregard test paragraph 17 with the deduction in force in each segment.

For the deduction the model calculates, the WTC the limit keeps back equals the WTC disregard. Write E for the earnings the standard and accommodation disregards leave, C for the capped charge, a for the additional amount, R for the lesser of WTC and a, and T for CTC. If E ≥ C + a, earnings reach the paragraph 17(1) total with the whole charge and with any smaller deduction, and neither test disregards WTC. Otherwise the limit keeps back R, and the deduction comes to D = min(C, E + (WTC − R) + T). Either D = C, or D < C and D ≥ E; either way E < D + a, so earnings fall short of the paragraph 17(1) total and the model disregards R. Keeping back no WTC would only enlarge the deduction, and earnings would still fall short, so R gives the only consistent answer. test_limit_keeps_back_the_wtc_disregard_in_force checks the identity for every generated family, and the segment property checks it within each segment.

A reform can replace the deduction with a formula that reads the limit. With half the capped charge, capped at the limit, the issue's family gets a £2,080 deduction and the additional earnings disregard, and income of £1,030.80. Where the limit binds, the WTC it keeps back stays consistent: with £2,000 of earnings, a £260 standard disregard, a £9,100 charge and £1,000 of WTC, the deduction comes to £1,850.80 and the model disregards £889.20 of WTC.

Supplied and replaced values

  • WTC disregard. The limit reads the WTC disregard only where reading it reads no other variable. That covers an amount that a dataset, set_input or a scenario supplies, and a disregard that a reform neutralizes. It also covers one that a reform leaves without a formula, an adds or subtracts aggregate or a defined_for mask, which the engine (policyengine-core) uprates or carries over from its own earlier inputs. The model applies each of these to income, to the limit and in every segment, and neutralizing housing_benefit_wtc_disregard gives main's calculation back. The engine computes any other replacement through a formula, an aggregate (adds or subtracts, each a list or the path of a parameter that lists variables) or a mask it calculates first, and any of these could read the deduction. The model applies such a replacement to income and, as an annual amount, in every segment, but the limit keeps back what the whole-charge test gives, so a forwarding replacement leaves the limit's figure unchanged. When a reform only adds a defined_for mask and keeps the model's formula, the annual output takes the mask, and each 2026-27 segment applies the same condition to its own WTC disregard (see Reform conditions in each segment).
  • Childcare deduction and its limit. The model applies a deduction that a dataset, set_input or a reform supplies or replaces, or the lesser of the capped charge and a supplied or replaced limit, in every segment, and reads it in both paragraph 17 tests. Where the deduction itself comes from a dataset, set_input or a reform, only a replacement formula that calls the limit reads it. The limit still keeps back the WTC that the whole-charge test gives, so it can differ from main: with the issue's family and a supplied nil deduction, it comes to £3,900 where main gives £4,000, while the model disregards no WTC.
  • Earnings disregard. A supplied housing_benefit_applicable_income_disregard stays as supplied, and the model still makes the WTC decision's own paragraph 17 test from the earnings inputs, because a supplied total doesn't show whether it includes the additional amount.

Accommodation segments

From 5 October 2026, the paragraph 17(3)(a) total also includes the paragraph 18 specified or temporary accommodation disregard. The Secretary of State made that change by SI 2026/753 reg 2(4), and replaced that regulation by SI 2026/978 reg 2(2) before it came into force on the date in reg 1. The Department for Communities made the same change in Northern Ireland by SR 2026/157 reg 2(4)(a). Since #2234, the model settles paragraph 17 separately before and after that date and combines the awards by days. The authority's paragraph 56 decision depends on the paragraph 17 outcome, so the model now gives each segment its own WTC disregard and its own limit. hb_segment_deductions returns the segment's earnings disregard, WTC disregard and childcare deduction, housing_benefit_entitlement deducts all three in each segment, and the annual housing_benefit_wtc_disregard combines the segments' amounts by days.

Tax credits ended on 5 April 2025, before the accommodation disregard starts, so no current-law award changes. Take a reform that sets gov.dwp.tax_credits.active to true in 2026, and a single claimant aged 40 in specified accommodation. The claimant earns £500, has a £260 standard disregard, £2,000 of WTC and no childcare or other income, pays rent of £10,400 and has a nil applicable amount. Main gives £9,022.21, and the model now gives £9,600.19:

  • From 6 April to 4 October 2026 (182 days), earnings of £500 fall short of £260 plus £889.20, so the model disregards £889.20 of WTC. Income comes to £2,500 − £260 − £889.20 = £1,350.80, and the award to £10,400 − 0.65 × £1,350.80 = £9,521.98.
  • From 5 October 2026 to 5 April 2027 (183 days), the £4,041.96 accommodation amount takes the £240 of earnings left, and earnings again fall short. Income comes to £2,500 − £500 − £889.20 = £1,110.80, and the award to £9,677.98.
  • Combined, (182 × £9,521.98 + 183 × £9,677.98) / 365 = £9,600.19.

The model needs a WTC disregard in each segment. With £2,000 of earnings, the same claimant's earnings cover £260 plus £889.20 before 5 October, so the model gives the additional earnings disregard and no WTC disregard. After 5 October the £1,740 accommodation amount leaves earnings short, and the model disregards £889.20 of WTC. The award comes to £9,114.03. A WTC disregard settled once from the annual accommodation amount would give both disregards before 5 October, and an award of £9,402.23. With £4,000 of earnings it would give neither disregard after 5 October, and £8,176.03 where the model gives £8,465.81.

Deducting the day-weighted annual amount in every segment matches the per-segment award while both segments stay on the taper. Once income after 5 October falls below a £2,000 applicable amount, it gives £10,268.74 instead of £10,124.25. Reading the childcare limit with that annual amount gives £7,394.22 instead of £7,266.74 for a lone parent whose limit binds after 5 October.

Reform conditions in each segment

policyengine-core masks a variable that has a defined_for condition: it keeps the amount where the condition exceeds zero and gives the variable's default elsewhere. The segment helpers rebuild amounts without calling those variables, so hb_apply_defined_for applies the same condition to each rebuilt amount. It reads the condition from the variable the simulation holds, calculates it for the year and maps it to the variable's entity. In each 2026-27 segment, and in the single segment of every other year:

Amount Where the model applies the condition
Accommodation disregard hb_accommodation_segments, to each segment's full amount
Earnings disregard hb_segment_earnings_disregard
WTC disregard hb_segment_wtc_disregard
Childcare limit and deduction hb_segment_childcare_deduction; hb_fixed_childcare_deduction applies the deduction's condition where a supplied or replaced limit sets it
Applicable income housing_benefit_entitlement, to each segment's income

The model still uses a supplied, neutralized or replaced annual amount in every segment, whatever its condition gives. When the WTC or earnings disregard's condition fails, the limit still keeps back the WTC that the whole-charge test gives and still subtracts the standard and accommodation disregards. To let those amounts fund childcare, a reform must change the limit too.

Take a reform that adds defined_for = "is_couple" to housing_benefit_wtc_disregard and keeps its formula, in 2026 with tax credits kept. A lone parent aged 60 continues a Housing Benefit award with a child aged 8 in specified or temporary accommodation, paying £10,400 of London council rent. They work 16 hours a week for £11,000, draw a £10,000 private pension, pay £12,000 for qualifying childcare and report £2,000 of WTC and £4,000 of CTC. From those reports the model calculates WTC of £6,530.35 and CTC of £3,646.49. It attributes £883.14 of the £1,686 of income tax to the wages, so net earnings come to £10,116.86 and income before deductions to £11,000 + £10,000 − £1,686 + £10,176.84 = £29,490.84. The lone parent disregard gives £1,300 (£25 a week, means_test/income_disregard/lone_parent.yaml) and the childcare cap £9,100 (£175 a week, working_tax_credit/elements/childcare_1.yaml). From 5 October the accommodation disregard gives £4,041.96 (£77.73 a week, specified_or_temporary_accommodation/lone_parent/older.yaml). The worker amount comes to £889.20 (£17.10 a week, income_disregard/worker.yaml) and the applicable amount to £4,968.60 (£95.55 a week, allowances/lone_parent/older.yaml). Net earnings fall short of £11,289.20 before 5 October and £15,331.16 after, so the model disregards £889.20 of WTC in each segment, and the condition takes it away:

  • From 6 April to 4 October (182 days), income comes to £29,490.84 − £1,300 − £9,100 = £19,090.84, and the award to £10,400 − 0.65 × (£19,090.84 − £4,968.60) = £1,220.54.
  • From 5 October (183 days), income comes to £29,490.84 − £5,341.96 − £9,100 = £15,048.88, and the award to £3,847.82.
  • Combined, the award comes to £2,537.78, with no benefit cap reduction, and annual income to £17,064.32. Without the condition in each segment, the model paid £3,115.76, 0.65 × £889.20 = £577.98 more, and household net income rose by the same amount. A reform that neutralizes the WTC disregard gives the same award and household net income as the condition. In 2024, with one segment, the condition gives £1,113.09 where the model gives £1,691.07.

A reform that replaces the earnings disregard's formula and adds a condition that reads the WTC disregard, the childcare limit or, in 2026-27, the accommodation disregard makes policyengine-core raise CycleError. The engine calculates the condition first, and with the replacement in place the condition's amount reads the annual earnings disregard the replacement gives, so the condition reads its own result. For a claimant in specified accommodation we found five such cases: a condition that reads the accommodation disregard, the WTC disregard or the limit in 2026-27, and one that reads the WTC disregard or the limit in 2024. Without the replacement, the model calculates each of these conditions. A condition that reads its own variable or applicable income raises CycleError with or without a replacement.

Invariants

For every generated population, with assessed earnings, disregards, childcare, credits and work conditions as inputs:

  • The WTC disregard lies between zero and the lesser of WTC and the statutory amount. It falls to zero when the work condition fails, when earnings meet paragraph 17(1), and when tax credits stop.
  • No benefit unit gets both the additional earnings disregard and a positive WTC disregard within a segment. Annual amounts can both exceed zero: with £2,000 of earnings in specified accommodation, the year has £443.38 of additional earnings disregard and £445.82 of WTC disregard. Exclusivity holds when a dataset or set_input supplies the deduction or its limit, and example tests cover a neutralized deduction, one with no formula and replacement formulas, one of which reads the limit.
  • A deduction the model calculates never exceeds the capped charge, or the earnings and credits the disregards leave. A supplied deduction stays as supplied: £25,000 stays £25,000 against a £4,160 charge.
  • Where the model calculates the deduction, its limit and the WTC disregard, the WTC the limit keeps back equals the WTC disregard, for the year and within each segment.
  • Assessed income equals, and never falls below, an independent scalar calculation of the regulations. That calculation settles paragraph 17 by trying both outcomes.
  • In 2026-27 the award equals the day-weighted sum of the awards the same calculation gives in each segment, and the annual WTC disregard equals the day-weighted sum of the segments' amounts.
  • Where the model disregards no WTC in any segment, every output equals main's, except the limit where a dataset or set_input supplies the deduction. Under current law that covers every family.
  • In a year with one accommodation amount, supplying the deduction the model calculates changes no result. In 2026-27, where the accommodation disregard changes, the model applies the supplied annual amount in every segment: replaying the lone parent's calculated £4,160 moves the award from £7,266.74 to £7,394.22.
  • A reform that replaces the childcare deduction, its limit or the WTC disregard, each alone, with one that forwards the model's own amount meets no circular definition and changes no output. For the WTC disregard, that holds whether the replacement forwards through a formula or through adds or subtracts, each listing the forwarding variable or naming a parameter that lists it. It holds in 2024 for families with and without WTC, and in 2026-27 with tax credits kept for families outside specified or temporary accommodation. On the example populations, every 2024 output stays identical and every 2026-27 output agrees within £0.001, the float32 rounding of the day weights. The computed replacements of the WTC disregard leave all six outputs of the eight test families unchanged in both years, and the generated populations allow 2p. In specified accommodation the model applies the replacement's annual amount in every segment, as for any replacement: a forwarding replacement of the WTC disregard, through a formula or an aggregate, gives £10,268.74 where the model gives £10,124.25.
  • The override check that segments use (hb_uses_annual_override) gives main's answer for all 1,200 combinations of the states the engine reads: neutralization, a default or replacement formula, adds and subtracts (none, empty, a list or a parameter path), a defined_for mask, uprating and a supplied input. Where only a defined_for mask joins the model's own formula, the check keeps the segment route, and each segment helper applies the condition itself.

Under a reform that gives each of the accommodation, earnings and WTC disregards, the childcare deduction, its limit and applicable income its own condition, which reads none of these amounts, for every generated population with or without tax credits:

  • Each annual disregard equals the day-weighted sum of its segment amounts.
  • Where a condition fails, its amount takes the variable's default in the annual output and in every segment, unless the population supplies the amount. Where applicable income's condition fails, the award equals the maximum benefit less 65% of the excess of the variable's default over the applicable amount.
  • A benefit unit whose conditions all hold keeps the model's outputs, award included.
  • The award agrees with an independent scalar calculation within each segment, combined by days.
  • In each year but 2026-27, the award the single segment gives equals the award the model builds from annual income.

Test plan

  • For policy changes: the 13 cases in housing_benefit_wtc_disregard.yaml and the Python examples take their expected values from the regulations, and their comments work through the arithmetic by hand.
  • make format: Ruff 0.17.2 formats and checks every changed Python file, ruff check --select F811,F821 policyengine_uk passes, and git diff --check passes.
  • Changelog fragments changelog.d/2730.fixed.md and changelog.d/2777.fixed.md

The tests build their own households and never load survey data. Rows that name 2f087ae7c ran at this branch's head, which merges main at 52b30809f; the other rows name the commit they ran at, and CI runs the rest of the suite.

Check Result
test_housing_benefit_wtc_disregard.py: examples, Hypothesis properties, differential tests and replacement tests 55 passed at 2f087ae7c
test_housing_benefit_wtc_disregard_segments.py: segment examples and properties 18 passed at 2f087ae7c
test_housing_benefit_segment_masks.py: a reform condition on each amount in each segment, #2777's reform, and the couples-only WTC disregard for a lone parent in 2026-27 and 2024 30 passed at 2f087ae7c
housing_benefit_wtc_disregard.yaml 13 passed at 2f087ae7c
test_housing_benefit_accommodation_dates_properties.py 6 passed at 2f087ae7c
test_housing_benefit_childcare_properties.py, which reads the limit 5 passed at 2f087ae7c
Housing Benefit YAML directory 597 passed at 2f087ae7c
policyengine_uk/tests/code_health 3,092 passed at 2f087ae7c
test_housing_benefit_accommodation_dates.py, with a 3,600 s cap 38 passed in 2,945 s at 6dc47664d
The two original test modules and the WTC YAML file on main (2da212abc) 55, 18 and 13 failed; the reproducer deducts £4,000 for childcare against £3,900
The couples-only lone parent at 2f087ae7c with the five model files back at 1d4a3e475 2026-27 fails: the award comes to £3,115.76 against £2,537.78. 2024 passes on the award and fails only where it reads the segment's WTC disregard, £889.20 against nil

The differential ran main (2da212abc) and this branch at fdc13f128 over the same 15,552 family-years: 1,944 families in each of 2018, 2020, 2021, 2024, 2025, 2026 and 2027, and in 2026 with tax credits kept. Since fdc13f128 the model code changes which replacements of the WTC disregard the limit reads, and applies reform conditions in each segment; none of these populations replaces the WTC disregard or adds a condition. Of each 1,944, 1,296 families have a calculated deduction, 324 a supplied nil deduction and 324 a supplied £1,000 limit. Earnings, WTC, childcare, the standard disregard, the work condition, pension age and specified accommodation vary; other income comes to £1,000, rent to £10,400 and the applicable amount to £2,000, and regions alternate between London and Northern Ireland. It compares the childcare deduction, earnings disregard, childcare limit, applicable income and award.

Family-years Where the model disregards WTC in no segment Where it disregards WTC in a segment
Calculated deduction 8,962: all 44,810 values equal main's exactly 1,406: every one moves by more than 1p
Supplied £1,000 limit 2,328: all 11,640 values equal main's exactly 264: every one moves
Supplied nil deduction 2,340: 11,578 of 11,700 values equal main's; the other 122 come from the limit, which keeps back the WTC the whole-charge test gives 252: every one moves

None of the 5,832 family-years in 2025, 2026 and 2027 under current law changes.

Of 44 mutations across the changed model files for #2730, 42 make at least one test fail. The other two add a floor or a cap to the paragraph 17 test, which can't change its answer while the additional amount stays positive. Each mutation runs the main module, the segment module and the WTC YAML file in full, with Hypothesis shrinking off. The 15 that touch the override helpers or their call sites ran in full at 6dc47664d. The other 29 ran at fdc13f128, or for the main module and YAML file at 9c2adcde9, whose model code and tests match fdc13f128. Since then the model code changes which replacements of the WTC disregard the limit reads, and applies reform conditions in each segment; the test modules keep every earlier test. For #2777, all 10 mutations of the condition helper and its call sites make a test fail; each ran the main module, the segment module and the segment-mask module in full at 2f087ae7c, with Hypothesis seed 0.

Mutation results
File Mutation Result
housing_benefit_childcare_earnings_limit.py The shared paragraph 17 test leaves out the accommodation disregard Caught: main module 3 failed; segment module 9 failed. (at fdc13f128)
housing_benefit_childcare_earnings_limit.py >= becomes > in the paragraph 17 test Caught: main module 4 failed; WTC YAML 1 failed. (at fdc13f128)
housing_benefit_childcare_earnings_limit.py The paragraph 17 test leaves out the standard disregard Caught: main module 13 failed; segment module 1 failed; WTC YAML 4 failed. (at fdc13f128)
housing_benefit_childcare_earnings_limit.py The paragraph 17 test floors net earnings at zero Survives, as intended: with a positive additional amount it can't change the answer (at fdc13f128)
housing_benefit_childcare_earnings_limit.py The paragraph 17 test caps the accommodation amount at the earnings left Survives, as intended: with a positive additional amount it can't change the answer (at fdc13f128)
housing_benefit_childcare_earnings_limit.py The WTC decision leaves the accommodation disregard out of the paragraph 17 test Caught: main module 3 failed; segment module 7 failed. (at fdc13f128)
housing_benefit_childcare_earnings_limit.py The disregard applies when earnings meet paragraph 17(1) Caught: main module 18 failed; segment module 8 failed; WTC YAML 3 failed. (at fdc13f128)
housing_benefit_childcare_earnings_limit.py The disregard ignores the work condition Caught: main module 2 failed; segment module 2 failed; WTC YAML 1 failed. (at fdc13f128)
housing_benefit_childcare_earnings_limit.py The disregard takes all WTC, uncapped Caught: main module 9 failed; segment module 7 failed; WTC YAML 4 failed. (at fdc13f128)
housing_benefit_childcare_earnings_limit.py A negative WTC award gives a negative disregard Caught: main module 1 failed. (segment module at fdc13f128; main module and YAML at 9c2adcde9)
housing_benefit_childcare_earnings_limit.py The disregard ignores the end of tax credits Caught: main module 4 failed; segment module 2 failed; WTC YAML 1 failed. (at fdc13f128)
housing_benefit_childcare_earnings_limit.py The cap uses the weekly amount Caught: main module 17 failed; segment module 8 failed; WTC YAML 7 failed. (segment module at fdc13f128; main module and YAML at 9c2adcde9)
housing_benefit_childcare_earnings_limit.py The limit reads the WTC disregard variable again Caught: main module 32 failed; segment module 10 failed; WTC YAML 10 failed. The forwarding-reform tests fail. The baseline itself meets a circular definition. (segment module at fdc13f128; main module and YAML at 9c2adcde9)
housing_benefit_childcare_earnings_limit.py The WTC kept back tests a supplied or replaced deduction in place of the capped charge Caught: main module 5 failed; segment module 1 failed. (at 6dc47664d)
housing_benefit_childcare_earnings_limit.py The WTC kept back tests the deduction variable in place of the capped charge Caught: main module 32 failed; segment module 10 failed; WTC YAML 10 failed. The forwarding-reform tests fail. The baseline itself meets a circular definition. (segment module at fdc13f128; main module and YAML at 9c2adcde9)
housing_benefit_childcare_earnings_limit.py The limit ignores a supplied, neutralized or input WTC disregard Caught: main module 3 failed; segment module 1 failed; WTC YAML 1 failed. (at 6dc47664d)
housing_benefit_childcare_earnings_limit.py The limit reads a replacement WTC disregard formula Caught: main module 4 failed; segment module 3 failed. (at 6dc47664d)
housing_benefit_childcare_earnings_limit.py A segment's limit keeps back the annual amount, not its own Caught: segment module 2 failed. (segment module at fdc13f128; main module and YAML at 9c2adcde9)
housing_benefit_childcare_earnings_limit.py The annual limit adds the segments' WTC without day weights Caught: segment module 1 failed. (segment module at fdc13f128; main module and YAML at 9c2adcde9)
housing_benefit_childcare_earnings_limit.py Childcare draws on the disregarded WTC Caught: main module 10 failed; segment module 3 failed; WTC YAML 4 failed. (segment module at fdc13f128; main module and YAML at 9c2adcde9)
housing_benefit_wtc_disregard.py The override check ignores a supplied or replaced limit Caught: main module 3 failed; segment module 2 failed; WTC YAML 1 failed. (at 6dc47664d)
housing_benefit_wtc_disregard.py The override check ignores a supplied or replaced deduction Caught: main module 9 failed; segment module 2 failed; WTC YAML 1 failed. (at 6dc47664d)
housing_benefit_wtc_disregard.py A supplied deduction reads the capped charge instead Caught: main module 10 failed; segment module 2 failed; WTC YAML 1 failed. The forwarding-reform tests fail. (segment module at fdc13f128; main module and YAML at 9c2adcde9)
housing_benefit_wtc_disregard.py A supplied limit sets the deduction without the charge cap Caught: main module 3 failed; segment module 2 failed. The forwarding-reform tests fail. (segment module at fdc13f128; main module and YAML at 9c2adcde9)
housing_benefit_wtc_disregard.py Each segment's deduction reads the annual limit Caught: segment module 3 failed. (segment module at fdc13f128; main module and YAML at 9c2adcde9)
housing_benefit_wtc_disregard.py Segments discard a supplied annual WTC disregard Caught: main module 5 failed; segment module 9 failed. (at 6dc47664d)
housing_benefit_wtc_disregard.py Segments ignore a supplied, neutralized or replaced annual WTC disregard Caught: segment module 9 failed. (at 6dc47664d)
housing_benefit_wtc_disregard.py The annual WTC disregard tests the capped charge, not the deduction in force Caught: main module 12 failed; segment module 2 failed; WTC YAML 2 failed. The forwarding-reform tests fail. (segment module at fdc13f128; main module and YAML at 9c2adcde9)
housing_benefit_wtc_disregard.py The annual amount adds segments without day weights Caught: segment module 7 failed. The forwarding-reform tests fail. (segment module at fdc13f128; main module and YAML at 9c2adcde9)
housing_benefit_wtc_disregard.py The annual amount comes from the annual accommodation amount Caught: segment module 5 failed. (segment module at fdc13f128; main module and YAML at 9c2adcde9)
housing_benefit_applicable_income_disregard.py A segment's WTC disregard tests the capped charge, not the deduction in force Caught: segment module 2 failed. The forwarding-reform tests fail. (segment module at fdc13f128; main module and YAML at 9c2adcde9)
housing_benefit_applicable_income_disregard.py Segments settle the WTC disregard from the annual accommodation amount Caught: segment module 6 failed. (segment module at fdc13f128; main module and YAML at 9c2adcde9)
housing_benefit_applicable_income_disregard.py The earnings disregard tests the capped charge, not the deduction Caught: main module 9 failed; segment module 1 failed; WTC YAML 2 failed. (segment module at fdc13f128; main module and YAML at 9c2adcde9)
housing_benefit_applicable_income.py Annual income leaves out the WTC disregard Caught: main module 9 failed; segment module 1 failed; WTC YAML 2 failed. (segment module at fdc13f128; main module and YAML at 9c2adcde9)
housing_benefit_entitlement.py Segment income leaves out the WTC disregard Caught: segment module 9 failed. (segment module at fdc13f128; main module and YAML at 9c2adcde9)
housing_benefit_entitlement.py Each segment deducts the day-weighted annual WTC disregard Caught: segment module 2 failed. (segment module at fdc13f128; main module and YAML at 9c2adcde9)
housing_benefit_specified_or_temporary_accommodation_disregard.py The override helper runs the formula of a neutralized variable Caught: main module 2 failed. (at 6dc47664d)
housing_benefit_specified_or_temporary_accommodation_disregard.py The helper lets the limit read an adds or subtracts aggregate Caught: main module 8 failed; segment module 4 failed. (at 6dc47664d)
housing_benefit_specified_or_temporary_accommodation_disregard.py The helper lets the limit read an adds aggregate Caught: main module 4 failed; segment module 2 failed. (at 6dc47664d)
housing_benefit_specified_or_temporary_accommodation_disregard.py The helper lets the limit read a subtracts aggregate Caught: main module 4 failed; segment module 2 failed. (at 6dc47664d)
housing_benefit_specified_or_temporary_accommodation_disregard.py The helper lets the limit read an aggregate named by a parameter path Caught: main module 4 failed; segment module 2 failed. (at 6dc47664d)
housing_benefit_specified_or_temporary_accommodation_disregard.py The helper lets the limit read a variable with a defined_for mask Caught: main module 1 failed. (at 6dc47664d)
housing_benefit_specified_or_temporary_accommodation_disregard.py The helper keeps an uprated input from the limit Caught: main module 1 failed. (at 6dc47664d)
housing_benefit_specified_or_temporary_accommodation_disregard.py Segments recalculate an aggregate replacement instead of applying its annual amount Caught: segment module 4 failed. (at 6dc47664d)
housing_benefit_specified_or_temporary_accommodation_disregard.py hb_apply_defined_for ignores every condition Caught: segment-mask module 17 failed; main and segment modules pass. (#2777, at 2f087ae7c)
housing_benefit_specified_or_temporary_accommodation_disregard.py A failed condition gives nil instead of the variable's default Caught: segment-mask module 1 failed; main and segment modules pass. (#2777, at 2f087ae7c)
housing_benefit_specified_or_temporary_accommodation_disregard.py The condition holds wherever it differs from zero, not where it exceeds zero Caught: segment-mask module 1 failed; main and segment modules pass. (#2777, at 2f087ae7c)
housing_benefit_specified_or_temporary_accommodation_disregard.py Segments leave the accommodation disregard's condition out Caught: segment-mask module 5 failed; main and segment modules pass. (#2777, at 2f087ae7c)
housing_benefit_applicable_income_disregard.py Segments leave the earnings disregard's condition out Caught: segment-mask module 7 failed; main and segment modules pass. (#2777, at 2f087ae7c)
housing_benefit_wtc_disregard.py A supplied or replaced limit sets the deduction without the deduction's condition Caught: segment-mask module 2 failed; main and segment modules pass. (#2777, at 2f087ae7c)
housing_benefit_wtc_disregard.py Segments leave the limit's condition out Caught: segment-mask module 3 failed; main and segment modules pass. (#2777, at 2f087ae7c)
housing_benefit_wtc_disregard.py Segments leave the childcare deduction's condition out Caught: segment-mask module 3 failed; main and segment modules pass. (#2777, at 2f087ae7c)
housing_benefit_wtc_disregard.py Segments leave the WTC disregard's condition out Caught: segment-mask module 9 failed; main and segment modules pass. (#2777, at 2f087ae7c)
housing_benefit_entitlement.py Segment income leaves applicable income's condition out Caught: segment-mask module 4 failed; main and segment modules pass. (#2777, at 2f087ae7c)

No test on main changes its expected value. #2719, #2700, #2727, #2731, #2735 and #2234 have merged, and this branch includes them. At 2f087ae7c, git merge-tree --write-tree --name-only against each open PR that edits this branch's files gives:

No other open PR edits the override helpers' file, the limit's file, the three test modules or the WTC YAML file. #2301 and #2302 no longer edit housing_benefit_applicable_income.py.

Reproducer

Reproducer: policyengine_uk/tests/test_housing_benefit_wtc_disregard.py::test_issue_2730_reserves_wtc_before_childcare, policyengine_uk/tests/test_housing_benefit_wtc_disregard_segments.py::test_wtc_disregard_applies_before_and_after_5_october, policyengine_uk/tests/test_housing_benefit_segment_masks.py::test_issue_2777_reform_masks_each_segment, policyengine_uk/tests/test_housing_benefit_segment_masks.py::test_a_couples_only_wtc_disregard_reaches_a_lone_parents_award

axiom: TheAxiomFoundation/rulespec-uk#510 queued

🤖 Generated with Claude Code

MaxGhenis and others added 20 commits October 30, 2026 18:44
Apply the Schedule 5 WTC disregard separately from earnings disregards and reserve it before the childcare deduction. Add statutory examples, properties, differential checks and historical cutoff coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The WTC disregard tested the full capped childcare charge, while the
additional earnings disregard reads the childcare deduction. A supplied
deduction, a supplied limit or a reform that replaces either could then
give a benefit unit both disregards. The WTC disregard now reads that
deduction whenever it differs from the model's own calculation, and keeps
the cycle-free full-charge test otherwise.

Tests add the accommodation disregard and supplied deductions to the
oracle, which now settles paragraph 17 by trying both outcomes. They add
a property that the two disregards never both apply and a differential
test that supplying the calculated deduction changes nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The WTC disregard compares the same net earnings and accommodation amount
as the additional earnings disregard. A cap on the accommodation amount
would not change the test, since earnings already fall short where it
binds. New tests cover a deduction with no formula and a negative WTC
award.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #2234's accommodation segments. The only conflict sat in the
supplied-input registry, where both sides add the two childcare names;
main's side lists them with #2234's three new names, so the merge keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#2234 settles Schedule 4 paragraph 17 separately before and after the
accommodation disregard starts on 5 October 2026, but its segment income
left out the WTC disregard. Paragraph 56 follows paragraph 17's outcome, so
each segment now has its own WTC disregard: the childcare limit takes it,
hb_segment_deductions returns it, and entitlement deducts it. The annual
variable combines the segments by days. The WTC and earnings decisions now
share one paragraph 17 test and one check for a supplied or replaced
childcare deduction, #2234's hb_uses_annual_override.

Under a reform that keeps tax credits in 2026-27, a claimant in specified
accommodation with £500 of earnings and £2,000 of WTC now gets £9,600.19
rather than £9,022.21. Current-law awards do not change: tax credits end
before the accommodation disregard starts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The WTC disregard is exactly nil where paragraph 56 does not apply, so the
checks now count any positive amount rather than one above 2p. A pinned
example gives earnings of £1,149.20, which cover the £260 standard
disregard, a supplied nil deduction and the £889.20 additional amount
exactly, with a 1p childcare charge and 1p of WTC.

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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Float32 stores £1,149.20 as £1,149.19995, so a plain float comparison would
find that earnings fall short of the £260 standard disregard plus £889.20
and grant the WTC disregard instead of the additional earnings disregard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The populations now supply the earnings and tax-credit limit as well as
the deduction, and the statutory oracle caps the deduction at the charge
under a supplied limit. A pinned case gives a £10,000 limit against a
£4,160 charge: the deduction stays at £4,160, the additional earnings
disregard applies, and paragraph 56 does not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After 5 October a lone parent with £8,000 of earnings, £3,000 of other
income, £2,000 of WTC and a £4,160 charge has paragraph 56 disregard
£889.20, so childcare can draw on only £3,768.84. The award comes to
£7,266.74; a limit read with the day-weighted annual WTC disregard would
give £7,394.21.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The earnings and tax-credit limit now keeps back the WTC that Sch 5 para
56 disregards by testing Sch 4 para 17 with the whole capped childcare
charge, so it reads neither the childcare deduction nor the
housing_benefit_wtc_disregard formula. The WTC disregard and the earnings
disregard then test para 17 with the deduction in force in each segment.
For the model's own deduction the two tests agree, so the limit, the
deduction and the disregard keep their values, and the direction runs one
way: limit, deduction, WTC disregard.

A reform that replaces the deduction, its limit or the WTC disregard with
a formula that forwards to the model's own no longer meets a circular
definition, and a replacement deduction can read the limit. A WTC
disregard that an input supplies, or that a reform neutralizes, still
reaches the limit and every segment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A supplied childcare deduction keeps its value above the charge and the
limit, the annual additional earnings and WTC disregards can both be
positive though no segment has both, and supplying the calculated annual
deduction in a segmented year applies it in every segment. The test
modules now build their forwarding reforms on demand, so they import
where housing_benefit_wtc_disregard does not exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In a year with two accommodation segments, the annual earnings and
tax-credit limit keeps back the day-weighted WTC disregard, as each
segment's limit keeps back its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The limit read a WTC disregard with no formula as a supplied amount, but
core still computes adds and subtracts there, and calculates a defined_for
mask before anything else, so an aggregate identity replacement cycled.
The limit now reads the WTC disregard only where reading it reads no other
variable: a neutralized variable, one with no formula, aggregate or mask
(which core uprates, carries over or defaults from its own inputs), or a
supplied input. hb_uses_annual_override still treats any variable without
a formula as an annual replacement in every segment.

Tests cover formula, adds and subtracts replacements, each as a list or a
parameter path, in 2024 and in 2026-27 with tax credits kept, and an
uprated input replacement with and without a mask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each reform system holds about 38 MB, and caching all eleven kept the
main test module above the 2 GB cap the mutation runs use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each computed replacement now runs one simulation against outputs the
model computes once for 2024 and once for 2026-27.

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 Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Aggregate impact on certified data: pass

Base 52b30809ff against head b61ab7a3fc, 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

Standard reform costings (change in gov_balance)

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise Class 1 main employee NICs rate to 10% 0 0 0 0 0 suppressed suppressed
Raise additional rate by 3pp 0 0 0 0 0 suppressed suppressed

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

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

Standard reforms: change in each reform's target

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise Class 1 main employee NICs rate to 10%: change in national_insurance 0 0 0 0 0 suppressed suppressed
Raise additional rate by 3pp: change in income_tax 0 0 0 0 0 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.

Checks failing on both base and head (1)
  • VAT excluding private school fees is never negative (sign:vat): 2025-26: 76 units below zero, a weighted £-53,888,684 in total; 2026-27: 76 units below zero, a weighted £-55,777,260 in total; 2027-28: 76 units below zero, a weighted £-57,727,917 in total
Counted-once check

Each of 92 components was raised by £1 per unit in 2026-27, and every head of its totals was read household by household: its own head must rise by exactly £1, no other tax head may move, no other benefit may rise, and the model's own total must move by what its heads moved. A head that breaks a rule is measured again twice: with every other head held at its baseline value, and with every other head held at its value after the first £1 while the component rises again. It fails where either still shows the response. 0 failed; 0 were skipped.

  • widowed_parents_allowance: pension_credit responds only through other heads in fewer than 10 households (other heads that moved there: carers_allowance; gov_spending; not a failure)
  • widowed_parents_allowance: pension_credit responds only through other heads in fewer than 10 households (other heads that moved there: carers_allowance; household_benefits; not a failure)
Provenance
Side Commit Runtime Environment hash Partition hash Time
base main 52b30809ff uk 2.172.0 (files f8400aea019e), core 3.34.1, py 3.13.18 aarch64 18199314be15 7cb61324a46b 2062.59s
head #2776 merged into main b61ab7a3fc uk 2.172.0 (files 27900fb6fb5f), core 3.34.1, py 3.13.18 aarch64 18199314be15 7cb61324a46b 2083.48s

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 £400.00bn
National Insurance (employee and self-employed) £51.70bn £54.14bn £56.13bn £57.73bn £59.76bn £61.64bn £63.51bn
National Insurance (employer) £148.71bn £155.37bn £160.88bn £165.09bn £172.06bn £177.36bn £182.57bn
Employer NI change borne by capital £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Employer NI change borne by consumers £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Capital gains tax £12.60bn £13.15bn £13.60bn £14.11bn £14.67bn £15.24bn £15.80bn
Student loan repayments £7.82bn £8.13bn £8.39bn £8.58bn £8.79bn £9.04bn £9.29bn
VAT excluding private school fees £249.37bn £256.87bn £266.33bn £272.70bn £278.97bn £285.24bn £291.59bn
Change in VAT from its baseline £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
VAT on private school fees £1.95bn £2.01bn £2.09bn £2.14bn £2.20bn £2.26bn £2.31bn
Change in private school VAT from its baseline £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Fuel duties £24.71bn £24.13bn £26.00bn £26.83bn £26.51bn £25.78bn £24.63bn
Alcohol duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Tobacco duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Vehicle excise duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Betting and gaming duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
TV licence fee £4.26bn £4.40bn £4.41bn £4.42bn £4.43bn £4.44bn £4.45bn
Council tax £49.21bn £52.16bn £54.89bn £57.95bn £61.16bn £64.47bn £67.91bn
High value council tax surcharge £0.00bn £0.00bn £0.00bn £0.48bn £0.49bn £0.50bn £0.51bn
Northern Ireland domestic rates £0.55bn £0.58bn £0.60bn £0.63bn £0.66bn £0.69bn £0.72bn
Business rates £34.68bn £36.20bn £38.00bn £39.46bn £42.27bn £43.25bn £44.29bn
Stamp duty land tax on the household's own purchases £6.88bn £7.32bn £7.66bn £8.06bn £8.47bn £8.91bn £9.35bn
Stamp duty land tax paid by companies £4.72bn £4.57bn £5.29bn £6.03bn £6.37bn £6.92bn £7.45bn
Land transaction tax (Wales) £0.07bn £0.08bn £0.08bn £0.09bn £0.10bn £0.10bn £0.11bn
Land and buildings transaction tax (Scotland) £0.31bn £0.32bn £0.33bn £0.35bn £0.36bn £0.38bn £0.40bn
Unspecified tax change borne by capital £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Unspecified tax change borne by consumers £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Wealth tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Wealth tax excluding the main residence £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Land value tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Carbon tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Spending by benefit and service: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Lifetime ISA bonus £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
State Pension £121.68bn £125.92bn £126.72bn £129.49bn £132.99bn £136.66bn £140.42bn
Pension Credit £6.66bn £7.03bn £7.29bn £7.48bn £7.60bn £7.84bn £8.03bn
Winter Fuel Payment £1.86bn £1.85bn £1.79bn £1.77bn £1.78bn £1.78bn £1.78bn
Pension Age Winter Heating Payment (Scotland) £0.19bn £0.20bn £0.20bn £0.21bn £0.21bn £0.22bn £0.22bn
Universal Credit £75.48bn £80.52bn £82.95bn £84.96bn £86.11bn £87.61bn £88.21bn
Housing Benefit £8.56bn £8.89bn £9.23bn £9.46bn £9.69bn £9.99bn £10.27bn
Council Tax Reduction £2.07bn £2.20bn £2.31bn £2.45bn £2.58bn £2.72bn £2.86bn
Employment and Support Allowance (income-related) £0.79bn £0.82bn £0.84bn £0.86bn £0.88bn £0.90bn £0.92bn
Employment and Support Allowance (contribution-based) £5.20bn £5.37bn £5.56bn £5.69bn £5.82bn £5.95bn £6.09bn
Jobseeker's Allowance (income-based) £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Jobseeker's Allowance (contribution-based) £0.29bn £0.30bn £0.31bn £0.31bn £0.32bn £0.33bn £0.34bn
Income Support £0.11bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Working Tax Credit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Child Tax Credit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Incapacity Benefit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Personal Independence Payment £26.95bn £28.03bn £29.05bn £30.09bn £30.75bn £31.51bn £32.21bn
Disability Living Allowance £7.49bn £7.79bn £8.07bn £8.36bn £8.55bn £8.76bn £8.95bn
Attendance Allowance £8.68bn £9.03bn £9.36bn £9.69bn £9.90bn £10.15bn £10.38bn
Carer's Allowance £4.48bn £4.66bn £4.83bn £5.00bn £5.11bn £5.27bn £5.42bn
Carer Support Payment (Scotland) £0.31bn £0.32bn £0.33bn £0.34bn £0.35bn £0.36bn £0.37bn
Carer's Allowance Supplement (Scotland) £0.00bn £0.04bn £0.04bn £0.05bn £0.05bn £0.05bn £0.05bn
Severe Disablement Allowance £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn
Industrial Injuries Disablement Benefit £0.50bn £0.52bn £0.54bn £0.55bn £0.56bn £0.58bn £0.59bn
Armed Forces Compensation Scheme £0.61bn £0.63bn £0.65bn £0.66bn £0.68bn £0.69bn £0.71bn
War Widow's or Widower's Pension £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Child Benefit £16.19bn £16.82bn £17.39bn £18.00bn £18.39bn £18.84bn £19.25bn
Scottish Child Payment £0.49bn £0.52bn £0.53bn £0.55bn £0.56bn £0.57bn £0.56bn
Two Child Limit Payment (Scotland, counterfactual) £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Healthy Start vouchers £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn
Maternity Allowance £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Statutory Maternity Pay £3.30bn £3.41bn £3.54bn £3.62bn £3.70bn £3.78bn £3.87bn
Statutory Paternity Pay £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Statutory Sick Pay £1.12bn £1.16bn £1.20bn £1.23bn £1.26bn £1.29bn £1.32bn
Sure Start Maternity Grant £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn
Bereavement Support Payment £0.67bn £0.69bn £0.71bn £0.73bn £0.75bn £0.76bn £0.78bn
Widowed Parent's Allowance £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Tax-Free Childcare £0.56bn £0.57bn £0.57bn £0.58bn £0.60bn £0.61bn £0.59bn
Extended childcare entitlement £2.99bn £3.50bn £3.59bn £3.71bn £3.78bn £3.86bn £3.93bn
Universal childcare entitlement £2.06bn £2.22bn £2.29bn £2.37bn £2.43bn £2.48bn £2.53bn
Targeted childcare entitlement £0.50bn £0.51bn £0.52bn £0.54bn £0.55bn £0.57bn £0.56bn
Childcare Offer for Wales £0.26bn £0.28bn £0.28bn £0.29bn £0.30bn £0.31bn £0.32bn
Care to Learn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Childcare Grant £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Parents' Learning Allowance £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn
Adult Dependants' Grant £0.06bn £0.06bn £0.04bn £0.04bn £0.04bn £0.04bn £0.04bn
Travel Grant £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed
16 to 19 Bursary Fund £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
NHS spending £203.89bn £204.30bn £204.56bn £204.91bn £205.34bn £205.85bn £206.35bn
State education spending £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn
Transport subsidy spending £25.05bn £25.10bn £25.14bn £25.18bn £25.23bn £25.30bn £25.36bn
Cost-of-living support payment £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Energy Bills Rebate £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Household Energy Payment £0.00bn £2.11bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Energy Price Guarantee subsidy £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Basic income £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Disability basic income £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Other public spending budget change £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Caseloads: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Lifetime ISA bonus 0k 0k 0k 0k 0k 0k 0k
State Pension 11,264k 11,145k 10,819k 10,700k 10,723k 10,750k 10,776k
Pension Credit 1,486k 1,480k 1,472k 1,452k 1,450k 1,455k 1,464k
Winter Fuel Payment 7,844k 7,783k 7,473k 7,375k 7,391k 7,409k 7,427k
Pension Age Winter Heating Payment (Scotland) 803k 802k 800k 797k 799k 801k 802k
Universal Credit 6,342k 6,347k 6,336k 6,297k 6,292k 6,271k 6,223k
Housing Benefit 1,249k 1,245k 1,244k 1,227k 1,227k 1,229k 1,232k
Council Tax Reduction 3,176k 3,195k 3,200k 3,214k 3,224k 3,235k 3,242k
Employment and Support Allowance (income-related) 71k 71k 71k 71k 71k 71k 71k
Employment and Support Allowance (contribution-based) 719k 720k 721k 722k 724k 726k 728k
Jobseeker's Allowance (income-based) 0k 0k 0k 0k 0k 0k 0k
Jobseeker's Allowance (contribution-based) 67k 67k 67k 67k 67k 68k 68k
Income Support 25k 0k 0k 0k 0k 0k 0k
Working Tax Credit 0k 0k 0k 0k 0k 0k 0k
Child Tax Credit 0k 0k 0k 0k 0k 0k 0k
Incapacity Benefit 0k 0k 0k 0k 0k 0k 0k
Personal Independence Payment 3,567k 3,575k 3,579k 3,585k 3,593k 3,602k 3,611k
Disability Living Allowance 1,291k 1,294k 1,296k 1,298k 1,300k 1,304k 1,307k
Attendance Allowance 1,695k 1,698k 1,700k 1,703k 1,707k 1,711k 1,715k
Carer's Allowance 1,034k 1,037k 1,038k 1,040k 1,042k 1,060k 1,062k
Carer Support Payment (Scotland) 71k 71k 71k 72k 72k 72k 72k
Carer's Allowance Supplement (Scotland) 0k 71k 71k 72k 72k 72k 72k
Severe Disablement Allowance 4k 4k 4k 4k 4k 4k 4k
Industrial Injuries Disablement Benefit 137k 137k 137k 138k 138k 138k 139k
Armed Forces Compensation Scheme 93k 93k 94k 94k 94k 94k 94k
War Widow's or Widower's Pension 0k 0k 0k 0k 0k 0k 0k
Child Benefit 8,123k 8,120k 8,120k 8,130k 8,145k 8,161k 8,178k
Scottish Child Payment 351k 352k 352k 353k 350k 348k 337k
Two Child Limit Payment (Scotland, counterfactual) 0k 0k 0k 0k 0k 0k 0k
Healthy Start vouchers 200k 200k 200k 201k 201k 201k 202k
Maternity Allowance 0k 0k 0k 0k 0k 0k 0k
Statutory Maternity Pay 333k 333k 334k 334k 335k 336k 337k
Statutory Paternity Pay 0k 0k 0k 0k 0k 0k 0k
Statutory Sick Pay 181k 182k 182k 182k 182k 183k 183k
Sure Start Maternity Grant 26k 26k 26k 26k 26k 26k 26k
Bereavement Support Payment 98k 98k 98k 98k 98k 99k 99k
Widowed Parent's Allowance 0k 0k 0k 0k 0k 0k 0k
Tax-Free Childcare 1,043k 1,027k 1,014k 1,009k 1,011k 1,009k 993k
Extended childcare entitlement 795k 800k 796k 797k 798k 798k 792k
Universal childcare entitlement 1,071k 1,072k 1,072k 1,073k 1,076k 1,079k 1,074k
Targeted childcare entitlement 165k 156k 154k 154k 155k 155k 150k
Childcare Offer for Wales 39k 39k 39k 39k 39k 39k 39k
Care to Learn 0k 0k 0k 0k 0k 0k 0k
Childcare Grant 0k 0k 0k 0k 0k 0k 0k
Parents' Learning Allowance 98k 98k 98k 99k 99k 99k 99k
Adult Dependants' Grant 18k 18k 12k 12k 12k 12k 12k
Travel Grant 0k 0k 0k 0k 0k 0k 0k
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed
16 to 19 Bursary Fund 0k 0k 0k 0k 0k 0k 0k
Cost-of-living support payment 0k 0k 0k 0k 0k 0k 0k
Energy Bills Rebate 0k 0k 0k 0k 0k 0k 0k
Household Energy Payment 0k 21,065k 0k 0k 0k 0k 0k
Energy Price Guarantee subsidy 0k 0k 0k 0k 0k 0k 0k
Basic income 0k 0k 0k 0k 0k 0k 0k
Disability basic income 0k 0k 0k 0k 0k 0k 0k
Government balance, each component counted once: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Government receipts, each component once £892.00bn £934.66bn £980.50bn £1,015.93bn £1,054.24bn £1,091.56bn £1,124.89bn
Government spending £529.38bn † £545.66bn † £550.77bn † £559.51bn † £567.20bn † £575.95bn † £583.60bn †
Government balance, each receipt once £362.62bn † £389.00bn † £429.74bn † £456.42bn † £487.04bn † £515.61bn † £541.29bn †
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.01bn £601.00bn £626.70bn £648.42bn
pre_budget_change_household_tax £484.15bn £512.28bn £542.82bn £566.95bn £591.72bn £617.16bn £638.61bn
gov_spending £529.38bn † £545.66bn † £550.77bn † £559.51bn † £567.20bn † £575.95bn † £583.60bn †
household_benefits £529.38bn † £545.66bn † £550.77bn † £559.51bn † £567.20bn † £575.95bn † £583.60bn †
gov_balance £362.62bn † £389.00bn † £429.74bn † £456.42bn † £487.04bn † £515.61bn † £541.29bn †
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.69bn † £1,687.82bn † £1,715.45bn † £1,747.49bn † £1,778.69bn † £1,817.02bn † £1,859.69bn †
Household net income (HBAI definition) £1,449.57bn £1,499.83bn £1,530.86bn £1,565.17bn £1,597.89bn £1,635.60bn £1,676.61bn
Population: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
People 69,245k 69,383k 69,473k 69,592k 69,738k 69,912k 70,080k
Benefit units 35,110k 35,181k 35,226k 35,286k 35,360k 35,449k 35,534k
Households 31,046k 31,108k 31,149k 31,201k 31,267k 31,345k 31,420k
Poverty rates: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Absolute poverty, before housing costs: everyone 16.3% 15.5% 16.4% 16.3% 16.5% 16.5% 16.6%
Absolute poverty, before housing costs: children 24.7% 22.1% 23.3% 22.9% 23.1% 23.2% 23.5%
Absolute poverty, before housing costs: working-age adults 14.1% 13.8% 14.7% 14.7% 14.8% 14.8% 15.0%
Absolute poverty, before housing costs: pensioners 13.7% 13.4% 14.0% 13.8% 13.9% 13.9% 13.9%
Absolute poverty, after housing costs: everyone 21.6% 21.0% 22.0% 22.6% 22.9% 23.1% 23.4%
Absolute poverty, after housing costs: children 33.3% 31.3% 32.3% 32.5% 32.9% 33.4% 34.2%
Absolute poverty, after housing costs: working-age adults 19.9% 19.8% 20.8% 21.6% 21.9% 22.0% 22.3%
Absolute poverty, after housing costs: pensioners 12.7% 12.4% 13.2% 13.2% 13.3% 13.4% 13.6%
Relative poverty, before housing costs: everyone 17.0% 16.0% 16.4% 16.3% 16.4% 16.3% 16.6%
Relative poverty, before housing costs: children 25.9% 22.9% 23.3% 22.9% 23.0% 22.9% 23.5%
Relative poverty, before housing costs: working-age adults 14.5% 14.1% 14.6% 14.7% 14.7% 14.7% 15.0%
Relative poverty, before housing costs: pensioners 14.5% 14.0% 13.9% 13.8% 13.8% 13.9% 13.9%
Relative poverty, after housing costs: everyone 21.4% 21.0% 21.3% 21.3% 21.3% 21.5% 21.7%
Relative poverty, after housing costs: children 33.1% 31.1% 31.2% 31.0% 31.1% 31.2% 31.8%
Relative poverty, after housing costs: working-age adults 19.9% 19.8% 20.2% 20.4% 20.5% 20.6% 20.8%
Relative poverty, after housing costs: pensioners 12.5% 12.4% 12.2% 11.7% 11.7% 11.8% 12.0%
People in poverty: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Absolute poverty, before housing costs: everyone 11,278k 10,779k 11,402k 11,365k 11,476k 11,513k 11,651k
Absolute poverty, before housing costs: children 3,688k 3,314k 3,496k 3,445k 3,480k 3,502k 3,549k
Absolute poverty, before housing costs: working-age adults 5,965k 5,893k 6,308k 6,362k 6,420k 6,430k 6,521k
Absolute poverty, before housing costs: pensioners 1,624k 1,572k 1,598k 1,558k 1,576k 1,580k 1,581k
Absolute poverty, after housing costs: everyone 14,944k 14,600k 15,310k 15,721k 15,937k 16,141k 16,426k
Absolute poverty, after housing costs: children 4,975k 4,682k 4,837k 4,888k 4,945k 5,040k 5,179k
Absolute poverty, after housing costs: working-age adults 8,463k 8,461k 8,971k 9,344k 9,491k 9,584k 9,706k
Absolute poverty, after housing costs: pensioners 1,506k 1,457k 1,502k 1,490k 1,501k 1,517k 1,542k
Relative poverty, before housing costs: everyone 11,750k 11,084k 11,388k 11,364k 11,416k 11,415k 11,651k
Relative poverty, before housing costs: children 3,872k 3,423k 3,496k 3,445k 3,466k 3,454k 3,549k
Relative poverty, before housing costs: working-age adults 6,159k 6,013k 6,301k 6,362k 6,389k 6,389k 6,521k
Relative poverty, before housing costs: pensioners 1,720k 1,648k 1,590k 1,557k 1,562k 1,572k 1,581k
Relative poverty, after housing costs: everyone 14,849k 14,552k 14,776k 14,809k 14,874k 15,014k 15,234k
Relative poverty, after housing costs: children 4,942k 4,660k 4,673k 4,663k 4,679k 4,707k 4,806k
Relative poverty, after housing costs: working-age adults 8,426k 8,440k 8,711k 8,823k 8,873k 8,967k 9,062k
Relative poverty, after housing costs: pensioners 1,482k 1,452k 1,392k 1,323k 1,322k 1,339k 1,365k
Inequality: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Gini coefficient, equivalised HBAI income before housing costs 0.3080 0.3052 0.3058 0.3058 0.3063 0.3069 0.3085
Mean household net income in each side's own deciles: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Decile 1: mean household_net_income per household £17,681 † £17,997 † £17,915 † £18,152 † £18,307 † £18,531 † £18,717 †
Decile 2: mean household_net_income per household £27,939 † £29,023 † £28,711 † £29,013 † £29,276 † £29,691 † £30,150 †
Decile 3: mean household_net_income per household £34,320 † £35,485 † £36,073 † £36,611 † £37,197 † £37,737 † £38,187 †
Decile 4: mean household_net_income per household £38,933 † £40,687 † £41,723 † £42,727 † £43,334 † £44,132 † £44,432 †
Decile 5: mean household_net_income per household £43,239 † £45,567 † £46,014 † £46,959 † £47,712 † £48,584 † £50,282 †
Decile 6: mean household_net_income per household £47,927 † £49,210 † £50,892 † £51,939 † £52,945 † £53,942 † £54,680 †
Decile 7: mean household_net_income per household £55,662 † £56,571 † £57,442 † £57,670 † £58,729 † £59,657 † £61,082 †
Decile 8: mean household_net_income per household £68,789 † £70,803 † £71,858 † £73,466 † £73,880 † £75,320 † £77,178 †
Decile 9: mean household_net_income per household £78,890 † £81,013 † £80,861 † £82,646 † £85,464 † £86,946 † £89,509 †
Decile 10: mean household_net_income per household £117,005 † £120,339 † £124,742 † £126,632 † £125,971 † £128,600 † £130,938 †
Standard reform costings (change in gov_balance): head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise basic rate by 1pp £6.41bn £6.66bn £6.63bn £6.85bn £7.03bn £7.25bn £7.49bn
Raise higher rate by 1pp £3.61bn £3.87bn £4.00bn £4.22bn £4.44bn £4.71bn £4.83bn
Raise personal allowance by ~800GBP/year £-3.80bn £-3.92bn £-3.98bn £-4.04bn £-4.10bn £-4.23bn £-1.95bn
Raise child benefit by 25GBP/week per additional child £-1.99bn £-1.75bn £-1.55bn £-1.38bn £-1.30bn £-1.18bn £-1.07bn
Reduce Universal Credit taper rate to 20% £-20.77bn £-21.32bn £-21.75bn £-21.99bn £-22.34bn £-22.75bn £-22.94bn
Raise Class 1 main employee NICs rate to 10% £10.89bn £11.30bn £11.71bn £12.01bn £12.31bn suppressed suppressed
Raise VAT standard rate by 2pp £24.89bn £25.71bn £26.63bn £27.24bn £27.87bn £28.50bn £29.14bn
Raise additional rate by 3pp £2.90bn £3.15bn £3.22bn £3.41bn £3.61bn suppressed suppressed
Raise the Universal Credit standard allowance by 10% £-3.06bn £-3.18bn £-3.26bn £-3.35bn £-3.44bn £-3.49bn £-3.50bn
Raise the State Pension by 10% £-7.78bn £-8.01bn £-8.24bn £-8.47bn £-8.82bn £-8.55bn £-8.89bn
Standard reforms: change in each reform's target: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise basic rate by 1pp: change in income_tax £6.55bn £6.86bn £6.79bn £7.00bn £7.20bn £7.44bn £7.66bn
Raise higher rate by 1pp: change in income_tax £3.61bn £3.92bn £4.01bn £4.23bn £4.45bn £4.72bn £4.84bn
Raise personal allowance by ~800GBP/year: change in income_tax £-3.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.74bn £21.31bn £21.78bn £22.04bn £22.37bn £22.78bn £22.99bn
Raise Class 1 main employee NICs rate to 10%: change in national_insurance £11.15bn £11.63bn £12.00bn £12.29bn £12.61bn suppressed suppressed
Raise VAT standard rate by 2pp: change in vat £24.69bn £25.51bn £26.42bn £27.03bn £27.65bn £28.28bn £28.91bn
Raise additional rate by 3pp: change in income_tax £2.90bn £3.15bn £3.22bn £3.42bn £3.62bn suppressed suppressed
Raise the Universal Credit standard allowance by 10%: change in universal_credit £3.06bn £3.19bn £3.28bn £3.37bn £3.43bn £3.49bn £3.51bn
Raise the State Pension by 10%: change in state_pension £10.68bn £11.17bn £11.28bn £11.60bn £12.03bn £12.47bn £12.97bn

@MaxGhenis
MaxGhenis marked this pull request as ready for review November 1, 2026 01:03
MaxGhenis and others added 6 commits October 31, 2026 22:49
A reform that adds defined_for to a Housing Benefit deduction and keeps the
model's formula masked only the annual output. In 2026-27 the segment
helpers rebuilt each period's amount without the condition, and the award
tapered income from those amounts. hb_apply_defined_for reads the condition
from the variable the simulation holds and applies it as core does, so each
segment's accommodation, earnings and WTC disregards, childcare deduction,
childcare limit and applicable income are nil where it fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One test each for the accommodation, earnings and WTC disregards, the
childcare deduction, its limit and applicable income, with the condition
failing and holding; the issue's reform; a supplied deduction; and a
condition, on benefit units or people, that holds for one of two benefit
units in a household. Hypothesis properties check that annual disregards
equal their day-weighted segments, failed conditions give nil, units whose
conditions hold keep the model's outputs, the award matches an independent
calculation within each segment, and one-segment years give the annual
route's award.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The annual earnings disregard summed hb_segment_deductions(...)[0], and that
helper calculated the WTC disregard the caller discarded. With the segment
mask in place, a reform giving the WTC disregard a defined_for condition
that reads the earnings disregard raised CycleError: the earnings formula
reached the WTC condition. hb_segment_earnings_disregard now returns the
earnings disregard and the childcare deduction, and hb_segment_deductions
adds the WTC disregard for entitlement. The amounts don't change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conditions of -1, 1 and 0 with a default of 7 give 7, 99 and 7 from Core
and from hb_apply_defined_for, and applicable income with a £17 default
reaches each 2026-27 segment. The tests fail if the helper uses nil instead
of the variable's default or tests the condition with != 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The limit's WTC reserve ignores a condition on the WTC disregard, and the
limit subtracts the standard disregard whatever the earnings disregard's
condition gives. The docstrings and M4 now say so, and a test pins both
cases with a limit a reform author can substitute. M2 states that a failed
condition gives the variable's default, and that the award is the maximum
benefit after non-dependant deductions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 2 commits November 1, 2026 05:25
Docstrings now say what each helper does and that a failed condition gives
the variable's default. The changelog names the amounts the helpers rebuild.
No code path changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Nov 1, 2026
The registration sat where #2765 adds self_employment_income, so the two
conflicted. The set's order means nothing; this place merges cleanly
with #1969, #2661, #2765 and #2776.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Name the annual overrides each helper keeps, state the award floor in M2
and limit M4 to the generated cases.

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.

Reviewed 1d4a3e4757f20b0b6cf8d36ad038784f1a9d2bc9. One confirmed blocker remains.

C1 — Respect the new WTC disregard's mask inside accommodation segments. At housing_benefit_wtc_disregard.py:53–57, adding only defined_for = "is_couple" retains the original formula code, so the annual-override check misses the mask and the segment path recalculates an unmasked disregard.

Executed 2026 reform case, with tax credits kept active: a single continuing-HB parent aged 60 with child 8, £11,000 wages/16 hours, £10,000 private pension, £12,000 qualifying childcare and £10,400 London council rent in specified/temporary accommodation; known WTC £2,000 and CTC £4,000. The false mask correctly gives annual WTC disregard £0 and assessed income £17,064.32, matching a neutralized-disregard control. Yet final HB remains £3,115.76 instead of £2,537.78: £577.98 too much, also entering household net income. Childcare is £9,100 and the benefit-cap reduction is zero in both controls. The 2024 one-segment control passes.

The older helper limitation recorded in #2777 is now exposed by this new variable's segment call. Please preserve the mask in segment deductions and add a final-HB regression while retaining cycle protection for replacement formulas.

Validation: 97 repository tests passed; the focused two-segment reproduction fails on the final-award contract. The corrected native control and original harness setup error are retained separately. Independent policy/source review is complete; exact-head hosted CI is green and does not cover this counterexample. Local runtime: Core 3.33.2.

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

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


Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Picking this up for the UK hub as brief 826, which replaces brief 814. Reply here if you're mid-change and I'll stop.


Generated by Claude Code

…ng Benefit

A reform that adds defined_for = "is_couple" to housing_benefit_wtc_disregard
and keeps its formula must take the disregard away in both 2026-27
accommodation segments, so the final award and household net income equal a
neutralized-disregard control. The 2024 case checks the one-segment year.

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

Copy link
Copy Markdown
Collaborator Author

PR_TITLE.txt

Disregard WTC before the Housing Benefit childcare deduction

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

PR_BODY.md

Fixes #2730

Also fixes #2777.

## What and why

The model now disregards up to £17.10 a week of Working Tax Credit (WTC) in Housing Benefit before it deducts childcare. Under Schedule 5 paragraph 56 of the Housing Benefit Regulations 2006, the authority administering Housing Benefit disregards that amount for a family that meets a paragraph 17(2) work condition when its earnings can't fund the additional earnings disregard. The new `housing_benefit_wtc_disregard` takes the lesser of WTC and £889.20 a year (£1,929.20 in 2020-21). `housing_benefit_childcare_earnings_limit` lets childcare draw only on the WTC left after it, and `housing_benefit_applicable_income` deducts it alongside the earnings disregard. The existing `gov.dwp.tax_credits.active` parameter switches it off from model year 2025, when tax credits end.

In the issue's example, a lone parent earns £5,200 a year, has a £1,300 standard disregard, pays £4,160 for childcare and gets £100 of WTC. The model now disregards the £100 and deducts £3,900 for childcare, where main deducts £4,000.

The model also settles the WTC disregard in each accommodation segment of the calculation that #2234 added to `housing_benefit_entitlement`. Where a reform adds a `defined_for` condition to an amount the segments rebuild and keeps the model's formula, each segment now applies the condition as the annual output does (#2777).

### The regulations

Under [paragraph 56](https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/56/2024-04-06), the authority disregards up to £17.10 of WTC, "any amount of working tax credit up to £17.10", except "in a case which falls under sub-paragraph (1) of paragraph 17 of Schedule 4". Under [paragraph 17(1)](https://www.legislation.gov.uk/uksi/2006/213/schedule/4/paragraph/17/2024-04-06), it raises the earnings disregard by £17.10 where "his net earnings equal or exceed the total" of the other earnings disregards, "the amount of child care charges calculated as deductible under regulation 27(1)(c)" and £17.10. Under [regulation 40(2)](https://www.legislation.gov.uk/uksi/2006/213/regulation/40/2024-04-06), it leaves out of other income "any sum, where applicable, specified in Schedule 5". Under [regulation 27(1)(c)](https://www.legislation.gov.uk/uksi/2006/213/regulation/27/2024-04-06), it then deducts childcare from earnings and, where earnings fall short and the family gets a tax credit, from "those earnings plus whichever credit" applies. In its [BW2 guidance](https://assets.publishing.service.gov.uk/media/5a7c7cf7ed915d6969f4538d/hbgm-bw2-assessment-of-income.pdf), DWP tells authorities to make that deduction "as the final step in the earnings calculation" (W2.131), to offset the rest against WTC or CTC "in addition to the existing disregards" (W2.181), and never to offset it against other income (W2.182).

The authority follows the same order under the pension-age and Northern Ireland regulations, and the model applies one calculation to all four:

| Regulations | Additional earnings test | WTC disregard | Childcare deduction |
|---|---|---|---|
| SI 2006/213 (working age) | [Sch 4 para 17](https://www.legislation.gov.uk/uksi/2006/213/schedule/4/paragraph/17) | [Sch 5 para 56](https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/56/2024-04-06) | [reg 27(1)(c)](https://www.legislation.gov.uk/uksi/2006/213/regulation/27/2024-04-06) |
| SI 2006/214 (pension age) | [Sch 4 para 9](https://www.legislation.gov.uk/uksi/2006/214/schedule/4/paragraph/9/2024-04-06) | [Sch 5 para 21](https://www.legislation.gov.uk/uksi/2006/214/schedule/5/paragraph/21/2024-04-06) | [reg 30(1)(c)](https://www.legislation.gov.uk/uksi/2006/214/regulation/30/2024-04-06) |
| SR 2006/405 (NI working age) | [Sch 5 para 17](https://www.legislation.gov.uk/nisr/2006/405/schedule/5/paragraph/17/2024-04-06) | [Sch 6 para 58](https://www.legislation.gov.uk/nisr/2006/405/schedule/6/2024-04-06) | [reg 24(1)(c)](https://www.legislation.gov.uk/nisr/2006/405/regulation/24/2024-04-06) |
| SR 2006/406 (NI pension age) | [Sch 5 para 9](https://www.legislation.gov.uk/nisr/2006/406/schedule/5/paragraph/9/2024-04-06) | [Sch 6 para 22](https://www.legislation.gov.uk/nisr/2006/406/schedule/6/2024-04-06) | [reg 28(1)(c)](https://www.legislation.gov.uk/nisr/2006/406/regulation/28/2024-04-06) |

Under [SI 2020/371 reg 5](https://www.legislation.gov.uk/uksi/2020/371/regulation/5/2021-01-01) and [SR 2020/53 reg 5](https://www.legislation.gov.uk/nisr/2020/53/regulation/5/2024-04-06), which the Secretary of State made, the authority read £37.10 for £17.10 in both disregards, in all four sets of regulations, from 6 April 2020 to the end of 4 April 2021. That period spans the 52 weeks from Monday 6 April 2020 to Sunday 4 April 2021. The model's fiscal year 2020 runs to 5 April 2021, and the model takes the £1,929.20 for those 52 weeks from the dated worker-disregard parameter it already has.

Where a claimant gets only the savings credit of Pension Credit, the authority may modify the Secretary of State's income figure under [SI 2006/214 reg 27(4)](https://www.legislation.gov.uk/uksi/2006/214/regulation/27/2024-04-06) "only in so far as necessary to take into account" the items in its list. The list includes "childcare charges taken into account under regulation 30(1)(c)" and the Schedule 4 paragraph 9(1) additional earnings disregard, but not the Schedule 5 paragraph 21 WTC disregard, and [SR 2006/406 reg 25(4)](https://www.legislation.gov.uk/nisr/2006/406/regulation/25/2024-04-06) has the same list. So on that route the model corrects the childcare charge for the disregarded WTC and deducts no WTC disregard from income.

### Limit, deduction, WTC disregard

The paragraph 17 total includes the childcare deduction, and under regulation 27(2) that deduction draws on the WTC left after the paragraph 56 disregard, so each decision needs the other. The model settles them in one direction:

1. `housing_benefit_childcare_earnings_limit` keeps back the WTC that the authority would disregard under paragraph 56 if childcare took the whole capped charge. It reads neither the deduction nor the `housing_benefit_wtc_disregard` formula.
2. `housing_benefit_applicable_income_childcare_element` takes the lesser of the capped charge and that limit.
3. `housing_benefit_wtc_disregard` and the additional earnings disregard test paragraph 17 with the deduction in force in each segment.

For the deduction the model calculates, the WTC the limit keeps back equals the WTC disregard. Write E for the earnings the standard and accommodation disregards leave, C for the capped charge, a for the additional amount, R for the lesser of WTC and a, and T for CTC. If E ≥ C + a, earnings reach the paragraph 17(1) total with the whole charge and with any smaller deduction, and neither test disregards WTC. Otherwise the limit keeps back R, and the deduction comes to D = min(C, E + (WTC − R) + T). Either D = C, or D < C and D ≥ E; either way E < D + a, so earnings fall short of the paragraph 17(1) total and the model disregards R. Keeping back no WTC would only enlarge the deduction, and earnings would still fall short, so R gives the only consistent answer. `test_limit_keeps_back_the_wtc_disregard_in_force` checks the identity for every generated family, and the segment property checks it within each segment.

A reform can replace the deduction with a formula that reads the limit. With half the capped charge, capped at the limit, the issue's family gets a £2,080 deduction and the additional earnings disregard, and income of £1,030.80. Where the limit binds, the WTC it keeps back stays consistent: with £2,000 of earnings, a £260 standard disregard, a £9,100 charge and £1,000 of WTC, the deduction comes to £1,850.80 and the model disregards £889.20 of WTC.

### Supplied and replaced values

- **WTC disregard.** The limit reads the WTC disregard only where reading it reads no other variable. That covers an amount that a dataset, `set_input` or a scenario supplies, and a disregard that a reform neutralizes. It also covers one that a reform leaves without a formula, an `adds` or `subtracts` aggregate or a `defined_for` mask, which the engine (policyengine-core) uprates or carries over from its own earlier inputs. The model applies each of these to income, to the limit and in every segment, and neutralizing `housing_benefit_wtc_disregard` gives main's calculation back. The engine computes any other replacement through a formula, an aggregate (`adds` or `subtracts`, each a list or the path of a parameter that lists variables) or a mask it calculates first, and any of these could read the deduction. The model applies such a replacement to income and, as an annual amount, in every segment, but the limit keeps back what the whole-charge test gives, so a forwarding replacement leaves the limit's figure unchanged. When a reform only adds a `defined_for` mask and keeps the model's formula, the annual output takes the mask, and each 2026-27 segment applies the same condition to its own WTC disregard (see [Reform conditions in each segment](#reform-conditions-in-each-segment)).
- **Childcare deduction and its limit.** The model applies a deduction that a dataset, `set_input` or a reform supplies or replaces, or the lesser of the capped charge and a supplied or replaced limit, in every segment, and reads it in both paragraph 17 tests. Where the deduction itself comes from a dataset, `set_input` or a reform, only a replacement formula that calls the limit reads it. The limit still keeps back the WTC that the whole-charge test gives, so it can differ from main: with the issue's family and a supplied nil deduction, it comes to £3,900 where main gives £4,000, while the model disregards no WTC.
- **Earnings disregard.** A supplied `housing_benefit_applicable_income_disregard` stays as supplied, and the model still makes the WTC decision's own paragraph 17 test from the earnings inputs, because a supplied total doesn't show whether it includes the additional amount.

### Accommodation segments

From 5 October 2026, the paragraph 17(3)(a) total also includes the paragraph 18 specified or temporary accommodation disregard. The Secretary of State made that change by [SI 2026/753 reg 2(4)](https://www.legislation.gov.uk/uksi/2026/753/regulation/2), and replaced that regulation by [SI 2026/978 reg 2(2)](https://www.legislation.gov.uk/uksi/2026/978/regulation/2) before it came into force on the date in [reg 1](https://www.legislation.gov.uk/uksi/2026/753/regulation/1). The Department for Communities made the same change in Northern Ireland by [SR 2026/157 reg 2(4)(a)](https://www.legislation.gov.uk/nisr/2026/157/regulation/2). Since #2234, the model settles paragraph 17 separately before and after that date and combines the awards by days. The authority's paragraph 56 decision depends on the paragraph 17 outcome, so the model now gives each segment its own WTC disregard and its own limit. `hb_segment_deductions` returns the segment's earnings disregard, WTC disregard and childcare deduction, `housing_benefit_entitlement` deducts all three in each segment, and the annual `housing_benefit_wtc_disregard` combines the segments' amounts by days.

Tax credits ended on 5 April 2025, before the accommodation disregard starts, so no current-law award changes. Take a reform that sets `gov.dwp.tax_credits.active` to true in 2026, and a single claimant aged 40 in specified accommodation. The claimant earns £500, has a £260 standard disregard, £2,000 of WTC and no childcare or other income, pays rent of £10,400 and has a nil applicable amount. Main gives £9,022.21, and the model now gives £9,600.19:

- From 6 April to 4 October 2026 (182 days), earnings of £500 fall short of £260 plus £889.20, so the model disregards £889.20 of WTC. Income comes to £2,500 − £260 − £889.20 = £1,350.80, and the award to £10,400 − 0.65 × £1,350.80 = £9,521.98.
- From 5 October 2026 to 5 April 2027 (183 days), the £4,041.96 accommodation amount takes the £240 of earnings left, and earnings again fall short. Income comes to £2,500 − £500 − £889.20 = £1,110.80, and the award to £9,677.98.
- Combined, (182 × £9,521.98 + 183 × £9,677.98) / 365 = £9,600.19.

The model needs a WTC disregard in each segment. With £2,000 of earnings, the same claimant's earnings cover £260 plus £889.20 before 5 October, so the model gives the additional earnings disregard and no WTC disregard. After 5 October the £1,740 accommodation amount leaves earnings short, and the model disregards £889.20 of WTC. The award comes to £9,114.03. A WTC disregard settled once from the annual accommodation amount would give both disregards before 5 October, and an award of £9,402.23. With £4,000 of earnings it would give neither disregard after 5 October, and £8,176.03 where the model gives £8,465.81.

Deducting the day-weighted annual amount in every segment matches the per-segment award while both segments stay on the taper. Once income after 5 October falls below a £2,000 applicable amount, it gives £10,268.74 instead of £10,124.25. Reading the childcare limit with that annual amount gives £7,394.22 instead of £7,266.74 for a lone parent whose limit binds after 5 October.

### Reform conditions in each segment

policyengine-core masks a variable that has a `defined_for` condition: it keeps the amount where the condition exceeds zero and gives the variable's default elsewhere. The segment helpers rebuild amounts without calling those variables, so `hb_apply_defined_for` applies the same condition to each rebuilt amount. It reads the condition from the variable the simulation holds, calculates it for the year and maps it to the variable's entity. In each 2026-27 segment, and in the single segment of every other year:

| Amount | Where the model applies the condition |
|---|---|
| Accommodation disregard | `hb_accommodation_segments`, to each segment's full amount |
| Earnings disregard | `hb_segment_earnings_disregard` |
| WTC disregard | `hb_segment_wtc_disregard` |
| Childcare limit and deduction | `hb_segment_childcare_deduction`; `hb_fixed_childcare_deduction` applies the deduction's condition where a supplied or replaced limit sets it |
| Applicable income | `housing_benefit_entitlement`, to each segment's income |

A supplied, neutralized or replaced annual amount still applies as supplied in every segment, whatever its condition gives. The limit still keeps back the WTC that the whole-charge test gives, and still subtracts the standard and accommodation disregards, when the WTC or earnings disregard's condition fails; a reform that wants those amounts to fund childcare changes the limit too.

Take a reform that adds `defined_for = "is_couple"` to `housing_benefit_wtc_disregard` and keeps its formula, in 2026 with tax credits kept. A lone parent aged 60, continuing a Housing Benefit award with a child aged 8, works 16 hours a week for £11,000, draws a £10,000 private pension, pays £12,000 for qualifying childcare and £10,400 of London council rent in specified or temporary accommodation, and reports £2,000 of WTC and £4,000 of CTC. From those reports the model calculates WTC of £6,530.35 and CTC of £3,646.49. It attributes £883.14 of the £1,686 of income tax to the wages, so net earnings come to £10,116.86 and income before deductions to £11,000 + £10,000 − £1,686 + £10,176.84 = £29,490.84. The lone parent disregard gives £1,300 (£25 a week, `means_test/income_disregard/lone_parent.yaml`), the childcare cap £9,100 (£175 a week, `working_tax_credit/elements/childcare_1.yaml`), the accommodation disregard £4,041.96 from 5 October (£77.73 a week, `specified_or_temporary_accommodation/lone_parent/older.yaml`), the worker amount £889.20 (£17.10 a week, `income_disregard/worker.yaml`) and the applicable amount £4,968.60 (£95.55 a week, `allowances/lone_parent/older.yaml`). Net earnings fall short of £11,289.20 before 5 October and £15,331.16 after, so the model disregards £889.20 of WTC in each segment, and the condition takes it away:

- From 6 April to 4 October (182 days), income comes to £29,490.84 − £1,300 − £9,100 = £19,090.84, and the award to £10,400 − 0.65 × (£19,090.84 − £4,968.60) = £1,220.54.
- From 5 October (183 days), income comes to £29,490.84 − £5,341.96 − £9,100 = £15,048.88, and the award to £3,847.82.
- Combined, the award comes to £2,537.78, with no benefit cap reduction, and annual income to £17,064.32. Without the condition in each segment, the model paid £3,115.76, 0.65 × £889.20 = £577.98 more, and household net income rose by the same amount. A reform that neutralizes the WTC disregard gives the same award and household net income as the condition. In 2024, with one segment, the condition gives £1,113.09 where the model gives £1,691.07.

A reform that replaces the earnings disregard's formula and adds a condition that reads the WTC disregard, the childcare limit or, in 2026-27, the accommodation disregard meets a circular definition, and policyengine-core raises `CycleError`. The engine calculates the condition first, and with the replacement in place the condition's amount reads the annual earnings disregard the replacement gives, so the condition reads its own result. For a claimant in specified accommodation that covers five cells, three in 2026-27 and two in 2024. The same conditions on the model's own formula run, and a condition that reads its own variable or applicable income raises `CycleError` with or without a replacement.

### Invariants

For every generated population, with assessed earnings, disregards, childcare, credits and work conditions as inputs:

- The WTC disregard lies between zero and the lesser of WTC and the statutory amount. It falls to zero when the work condition fails, when earnings meet paragraph 17(1), and when tax credits stop.
- No benefit unit gets both the additional earnings disregard and a positive WTC disregard within a segment. Annual amounts can both exceed zero: with £2,000 of earnings in specified accommodation, the year has £443.38 of additional earnings disregard and £445.82 of WTC disregard. Exclusivity holds when a dataset or `set_input` supplies the deduction or its limit, and example tests cover a neutralized deduction, one with no formula and replacement formulas, one of which reads the limit.
- A deduction the model calculates never exceeds the capped charge, or the earnings and credits the disregards leave. A supplied deduction stays as supplied: £25,000 stays £25,000 against a £4,160 charge.
- Where the model calculates the deduction, its limit and the WTC disregard, the WTC the limit keeps back equals the WTC disregard, for the year and within each segment.
- Assessed income equals, and never falls below, an independent scalar calculation of the regulations. That calculation settles paragraph 17 by trying both outcomes.
- In 2026-27 the award equals the day-weighted sum of the awards the same calculation gives in each segment, and the annual WTC disregard equals the day-weighted sum of the segments' amounts.
- Where the model disregards no WTC in any segment, every output equals main's, except the limit where a dataset or `set_input` supplies the deduction. Under current law that covers every family.
- In a year with one accommodation amount, supplying the deduction the model calculates changes no result. In 2026-27, where the accommodation disregard changes, the model applies the supplied annual amount in every segment: replaying the lone parent's calculated £4,160 moves the award from £7,266.74 to £7,394.22.
- A reform that replaces the childcare deduction, its limit or the WTC disregard, each alone, with one that forwards the model's own amount meets no circular definition and changes no output. For the WTC disregard, that holds whether the replacement forwards through a formula or through `adds` or `subtracts`, each listing the forwarding variable or naming a parameter that lists it. It holds in 2024 for families with and without WTC, and in 2026-27 with tax credits kept for families outside specified or temporary accommodation. On the example populations, every 2024 output stays identical and every 2026-27 output agrees within £0.001, the float32 rounding of the day weights. The computed replacements of the WTC disregard leave all six outputs of the eight test families unchanged in both years, and the generated populations allow 2p. In specified accommodation the model applies the replacement's annual amount in every segment, as for any replacement: a forwarding replacement of the WTC disregard, through a formula or an aggregate, gives £10,268.74 where the model gives £10,124.25.
- The override check that segments use (`hb_uses_annual_override`) gives main's answer for all 1,200 combinations of the states the engine reads: neutralization, a default or replacement formula, `adds` and `subtracts` (none, empty, a list or a parameter path), a `defined_for` mask, uprating and a supplied input. Where only a `defined_for` mask joins the model's own formula, the check keeps the segment route, and each segment helper applies the condition itself.

Under a reform that gives each of the accommodation, earnings and WTC disregards, the childcare deduction, its limit and applicable income its own condition, which reads none of these amounts, for every generated population with or without tax credits:

- Each annual disregard equals the day-weighted sum of its segment amounts.
- Where a condition fails, its amount takes the variable's default in the annual output and in every segment, unless the population supplies the amount. Where applicable income's condition fails, the award equals the maximum benefit less 65% of the excess of the variable's default over the applicable amount.
- A benefit unit whose conditions all hold keeps the model's outputs, award included.
- The award agrees with an independent scalar calculation within each segment, combined by days.
- In each year but 2026-27, the award the single segment gives equals the award the model builds from annual income.

## Test plan

- [x] For policy changes: the 13 cases in `housing_benefit_wtc_disregard.yaml` and the Python examples take their expected values from the regulations, and their comments work through the arithmetic by hand.
- [x] `make format`: Ruff 0.17.2 formats and checks every changed Python file, `ruff check --select F811,F821 policyengine_uk` passes, and `git diff --check` passes.
- [x] Changelog fragments `changelog.d/2730.fixed.md` and `changelog.d/2777.fixed.md`

The tests build their own households and never load survey data. Rows that name `2f087ae7c` ran at this branch's head, which merges main at `52b30809f`; the other rows name the commit they ran at, and CI runs the rest of the suite.

| Check | Result |
|---|---|
| `test_housing_benefit_wtc_disregard.py`: examples, Hypothesis properties, differential tests and replacement tests | 55 passed at `2f087ae7c` |
| `test_housing_benefit_wtc_disregard_segments.py`: segment examples and properties | 18 passed at `2f087ae7c` |
| `test_housing_benefit_segment_masks.py`: a reform condition on each amount in each segment, #2777's reform, and the couples-only WTC disregard for a lone parent in 2026-27 and 2024 | 30 passed at `2f087ae7c` |
| `housing_benefit_wtc_disregard.yaml` | 13 passed at `2f087ae7c` |
| `test_housing_benefit_accommodation_dates_properties.py` | 6 passed at `2f087ae7c` |
| `test_housing_benefit_childcare_properties.py`, which reads the limit | 5 passed at `2f087ae7c` |
| Housing Benefit YAML directory | 597 passed at `2f087ae7c` |
| `policyengine_uk/tests/code_health` | 3,092 passed at `2f087ae7c` |
| `test_housing_benefit_accommodation_dates.py`, with a 3,600 s cap | 38 passed in 2,945 s at `6dc47664d` |
| The two original test modules and the WTC YAML file on main (`2da212abc`) | 55, 18 and 13 failed; the reproducer deducts £4,000 for childcare against £3,900 |
| The couples-only lone parent at `2f087ae7c` with the five model files back at `1d4a3e475` | 2026-27 fails: the award comes to £3,115.76 against £2,537.78. 2024 passes on the award and fails only where it reads the segment's WTC disregard, £889.20 against nil |

The differential ran main (`2da212abc`) and this branch at `fdc13f128` over the same 15,552 family-years: 1,944 families in each of 2018, 2020, 2021, 2024, 2025, 2026 and 2027, and in 2026 with tax credits kept. Since `fdc13f128` the model code changes which replacements of the WTC disregard the limit reads, and applies reform conditions in each segment; none of these populations replaces the WTC disregard or adds a condition. Of each 1,944, 1,296 families have a calculated deduction, 324 a supplied nil deduction and 324 a supplied £1,000 limit. Earnings, WTC, childcare, the standard disregard, the work condition, pension age and specified accommodation vary; other income comes to £1,000, rent to £10,400 and the applicable amount to £2,000, and regions alternate between London and Northern Ireland. It compares the childcare deduction, earnings disregard, childcare limit, applicable income and award.

| Family-years | Where the model disregards WTC in no segment | Where it disregards WTC in a segment |
|---|---|---|
| Calculated deduction | 8,962: all 44,810 values equal main's exactly | 1,406: every one moves by more than 1p |
| Supplied £1,000 limit | 2,328: all 11,640 values equal main's exactly | 264: every one moves |
| Supplied nil deduction | 2,340: 11,578 of 11,700 values equal main's; the other 122 come from the limit, which keeps back the WTC the whole-charge test gives | 252: every one moves |

None of the 5,832 family-years in 2025, 2026 and 2027 under current law changes.

Of 44 mutations across the changed model files for #2730, 42 make at least one test fail. The other two add a floor or a cap to the paragraph 17 test, which can't change its answer while the additional amount stays positive. Each mutation runs the main module, the segment module and the WTC YAML file in full, with Hypothesis shrinking off. The 15 that touch the override helpers or their call sites ran in full at `6dc47664d`. The other 29 ran at `fdc13f128`, or for the main module and YAML file at `9c2adcde9`, whose model code and tests match `fdc13f128`. Since then the model code changes which replacements of the WTC disregard the limit reads, and applies reform conditions in each segment; the test modules keep every earlier test. For #2777, all 10 mutations of the condition helper and its call sites make a test fail; each ran the main module, the segment module and the segment-mask module in full at `2f087ae7c`, with Hypothesis seed 0.

<details>
<summary>Mutation results</summary>

| File | Mutation | Result |
|---|---|---|
| `housing_benefit_childcare_earnings_limit.py` | The shared paragraph 17 test leaves out the accommodation disregard | Caught: main module 3 failed; segment module 9 failed. (at `fdc13f128`) |
| `housing_benefit_childcare_earnings_limit.py` | `>=` becomes `>` in the paragraph 17 test | Caught: main module 4 failed; WTC YAML 1 failed. (at `fdc13f128`) |
| `housing_benefit_childcare_earnings_limit.py` | The paragraph 17 test leaves out the standard disregard | Caught: main module 13 failed; segment module 1 failed; WTC YAML 4 failed. (at `fdc13f128`) |
| `housing_benefit_childcare_earnings_limit.py` | The paragraph 17 test floors net earnings at zero | Survives, as intended: with a positive additional amount it can't change the answer (at `fdc13f128`) |
| `housing_benefit_childcare_earnings_limit.py` | The paragraph 17 test caps the accommodation amount at the earnings left | Survives, as intended: with a positive additional amount it can't change the answer (at `fdc13f128`) |
| `housing_benefit_childcare_earnings_limit.py` | The WTC decision leaves the accommodation disregard out of the paragraph 17 test | Caught: main module 3 failed; segment module 7 failed. (at `fdc13f128`) |
| `housing_benefit_childcare_earnings_limit.py` | The disregard applies when earnings meet paragraph 17(1) | Caught: main module 18 failed; segment module 8 failed; WTC YAML 3 failed. (at `fdc13f128`) |
| `housing_benefit_childcare_earnings_limit.py` | The disregard ignores the work condition | Caught: main module 2 failed; segment module 2 failed; WTC YAML 1 failed. (at `fdc13f128`) |
| `housing_benefit_childcare_earnings_limit.py` | The disregard takes all WTC, uncapped | Caught: main module 9 failed; segment module 7 failed; WTC YAML 4 failed. (at `fdc13f128`) |
| `housing_benefit_childcare_earnings_limit.py` | A negative WTC award gives a negative disregard | Caught: main module 1 failed. (segment module at `fdc13f128`; main module and YAML at `9c2adcde9`) |
| `housing_benefit_childcare_earnings_limit.py` | The disregard ignores the end of tax credits | Caught: main module 4 failed; segment module 2 failed; WTC YAML 1 failed. (at `fdc13f128`) |
| `housing_benefit_childcare_earnings_limit.py` | The cap uses the weekly amount | Caught: main module 17 failed; segment module 8 failed; WTC YAML 7 failed. (segment module at `fdc13f128`; main module and YAML at `9c2adcde9`) |
| `housing_benefit_childcare_earnings_limit.py` | The limit reads the WTC disregard variable again | Caught: main module 32 failed; segment module 10 failed; WTC YAML 10 failed. The forwarding-reform tests fail. The baseline itself meets a circular definition. (segment module at `fdc13f128`; main module and YAML at `9c2adcde9`) |
| `housing_benefit_childcare_earnings_limit.py` | The WTC kept back tests a supplied or replaced deduction in place of the capped charge | Caught: main module 5 failed; segment module 1 failed. (at `6dc47664d`) |
| `housing_benefit_childcare_earnings_limit.py` | The WTC kept back tests the deduction variable in place of the capped charge | Caught: main module 32 failed; segment module 10 failed; WTC YAML 10 failed. The forwarding-reform tests fail. The baseline itself meets a circular definition. (segment module at `fdc13f128`; main module and YAML at `9c2adcde9`) |
| `housing_benefit_childcare_earnings_limit.py` | The limit ignores a supplied, neutralized or input WTC disregard | Caught: main module 3 failed; segment module 1 failed; WTC YAML 1 failed. (at `6dc47664d`) |
| `housing_benefit_childcare_earnings_limit.py` | The limit reads a replacement WTC disregard formula | Caught: main module 4 failed; segment module 3 failed. (at `6dc47664d`) |
| `housing_benefit_childcare_earnings_limit.py` | A segment's limit keeps back the annual amount, not its own | Caught: segment module 2 failed. (segment module at `fdc13f128`; main module and YAML at `9c2adcde9`) |
| `housing_benefit_childcare_earnings_limit.py` | The annual limit adds the segments' WTC without day weights | Caught: segment module 1 failed. (segment module at `fdc13f128`; main module and YAML at `9c2adcde9`) |
| `housing_benefit_childcare_earnings_limit.py` | Childcare draws on the disregarded WTC | Caught: main module 10 failed; segment module 3 failed; WTC YAML 4 failed. (segment module at `fdc13f128`; main module and YAML at `9c2adcde9`) |
| `housing_benefit_wtc_disregard.py` | The override check ignores a supplied or replaced limit | Caught: main module 3 failed; segment module 2 failed; WTC YAML 1 failed. (at `6dc47664d`) |
| `housing_benefit_wtc_disregard.py` | The override check ignores a supplied or replaced deduction | Caught: main module 9 failed; segment module 2 failed; WTC YAML 1 failed. (at `6dc47664d`) |
| `housing_benefit_wtc_disregard.py` | A supplied deduction reads the capped charge instead | Caught: main module 10 failed; segment module 2 failed; WTC YAML 1 failed. The forwarding-reform tests fail. (segment module at `fdc13f128`; main module and YAML at `9c2adcde9`) |
| `housing_benefit_wtc_disregard.py` | A supplied limit sets the deduction without the charge cap | Caught: main module 3 failed; segment module 2 failed. The forwarding-reform tests fail. (segment module at `fdc13f128`; main module and YAML at `9c2adcde9`) |
| `housing_benefit_wtc_disregard.py` | Each segment's deduction reads the annual limit | Caught: segment module 3 failed. (segment module at `fdc13f128`; main module and YAML at `9c2adcde9`) |
| `housing_benefit_wtc_disregard.py` | Segments discard a supplied annual WTC disregard | Caught: main module 5 failed; segment module 9 failed. (at `6dc47664d`) |
| `housing_benefit_wtc_disregard.py` | Segments ignore a supplied, neutralized or replaced annual WTC disregard | Caught: segment module 9 failed. (at `6dc47664d`) |
| `housing_benefit_wtc_disregard.py` | The annual WTC disregard tests the capped charge, not the deduction in force | Caught: main module 12 failed; segment module 2 failed; WTC YAML 2 failed. The forwarding-reform tests fail. (segment module at `fdc13f128`; main module and YAML at `9c2adcde9`) |
| `housing_benefit_wtc_disregard.py` | The annual amount adds segments without day weights | Caught: segment module 7 failed. The forwarding-reform tests fail. (segment module at `fdc13f128`; main module and YAML at `9c2adcde9`) |
| `housing_benefit_wtc_disregard.py` | The annual amount comes from the annual accommodation amount | Caught: segment module 5 failed. (segment module at `fdc13f128`; main module and YAML at `9c2adcde9`) |
| `housing_benefit_applicable_income_disregard.py` | A segment's WTC disregard tests the capped charge, not the deduction in force | Caught: segment module 2 failed. The forwarding-reform tests fail. (segment module at `fdc13f128`; main module and YAML at `9c2adcde9`) |
| `housing_benefit_applicable_income_disregard.py` | Segments settle the WTC disregard from the annual accommodation amount | Caught: segment module 6 failed. (segment module at `fdc13f128`; main module and YAML at `9c2adcde9`) |
| `housing_benefit_applicable_income_disregard.py` | The earnings disregard tests the capped charge, not the deduction | Caught: main module 9 failed; segment module 1 failed; WTC YAML 2 failed. (segment module at `fdc13f128`; main module and YAML at `9c2adcde9`) |
| `housing_benefit_applicable_income.py` | Annual income leaves out the WTC disregard | Caught: main module 9 failed; segment module 1 failed; WTC YAML 2 failed. (segment module at `fdc13f128`; main module and YAML at `9c2adcde9`) |
| `housing_benefit_entitlement.py` | Segment income leaves out the WTC disregard | Caught: segment module 9 failed. (segment module at `fdc13f128`; main module and YAML at `9c2adcde9`) |
| `housing_benefit_entitlement.py` | Each segment deducts the day-weighted annual WTC disregard | Caught: segment module 2 failed. (segment module at `fdc13f128`; main module and YAML at `9c2adcde9`) |
| `housing_benefit_specified_or_temporary_accommodation_disregard.py` | The override helper runs the formula of a neutralized variable | Caught: main module 2 failed. (at `6dc47664d`) |
| `housing_benefit_specified_or_temporary_accommodation_disregard.py` | The helper lets the limit read an adds or subtracts aggregate | Caught: main module 8 failed; segment module 4 failed. (at `6dc47664d`) |
| `housing_benefit_specified_or_temporary_accommodation_disregard.py` | The helper lets the limit read an adds aggregate | Caught: main module 4 failed; segment module 2 failed. (at `6dc47664d`) |
| `housing_benefit_specified_or_temporary_accommodation_disregard.py` | The helper lets the limit read a subtracts aggregate | Caught: main module 4 failed; segment module 2 failed. (at `6dc47664d`) |
| `housing_benefit_specified_or_temporary_accommodation_disregard.py` | The helper lets the limit read an aggregate named by a parameter path | Caught: main module 4 failed; segment module 2 failed. (at `6dc47664d`) |
| `housing_benefit_specified_or_temporary_accommodation_disregard.py` | The helper lets the limit read a variable with a defined_for mask | Caught: main module 1 failed. (at `6dc47664d`) |
| `housing_benefit_specified_or_temporary_accommodation_disregard.py` | The helper keeps an uprated input from the limit | Caught: main module 1 failed. (at `6dc47664d`) |
| `housing_benefit_specified_or_temporary_accommodation_disregard.py` | Segments recalculate an aggregate replacement instead of applying its annual amount | Caught: segment module 4 failed. (at `6dc47664d`) |
| `housing_benefit_specified_or_temporary_accommodation_disregard.py` | `hb_apply_defined_for` ignores every condition | Caught: segment-mask module 17 failed; main and segment modules pass. (#2777, at `2f087ae7c`) |
| `housing_benefit_specified_or_temporary_accommodation_disregard.py` | A failed condition gives nil instead of the variable's default | Caught: segment-mask module 1 failed; main and segment modules pass. (#2777, at `2f087ae7c`) |
| `housing_benefit_specified_or_temporary_accommodation_disregard.py` | The condition holds wherever it differs from zero, not where it exceeds zero | Caught: segment-mask module 1 failed; main and segment modules pass. (#2777, at `2f087ae7c`) |
| `housing_benefit_specified_or_temporary_accommodation_disregard.py` | Segments leave the accommodation disregard's condition out | Caught: segment-mask module 5 failed; main and segment modules pass. (#2777, at `2f087ae7c`) |
| `housing_benefit_applicable_income_disregard.py` | Segments leave the earnings disregard's condition out | Caught: segment-mask module 7 failed; main and segment modules pass. (#2777, at `2f087ae7c`) |
| `housing_benefit_wtc_disregard.py` | A supplied or replaced limit sets the deduction without the deduction's condition | Caught: segment-mask module 2 failed; main and segment modules pass. (#2777, at `2f087ae7c`) |
| `housing_benefit_wtc_disregard.py` | Segments leave the limit's condition out | Caught: segment-mask module 3 failed; main and segment modules pass. (#2777, at `2f087ae7c`) |
| `housing_benefit_wtc_disregard.py` | Segments leave the childcare deduction's condition out | Caught: segment-mask module 3 failed; main and segment modules pass. (#2777, at `2f087ae7c`) |
| `housing_benefit_wtc_disregard.py` | Segments leave the WTC disregard's condition out | Caught: segment-mask module 9 failed; main and segment modules pass. (#2777, at `2f087ae7c`) |
| `housing_benefit_entitlement.py` | Segment income leaves applicable income's condition out | Caught: segment-mask module 4 failed; main and segment modules pass. (#2777, at `2f087ae7c`) |
</details>

No test on main changes its expected value. #2719, #2700, #2727, #2731, #2735 and #2234 have merged, and this branch includes them. At `2f087ae7c`, `git merge-tree --write-tree --name-only` against each open PR that edits this branch's files gives:

- #1995 (`7381a46c7`) edits `housing_benefit_applicable_income.py`, and Git reports a conflict there: both PRs extend the last sentence of the variable's documentation string. Git reports the same conflict against `1d4a3e475`. Whoever merges second keeps both sentences. #1995 also changes the income tax that Housing Benefit deducts, so once it lands the lone parent's figures above will change.
- #1969, #2007, #2010, #2661, #2807 and #2827 edit `policyengine_uk/utils/supplied_inputs.py`, and Git merges this branch with each of them without a conflict.
- #2265 and #2308 edit `policyengine_uk/utils/supplied_inputs.py` and conflict with main itself; against this branch Git reports the same files as against main, so this branch adds no conflict.

No other open PR edits the override helpers' file, the limit's file, the three test modules or the WTC YAML file. #2301 and #2302 no longer edit `housing_benefit_applicable_income.py`.

## Reproducer

Reproducer: policyengine_uk/tests/test_housing_benefit_wtc_disregard.py::test_issue_2730_reserves_wtc_before_childcare, policyengine_uk/tests/test_housing_benefit_wtc_disregard_segments.py::test_wtc_disregard_applies_before_and_after_5_october, policyengine_uk/tests/test_housing_benefit_segment_masks.py::test_issue_2777_reform_masks_each_segment, policyengine_uk/tests/test_housing_benefit_segment_masks.py::test_a_couples_only_wtc_disregard_reaches_a_lone_parents_award

axiom: TheAxiomFoundation/rulespec-uk#510 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

Brief 826 (replaces 814). Head 2f087ae7c349abcb7651e1b48b099230dd57f1d7 on branch claude/reform-masks-accommodation-a3bc5v. The harness pinned pushes to that claude/ branch. I opened no PR and pushed nothing to fix-2730-hb-wtc-disregard or fix-2777-hb-segment-masks.

Branch history: af4819b14 (the nine #2777 commits on 1d4a3e475) → b8372df8d merges origin/main at 52b30809f → 2f087ae7c adds the review's case.

Writer check

Findings

1. #2777's fix and whether its tests cover C1. git log 1d4a3e475..af4819b14 adds hb_apply_defined_for (housing_benefit_specified_or_temporary_accommodation_disregard.py:18-32). Each segment helper calls it for its own amount:

  • accommodation disregard, :120
  • earnings disregard, housing_benefit_applicable_income_disregard.py:58
  • fixed deduction, limit, deduction and WTC disregard, housing_benefit_wtc_disregard.py:33,46,52,70
  • segment income, housing_benefit_entitlement.py:120

test_housing_benefit_segment_masks.py already covered a mask on the WTC disregard reaching each segment and the award (SINGLE_MASKS "WTC disregard", test_issue_2777_reform_masks_each_segment), so the mechanism behind C1 was covered. It did not cover the review's case: a condition on a core variable (is_couple) for a lone parent with childcare, a pension and model-calculated tax credits, checked through final housing_benefit and household_net_income. I added that case.

CycleError cells at 2f087ae7c. I probed each of the six masked variables as the target, with a condition reading each of the six as the source, comparing a mask on the model's formula against a forwarding replacement formula. The claimant was the specified-accommodation single claimant, in 2026 with tax credits kept and in 2024. Reform-only cells (the mask runs, the replacement raises CycleError):

  • 2026: the earnings disregard replaced, with a condition reading the accommodation disregard, the WTC disregard or the childcare limit. That's 3 cells.
  • 2024: the earnings disregard replaced, with a condition reading the WTC disregard or the limit. That's 2 cells.

That makes five, not the six the brief names. I couldn't find a list of the six in the repo, the commits or the PR/issue comments. Any cell whose condition reads its own variable or applicable income cycles with or without the replacement. The probe is a scratch script, not a test. I added no test for these cells, following the hub's 1 Nov call.

2. Merging main. b8372df8d merges origin/main (52b30809f, 115 commits after 1d4a3e475) with no conflicts. Of main's changes since 1d4a3e475, only policyengine_uk/utils/supplied_inputs.py touches a file this PR changes: main added self_employment_income, miscellaneous_income_trading_allowance_deduction and trading_allowance_deduction to SUPPLIED_INPUT_VARIABLES, and this PR adds one entry. No other main change touches Housing Benefit files. git merge-tree --write-tree --name-only HEAD origin/main → fd676ff73… with no conflicted files.

3. The review's case. test_a_couples_only_wtc_disregard_reaches_a_lone_parents_award (test_housing_benefit_segment_masks.py, end of file) covers 2026 with tax credits kept, plus the 2024 one-segment control. Masked outputs equal a neutralized-disregard control for the WTC disregard, earnings disregard, childcare deduction, applicable income, benefit cap reduction, housing_benefit and household_net_income. The model's own award exceeds the masked one by £577.98, and household net income moves by the same amount.

  • Where my figures differ from the review's. The figures match only when the £2,000 WTC and £4,000 CTC are reported awards (working_tax_credit_reported and child_tax_credit_reported). From those reports the model calculates WTC £6,530.35 and CTC £3,646.49 (2024: £6,079.55 and £4,000).

    • Supplying the awards themselves (working_tax_credit, child_tax_credit and tax_credits) gives annual income of £12,887.48, not £17,064.32.
    • Without tax_credits the model pays no tax credits at all, because is_WTC_eligible needs a reported award.
    • With the reported awards, every review figure reproduces exactly: WTC disregard nil, income £17,064.32, childcare £9,100, cap reduction nil, HB £2,537.78 against £3,115.76.
  • Hand working. Net earnings come to £10,116.86: £11,000 less 11,000/21,000 of the £1,686 of tax. Income before deductions is £29,490.84. The parameter files:

    • lone_parent.yaml: £25/wk → £1,300
    • childcare_1.yaml: £175/wk → £9,100
    • specified_or_temporary_accommodation/lone_parent/older.yaml: £77.73/wk → £4,041.96 from 5 October
    • worker.yaml: £17.10/wk → £889.20
    • allowances/lone_parent/older.yaml: £95.55/wk → £4,968.60
    • withdrawal_rate.yaml: 0.65

    Before 5 October (182 days), income is £19,090.84 and the award £1,220.54. After 5 October (183 days), income is £15,048.88 and the award £3,847.82. Combined, the award comes to £2,537.78. The test comment shows the working, and the PR body has it too.

  • Revert check. In a scratch worktree of 2f087ae7c, I checked out the five Housing Benefit model files at 1d4a3e475. I also added an identity hb_apply_defined_for stub, because the test module imports it.

    • 2026 fails: housing_benefit comes to 3,115.7595 against 2,537.78.
    • 2024 fails only where it reads the segment's WTC disregard (£889.20 against nil). With that one assertion dropped, the 2024 award passes, as the review found.

4. The body. It adds "Also fixes #2777." and replaces the old defined_for limitation sentence. The override-check invariant now says the check keeps the segment route for a mask, and each helper applies the condition. A new section, "Reform conditions in each segment", has the helper table, the worked lone parent and the CycleError cells. It also adds #2777's invariants (M1–M5), the masks-module rows and the revert row in the test plan, and 10 mutation rows labelled (#2777, at 2f087ae7c). I redid the overlap paragraph and added the new test to Reproducer:.

Tests at 2f087ae7c

Check Result
test_housing_benefit_segment_masks.py -n 4 30 passed
test_housing_benefit_wtc_disregard.py -n 4 55 passed
test_housing_benefit_wtc_disregard_segments.py -n 4 18 passed
test_housing_benefit_accommodation_dates_properties.py 6 passed
test_housing_benefit_childcare_properties.py 5 passed
housing_benefit_wtc_disregard.yaml 13 passed
Housing Benefit YAML directory 597 passed
policyengine_uk/tests/code_health -n 4 3,092 passed
check_fix_pr.py on the posted title and body, with this branch's changes and empty labels PASS, 4 changed executable tests
uvx ruff@0.17.2 format --check ., ruff check ., ruff check --select F811,F821 policyengine_uk all pass
git diff --check 1d4a3e475..HEAD clean
git merge-tree --write-tree --name-only HEAD origin/main no conflicts

policyengine_uk/tests/policy/baseline/gov/dwp/housing_benefit doesn't exist. The Housing Benefit YAML directory is policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit, and I ran that instead.

Mutants (#2777, at 2f087ae7c)

Each mutant ran the segment-mask, segment and main modules with -n 4 and Hypothesis seed 0. All 10 were caught, each by the segment-mask module; the other two modules passed every time.

Mutation Segment-mask module
The helper ignores every condition 17 failed
A failed condition gives nil, not the default 1 failed
The condition holds where it is != 0, not > 0 1 failed
No accommodation condition in segments 5 failed
No earnings condition in segments 7 failed
No deduction condition on the fixed-limit path 2 failed
No limit condition 3 failed
No deduction condition 3 failed
No WTC condition 9 failed
No condition on segment income 4 failed

Open-PR overlap at 2f087ae7c

Skipped

Judgment calls

  • I built the review's "supplied" WTC and CTC as reported awards, because that reproduces every review figure and supplied awards don't. I didn't assert absolute household net income, since it depends on unrelated parts of the model. I asserted it against the neutralized control and the £577.98 gap instead.
  • I reported five CycleError cells as measured, not the brief's six.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Thanks for the lone parent case. At 2f087ae7c, each segment helper applies a reform's defined_for condition to the amount it rebuilds, the way policyengine-core masks the annual output. With your couples-only condition, Housing Benefit comes to £2,537.78 in 2026-27, as in your neutralized control and £577.98 below the model's own award, and household net income falls by the same amount. The 2024 one-segment case gives £1,113.09. test_a_couples_only_wtc_disregard_reaches_a_lone_parents_award checks both years through final Housing Benefit and household net income.

We reproduced your figures when we entered the £2,000 of WTC and £4,000 of CTC as reported awards (working_tax_credit_reported and child_tax_credit_reported), from which the model calculates WTC of £6,530.35 and CTC of £3,646.49. A replacement formula without a condition still takes the annual route in every segment. A reform that replaces the earnings disregard's formula and adds a condition that reads the WTC disregard, the limit or the accommodation disregard raises CycleError, because that condition then reads its own result. We list the five cases we found in the description.

@MaxGhenis
MaxGhenis requested a review from juaristi22 November 3, 2026 07:42

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed exact head 2f087ae7c349abcb7651e1b48b099230dd57f1d7. Review COMPLETE, with C1 resolved, no new findings and no material evidence gaps.

The segment helpers now apply the reform's defined_for mask. The coherent reported-award lone-parent regression reaches final Housing Benefit and household net income: the couples-only mask matches neutralisation (£2,537.78 HB in the 2026 tax-credits-active counterfactual), £577.98 below the unrestricted award. The 2024 control also passes. 127 focused checks passed in the matched locked environment; unchanged legal evidence and the current Axiom queue record were checked.

One qualification to my earlier wording: the original diagnostic mixed reported credits with supplied component awards, so it should not be read as certifying a £6,000 aggregate tax-credit award. The coherent reported-award regression independently supports the fix.

The full exact-head Python suite and Aggregate impact are still running, so I am withholding approval pending the applicable required checks. I will check CI again without repeating the completed review if the head remains unchanged.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

3 participants