Repository navigation
Conversation
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>
#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>
Aggregate impact on certified data: passBase Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 0 (thresholds in Notes
Moves flagged for reviewNone. Spending by benefit and service
Caseloads
Standard reform costings (change in gov_balance)
The report suppresses a reform's costing and target change in these years, for the reasons below (
Standard reforms: change in each reform's target
Household net income by decileNo 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)
Counted-once checkEach of 92 components was raised by £1 per unit in 2026-27, and every head of its totals was read household by household: its own head must rise by exactly £1, no other tax head may move, no other benefit may rise, and the model's own total must move by what its heads moved. A head that breaks a rule is measured again twice: with every other head held at its baseline value, and with every other head held at its value after the first £1 while the component rises again. It fails where either still shows the response. 0 failed; 0 were skipped.
Provenance
Dataset: Receipts by tax head: head levels
Spending by benefit and service: head levels
Caseloads: head levels
Government balance, each component counted once: head levels
Model totals: head levels
Household income totals: head levels
Population: head levels
Poverty rates: head levels
People in poverty: head levels
Inequality: head levels
Mean household net income in each side's own deciles: head levels
Standard reform costings (change in gov_balance): head levels
Standard reforms: change in each reform's target: head levels
|
A 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>
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>
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
left a comment
There was a problem hiding this comment.
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.
|
Picking this up for the UK hub. Reply here if you're mid-change and I'll stop. Generated by Claude Code |
|
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
|
Generated by Claude Code |
|
Generated by Claude Code |
|
Ready for hub review Brief 826 (replaces 814). Head Branch history: Writer check
Findings1. #2777's fix and whether its tests cover C1.
CycleError cells at
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. 3. The review's case.
4. The body. It adds "Also fixes #2777." and replaces the old Tests at
|
| 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
- Treat income derived from capital as capital in the legacy means tests #1995 (
7381a46c7) conflicts inhousing_benefit_applicable_income.py: both PRs extend the documentation string's last sentence. It conflicts the same way against1d4a3e475. This is new against the old body, which said Git merges cleanly. Treat income derived from capital as capital in the legacy means tests #1995 also changes the tax that Housing Benefit deducts, so the lone parent's figures will move once it lands. - Value means-tested capital less 10% sale expenses and secured debt #1969, Fix HB and national CTR non-dependant deductions #2007, Give benefit-unit members aged 16 or over a bedroom under LHA rules #2010, Carry unused Income Tax trading losses forward once #2661, Bar income-related ESA at pensionable age and on Pension Credit #2807 and Count UC carer awards for severe disability and Scottish CTR premiums #2827 merge cleanly.
- Give UK simulations a working subsample #2265 and Count only supplied inputs that are still stored in their tier #2308 conflict with main itself, in the same files.
- Add the CTC qualifying young person period after leaving education #2301 and Exclude young Income Support claimants from PC, CB and CTC QYP status #2302 no longer edit this branch's files.
Skipped
test_housing_benefit_accommodation_dates.py(3,600 s cap): the body keeps its6dc47664drow.- The 15,552-family differential: the body keeps its
fdc13f128figures. None of its populations adds a condition. - The Housing Benefit: reserve WTC for the Schedule 5 paragraph 56 disregard before childcare #2730 mutation table: kept at its earlier commits.
- I loaded no survey data.
- Title unchanged.
- Aggregate-report figures are left for the hub.
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
|
Thanks for the lone parent case. At We reproduced your figures when we entered the £2,000 of WTC and £4,000 of CTC as reported awards ( |
juaristi22
left a comment
There was a problem hiding this comment.
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.
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_disregardtakes the lesser of WTC and £889.20 a year (£1,929.20 in 2020-21).housing_benefit_childcare_earnings_limitlets childcare draw only on the WTC left after it, andhousing_benefit_applicable_incomededucts it alongside the earnings disregard. The existinggov.dwp.tax_credits.activeparameter 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 adefined_forcondition 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:
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:
housing_benefit_childcare_earnings_limitkeeps back the WTC that the authority would disregard under paragraph 56 if childcare took the whole capped charge. It reads neither the deduction nor thehousing_benefit_wtc_disregardformula.housing_benefit_applicable_income_childcare_elementtakes the lesser of the capped charge and that limit.housing_benefit_wtc_disregardand 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_forcechecks 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
set_inputor a scenario supplies, and a disregard that a reform neutralizes. It also covers one that a reform leaves without a formula, anaddsorsubtractsaggregate or adefined_formask, 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 neutralizinghousing_benefit_wtc_disregardgives main's calculation back. The engine computes any other replacement through a formula, an aggregate (addsorsubtracts, 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 adefined_formask 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).set_inputor 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_inputor 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.housing_benefit_applicable_income_disregardstays 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_deductionsreturns the segment's earnings disregard, WTC disregard and childcare deduction,housing_benefit_entitlementdeducts all three in each segment, and the annualhousing_benefit_wtc_disregardcombines 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.activeto 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: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_forcondition: 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, sohb_apply_defined_forapplies 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:hb_accommodation_segments, to each segment's full amounthb_segment_earnings_disregardhb_segment_wtc_disregardhb_segment_childcare_deduction;hb_fixed_childcare_deductionapplies the deduction's condition where a supplied or replaced limit sets ithousing_benefit_entitlement, to each segment's incomeThe 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"tohousing_benefit_wtc_disregardand 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: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 raisesCycleErrorwith or without a replacement.Invariants
For every generated population, with assessed earnings, disregards, childcare, credits and work conditions as inputs:
set_inputsupplies 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.set_inputsupplies the deduction. Under current law that covers every family.addsorsubtracts, 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.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,addsandsubtracts(none, empty, a list or a parameter path), adefined_formask, uprating and a supplied input. Where only adefined_formask 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:
Test plan
housing_benefit_wtc_disregard.yamland 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_ukpasses, andgit diff --checkpasses.changelog.d/2730.fixed.mdandchangelog.d/2777.fixed.mdThe tests build their own households and never load survey data. Rows that name
2f087ae7cran at this branch's head, which merges main at52b30809f; the other rows name the commit they ran at, and CI runs the rest of the suite.test_housing_benefit_wtc_disregard.py: examples, Hypothesis properties, differential tests and replacement tests2f087ae7ctest_housing_benefit_wtc_disregard_segments.py: segment examples and properties2f087ae7ctest_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 20242f087ae7chousing_benefit_wtc_disregard.yaml2f087ae7ctest_housing_benefit_accommodation_dates_properties.py2f087ae7ctest_housing_benefit_childcare_properties.py, which reads the limit2f087ae7c2f087ae7cpolicyengine_uk/tests/code_health2f087ae7ctest_housing_benefit_accommodation_dates.py, with a 3,600 s cap6dc47664d2da212abc)2f087ae7cwith the five model files back at1d4a3e475The differential ran main (
2da212abc) and this branch atfdc13f128over 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. Sincefdc13f128the 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.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 atfdc13f128, or for the main module and YAML file at9c2adcde9, whose model code and tests matchfdc13f128. 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 at2f087ae7c, with Hypothesis seed 0.Mutation results
housing_benefit_childcare_earnings_limit.pyfdc13f128)housing_benefit_childcare_earnings_limit.py>=becomes>in the paragraph 17 testfdc13f128)housing_benefit_childcare_earnings_limit.pyfdc13f128)housing_benefit_childcare_earnings_limit.pyfdc13f128)housing_benefit_childcare_earnings_limit.pyfdc13f128)housing_benefit_childcare_earnings_limit.pyfdc13f128)housing_benefit_childcare_earnings_limit.pyfdc13f128)housing_benefit_childcare_earnings_limit.pyfdc13f128)housing_benefit_childcare_earnings_limit.pyfdc13f128)housing_benefit_childcare_earnings_limit.pyfdc13f128; main module and YAML at9c2adcde9)housing_benefit_childcare_earnings_limit.pyfdc13f128)housing_benefit_childcare_earnings_limit.pyfdc13f128; main module and YAML at9c2adcde9)housing_benefit_childcare_earnings_limit.pyfdc13f128; main module and YAML at9c2adcde9)housing_benefit_childcare_earnings_limit.py6dc47664d)housing_benefit_childcare_earnings_limit.pyfdc13f128; main module and YAML at9c2adcde9)housing_benefit_childcare_earnings_limit.py6dc47664d)housing_benefit_childcare_earnings_limit.py6dc47664d)housing_benefit_childcare_earnings_limit.pyfdc13f128; main module and YAML at9c2adcde9)housing_benefit_childcare_earnings_limit.pyfdc13f128; main module and YAML at9c2adcde9)housing_benefit_childcare_earnings_limit.pyfdc13f128; main module and YAML at9c2adcde9)housing_benefit_wtc_disregard.py6dc47664d)housing_benefit_wtc_disregard.py6dc47664d)housing_benefit_wtc_disregard.pyfdc13f128; main module and YAML at9c2adcde9)housing_benefit_wtc_disregard.pyfdc13f128; main module and YAML at9c2adcde9)housing_benefit_wtc_disregard.pyfdc13f128; main module and YAML at9c2adcde9)housing_benefit_wtc_disregard.py6dc47664d)housing_benefit_wtc_disregard.py6dc47664d)housing_benefit_wtc_disregard.pyfdc13f128; main module and YAML at9c2adcde9)housing_benefit_wtc_disregard.pyfdc13f128; main module and YAML at9c2adcde9)housing_benefit_wtc_disregard.pyfdc13f128; main module and YAML at9c2adcde9)housing_benefit_applicable_income_disregard.pyfdc13f128; main module and YAML at9c2adcde9)housing_benefit_applicable_income_disregard.pyfdc13f128; main module and YAML at9c2adcde9)housing_benefit_applicable_income_disregard.pyfdc13f128; main module and YAML at9c2adcde9)housing_benefit_applicable_income.pyfdc13f128; main module and YAML at9c2adcde9)housing_benefit_entitlement.pyfdc13f128; main module and YAML at9c2adcde9)housing_benefit_entitlement.pyfdc13f128; main module and YAML at9c2adcde9)housing_benefit_specified_or_temporary_accommodation_disregard.py6dc47664d)housing_benefit_specified_or_temporary_accommodation_disregard.py6dc47664d)housing_benefit_specified_or_temporary_accommodation_disregard.py6dc47664d)housing_benefit_specified_or_temporary_accommodation_disregard.py6dc47664d)housing_benefit_specified_or_temporary_accommodation_disregard.py6dc47664d)housing_benefit_specified_or_temporary_accommodation_disregard.py6dc47664d)housing_benefit_specified_or_temporary_accommodation_disregard.py6dc47664d)housing_benefit_specified_or_temporary_accommodation_disregard.py6dc47664d)housing_benefit_specified_or_temporary_accommodation_disregard.pyhb_apply_defined_forignores every condition2f087ae7c)housing_benefit_specified_or_temporary_accommodation_disregard.py2f087ae7c)housing_benefit_specified_or_temporary_accommodation_disregard.py2f087ae7c)housing_benefit_specified_or_temporary_accommodation_disregard.py2f087ae7c)housing_benefit_applicable_income_disregard.py2f087ae7c)housing_benefit_wtc_disregard.py2f087ae7c)housing_benefit_wtc_disregard.py2f087ae7c)housing_benefit_wtc_disregard.py2f087ae7c)housing_benefit_wtc_disregard.py2f087ae7c)housing_benefit_entitlement.py2f087ae7c)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-onlyagainst each open PR that edits this branch's files gives:7381a46c7) editshousing_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 against1d4a3e475. Whoever merges second keeps both sentences. Treat income derived from capital as capital in the legacy means tests #1995 also changes the income tax that Housing Benefit deducts, so once it lands the lone parent's figures above will change.policyengine_uk/utils/supplied_inputs.py, and Git merges this branch with each of them without a conflict.policyengine_uk/utils/supplied_inputs.pyand 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