Repository navigation
Conversation
Exclude a person who reports their own Income Support, Housing Benefit, Child Tax Credit or Working Tax Credit, and a claimant or partner on the couple's joint tax credit award, from is_qualifying_young_person_for_universal_credit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6HNNv4UarCy16Lmxzo
Draw reg 5(5) reports rarely and each existing benefit report one time in three, and let claimants and partners be 16 to 19, so the properties reach people whom only reg 28 excludes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6HNNv4UarCy16Lmxzo
Split the UC qualifying young person test: a new reg. 5 variable applies UC Regs reg. 5 with its own reg. 5(5) exclusions (Universal Credit, contributory ESA, contribution-based JSA), and the UC predicate is that variable less SI 2014/1230 reg. 28. The predicate reads a report of Income Support, income-based JSA or a tax credit only in a year the model pays that benefit and no reform neutralises it. Scotland's working-age Council Tax Reduction reads the reg. 5 variable, since SSI 2021/249 reg. 6(1) imports reg. 5 and reg. 28 applies only for the UC Regulations and WRA 2012 Part 1. Tests: closed-benefit cases, the two couple-rule cases, an exact reg. 5 less reg. 28 property, Scottish CTR cases, and the knock-on changes to the reported-awards YAML and the Pension Credit test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6JZXfdURcTxpkfm2gf
Scotland's working-age Council Tax Reduction reads the reg. 5 variable, so the input grid sets that status rather than the UC predicate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6JZXfdURcTxpkfm2gf
The UC qualifying young person predicate now skips a report of income-related ESA or Housing Benefit when a reform neutralises esa_income or housing_benefit, and checks Child Tax Credit and Working Tax Credit each against its own variable or tax_credits, in the couple branch too. New Python tests cover each benefit under a reform that neutralises it. Also: Case 13 pins a Scottish CTR award that the reg 5 reading raises; the qualifying generator draws no reg 5(5) report; review wording rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6HB6mSfknnmvFfWQja
The specified-benefit oracle now counts a young person's income-based JSA report as an own award only in a year the model pays income-based JSA, as the UC predicate does under SI 2014/1230 reg 28. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6HB6mSfknnmvFfWQja
The exact reg 28 property now gives each qualifying member at most one existing benefit report, so reg 28 leaves more of them in the test and the property catches a couple-sum mutant again. The changelog fragment says the predicate reads a report only while the model pays the benefit and no reform neutralises it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6HB6mSfknnmvFfWQja
|
Ready for hub review Verdict: REQUEST_CHANGES at This is the delta review of Before starting I checked #2845 for a What the delta does well.
Why I'm requesting changes. Two P2 items, both about income-related ESA:
There are no P1s. Scripts named below ran in this review's scratch space against detached worktrees, and they aren't in the repository. FindingsP2-a – #2813: the merged tree excludes on income-related ESA claims that pay nothing; the merge note needs the exact changeI made a scratch tree with Each cell gives the young person's predicate /
On the merged tree in 2027, #2813 pays nothing on the report: The predicate can't read Proposed change for whichever of #2845 and #2813 lands second, in - esa_income_paying = paying(True, "esa_income")
+ # The Secretary of State ends old style income-related ESA claims on
+ # the appointed date unless an appointee saving keeps them (SI
+ # 2025/1148 art. 3A; SR 2025/176 art. 3A; esa_income_payable_share).
+ esa_income_paying = paying(True, "esa_income") & (
+ person("esa_income_payable_share", period) > 0
+ )Text to update with it:
The year 2026 keeps the exclusion. The annual model reads any payable share as entitlement for the year, as #2813's own YAML cases, worked by hand from
- name: A young person's own income-related ESA report after the claim stops paying leaves the child element.
# The Secretary of State appointed 1 July 2026 for the abolition of
# income-related ESA (SI 2025/1148 art. 3A(1)), so the claim pays nothing in
# 2027-28 (gov.dwp.ESA.income.awards_payable) and nobody is entitled to it.
period: 2027
absolute_error_margin: 0.01
input:
people:
parent:
age: 45
is_parent: true
is_claimant_or_partner: true
young_person:
age: 17
current_education: POST_SECONDARY
is_claimant_or_partner: false
esa_income_reported: 3_000
benunits:
benunit:
members: [parent, young_person]
households:
household:
members: [parent, young_person]
region: NORTH_EAST
tenure_type: OWNED_OUTRIGHT
output:
esa_income_payable_share: [0, 0]
is_qualifying_young_person_for_universal_credit: [false, true]
universal_credit: 9_688.86
- name: A young person's own income-related ESA report saved for an appointee removes the child element.
# An award saved for an appointee stays payable (SI 2025/1148 art. 3A(2)).
period: 2027
absolute_error_margin: 0.01
input: # as above, with esa_income_appointee_saving: true for young_person
output:
esa_income_payable_share: [0, 1]
is_qualifying_young_person_for_universal_credit: [false, false]
universal_credit: 5_318.52
- name: A young person's own income-related ESA report in the year the claim stops paying removes the child element.
# The claim pays for 86 of the 365 days of 2026-27 in Great Britain.
period: 2026
absolute_error_margin: 0.0001
input: # as the first case
output:
esa_income_payable_share: [0.2356, 0.2356]
is_qualifying_young_person_for_universal_credit: [false, false]
universal_credit: 5_098.80(The second and third cases repeat the first case's inputs in full in the file I ran; I shortened them here.) Runs on the merged tree:
Cycle check, with the change ( The readers were:
Replacement for the body's #2813 merge note:
P2-b – At the head, the predicate excludes on an income-related ESA report that the model has already stopped payingThe body says "the predicate reads a report only in a year the model pays that benefit". The predicate's docstring and both YAML headers say the same. At the head,
In the last three rows the closed benefit excludes nobody, and the model pays no income-related ESA either. The predicate still removes the child element through the income-related ESA report. The same holds on The ruling keeps income-related ESA "as it is until the model has a closure parameter", so the code follows it. But the body and docstring claim is false for these households, so the hub needs to choose:
Housing Benefit needs no change. The model has no year closure for Housing Benefit, at the head or on P3-a – The CTR sentence holds for three of the seven Scotland rowsThe body says: "In these households, the CTR award follows the parent's Universal Credit, which drops by the child element under reg. 28." I reran the table with the parent's UC at #2279 (
Replace the sentence with:
P3-b – A reform that zeroes a benefit's formula leaves every exclusion in place; say soI replaced each benefit's formula with one that returns 0, without neutralising it (
So for all six benefits the predicate reads the report as entitlement, which is the case juaristi22 raised for #2302. The model's other neutralisation readers (
In the body's second paragraph, after the sentence that lists the variables, add: "A reform that zeroes a benefit's formula without neutralising its variable leaves the exclusion in place." P3-c –
|
| Where | Text | Replacement |
|---|---|---|
is_qualifying_young_person_for_universal_credit.py:16 (docstring) |
Reg. 28 turns on entitlement, so the formula reads a report only… | The Secretary of State ties the exclusion to entitlement (reg. 28), so the formula reads a report only… |
test_benefit_cap_specified_benefits_properties.py:286-287 |
Reg. 28 turns on entitlement, so an income-based JSA report counts only in a year the model pays it. | The Secretary of State ties reg. 28's exclusion to entitlement, so the oracle counts an income-based JSA report only in a year the model pays it. |
test_legacy_award_readers_properties.py:690-692 (the previous review's text) |
SI 2014/1230 reg. 28 excludes the member's own income-related ESA or income-based JSA (paying in 2025) from the UC test, and SPC reg. 4A(5) from the Pension Credit test. | The Secretary of State excludes the member's own income-related ESA or income-based JSA (paying in 2025) from the UC test (SI 2014/1230 reg. 28) and from the Pension Credit test (SPC reg. 4A(5)). |
is_qualifying_young_person_under_universal_credit_regulation_5.py:51-55 |
# routes. The formula reads raw breaks the comment off at 30 characters |
Reflow the comment to the 88-column width. |
test_uc_qualifying_young_person_existing_benefits_properties.py:176-179 (docstring) |
Over 10,000 derandomised draws for 2025, the claimant met reg. 5 in 59% of draws, the partner or dependant in 62%, and the dependants in 65% and 69%. | Each slot meets reg. 5 in about three draws in five. (The exact shares depend on how the draws are made; see "Checked and sound".) |
| Body, Merge notes, first bullet | #2300 (93d59f052) … #2813 (3c9890a19) |
Both heads have moved: #2300 to ee0482e75 and #2813 to dc69f3ada. Both merge cleanly with the head, so add or replace the SHAs. |
Previous review (REVIEW-4b4d30df.md) and the author's brief (brief-694-job.md)
| Item | Status at d5d32074d |
Evidence |
|---|---|---|
| P2-a / brief 1 (reg. 5(5) mislabel in #2279's test files) | Closed | The replacement comments sit at test_legacy_award_readers_properties.py:690-692 and test_benefit_cap_specified_benefits_properties.py:34 and :283-287. git grep -n -i "5(5)" -- policyengine_uk, filtered for income-related/income-based/ESA/JSA, gives only the property file :49 (UC, contributory ESA and contribution-based JSA) and the predicate docstring :13 (reg. 5(5) for UC, contributory ESA and contribution-based JSA, then reg. 28's list). Neither puts income-related ESA or income-based JSA under reg. 5(5). own_benefits (:282-294) applies the paying-year filter. Its {year}-01-01 read is right: system.parameters("2026-01-01").gov.dwp.JSA.income.active is False, the same as the model's own read for 2026, because the tree is converted to fiscal years. YEAR = 2025 is kept. |
| P2-b / brief 2 (Case 11 sentence; new case) | Closed | The body uses the corrected paragraph. Case 13 at council_tax_reduction_young_person.yaml:411. By hand from the parameters: IS amount_lone_over_18 £95.55 × 52 = £4,968.60, Child Benefit eldest £27.05 × 52 = £1,406.60, withdrawal rate 0.2, and £1,800 − 0.2 × (£6,406.60 − £4,968.60) = £1,512.40. At #2279, amount_16_24 £75.65 × 52 = £3,933.80 gives £1,800 − 0.2 × £2,472.80 = £1,305.44. Case 13 passes at the head and fails at c5b9850a3. |
| P2-c / brief 3 (#2806 note) | Closed | See "#2806" below. |
| P3-a / brief 4 (neutralisation) | Closed | is_qualifying_young_person_for_universal_credit.py:47-89: paying(active, *names) holds for each of the six benefits, with CTC and WTC each gated by their own name or tax_credits, in the single and the couple branch (:63-65). There are 19 tests in test_uc_qualifying_young_person_neutralised_benefits.py:87, :99, :129, :139 and :153. I reran two of the author's row mutants: dropping esa_income fails only [esa_income] of :87, and checking WTC only against tax_credits fails [working_tax_credit] of :87 and :99. Both killed, matching the hand-off. |
| P3-b / brief 5 (generator) | Closed, wording nit in P3-d | member(..., reg_5_5_reports, one_existing_benefit) at :80-114; qualifying passes reg_5_5_reports=False (:138). My rerun is under "Checked and sound". |
| P3-c / brief 6 | Closed | The table reads "CTR before (#2279) / now", and no dbaf71872 remains in the body. |
| P3-d / brief 7 | Closed | The reg. 5 variable :18-19 names "the Universal Credit Regulations (Northern Ireland) 2016 (SR 2016/216)", and the reference nisr/2016/216/regulation/6 stays (:27). The body quotes reg. 6(5) word for word as the brief's extract gives it. |
| P3-e / brief 8 | Closed | Axiom case 6 rests only on reg. 5's age condition. |
| P3-f / brief 9 | Closed, with the extra rows in P3-d | All eleven rows are applied. |
| Brief 10 (merges) | Closed | See "Merges" below. |
Brief item 1 "keep YEAR = 2025" and the rest of the brief's Tests list |
Closed | Counts below. |
The neutralisation gate
- Core.
TaxBenefitSystem.neutralize_variable(policyengine-core 3.33.12) replaces the variable withget_neutralized_variable(...), a clone withis_neutralized = True(variables/helpers.py:49).Variable.__init__sets it False (variables/variable.py:355).CountryTaxBenefitSystem.neutralize_variablealso neutralises the variable's claimant-or-partner part. - Each simulation builds its own system (
simulation.py:316), and a reform simulation builds its baseline as a separateSimulation(:440-447). Core'sReform.__init__copiesbaseline.variables, andapply_reformrunsreform.apply(self.tax_benefit_system)on the simulation's own system. - Probe results (
scripts/neut.py, one young person per benefit, 2024 unless stated):
| Path | Flags set (of the 7 names) | Predicate (IS, IB-JSA, IR-ESA, HB, CTC, WTC) |
|---|---|---|
Baseline, 2024 to 2030, and sim.baseline |
none in any year | 2024 all false; 2025 CTC/WTC true; 2026+ IS/IB-JSA/CTC/WTC true |
Simulation(reform=Reform subclass) neutralising housing_benefit |
housing_benefit; its baseline branch: none |
HB true only; the baseline branch keeps all six false |
| A fresh baseline after that | none | – |
sim.apply_reform(...) after a calculation, neutralising esa_income |
esa_income |
all false → IR-ESA true |
A tuple (parameter dict, structural Reform neutralising tax_credits) |
tax_credits |
CTC and WTC true |
| Zero-formula reforms | none | see P3-b |
- The parameter-driven structural reforms (
create_structural_reforms_from_parameters) neutralise none of the seven names.measurement_branches.py:132neutralises only on a branch.
#2806 (MERGE_2806.md)
- I merged Apply the Universal Credit education condition #2806's head
4d363920686117ae58ce23fa284918964ab86bbcinto the head (clean,git merge-treetreeab792e8b), then extracted the diff from the posted note (comment 6594856385).git apply --checkpassed and the diff applied cleanly. uc_receiving_education.yaml: 3 failed, 32 passed before; 35 passed after.test_uc_receiving_education_properties.pyafter: 3 passed (149 s).- The four-row probe on the merged tree reproduces the note's table exactly:
| Report | UC predicate | #2806 variable | uc_receiving_education |
UC |
|---|---|---|---|---|
| IS 2026 | true | true | false | £4,062.96 |
| IB-JSA 2026 | true | true | false | £4,062.96 |
| CTC 2025 | true | true | false | £3,803.76 |
| IS 2025 | false | false | false | £3,803.76 |
The body
Voice pass. I diffed PR_BODY.author.md (comment 6594150601, title dropped) against the live body sentence by sentence. Every change rewords. None adds or drops a figure, a provision, a case or a condition, apart from the three additions below. "Each reform lifts exactly the exclusions" became "lifts the exclusions … and no others", and "exactly" in the invariant became two sentences; both keep the meaning.
What the hub added:
- the
axiom:line; - the Merge notes'
git merge-treebullet; - "should" and the link in the Apply the Universal Credit education condition #2806 note;
- the Model the 2026 income-related ESA abolition and its UC claims #2813 note.
Each added or rewritten sentence against the law:
- The Secretary of State made SR 2016/226 regs. 2(1) and 29. True: its introductory text says "The Secretary of State makes the following Regulations…".
- The Secretary of State set out reg. 6 of SR 2016/216. True: "The Secretary of State makes the following Regulations…". Reg. 6(5) is quoted correctly.
- The Department for Communities appointed 1 October 2026 in Northern Ireland (SR 2025/176 art. 3A). True: "The Department for Communities makes the following Order…".
- The Scottish Ministers define a young person by reference to reg. 5 (SSI 2021/249 reg. 6(1)). Matches the quoted text.
- The Secretary of State prescribes incapacity benefit and severe disablement allowance in reg. 25(2) for regs. 22 to 25 only, so they don't count for reg. 28. Matches reg. 25(2) and reg. 2(1) ("but see also … regulation 25(2)").
- "The CTR award follows the parent's Universal Credit, which drops by the child element under reg. 28". False in four of the seven rows (P3-a).
- "The model holds no closure date for income-related ESA until Model the 2026 income-related ESA abolition and its UC claims #2813 lands". True. The claim that the predicate reads the report "only in a year the model pays that benefit" fails at the head for the households in P2-b.
Merge notes' git merge-tree claims. git merge-tree --write-tree --name-only d5d32074d <x> exits 0 with no conflicted paths for every tree:
| Tree | Result |
|---|---|
main 52b30809f |
15d32fbc |
#2300 93d59f052 |
d07bacab |
#2300's current head ee0482e75 |
ce5eac88 |
#2806 4d3639206 |
ab792e8b |
#2813 3c9890a19 |
9ce99e7c |
#2813's current head dc69f3ada |
2bf2eee8 |
#2279 c5b9850a3 |
5db552cb |
Backticked names. Every backticked variable, parameter, file and test name in the body exists at the head, except five that the body attributes to #2806 or #2813:
- Apply the Universal Credit education condition #2806:
is_qualifying_young_person_for_uc_education,uc_receiving_education.yamlandtest_uc_receiving_education_properties.py; - Model the 2026 income-related ESA abolition and its UC claims #2813:
gov.dwp.ESA.income.awards_payableandesa_income_payable_share.
All five exist on those heads. (test_examples_reach_the_cases exists at the head in another file and on #2806.)
axiom: line. axiom: TheAxiomFoundation/rulespec-uk#514 queued has the template's <rulespec-uk issue> queued form (.github/PULL_REQUEST_TEMPLATE.md:17, docs/engineering/skills/github-prs.md:147). I couldn't read #514 (see below).
Fix evidence. .github/check_fix_pr.py from main, run the way pr_fix_evidence.yaml runs it, on the live title and body with no labels:
- changes from the merge base with
main(daec84d5b): PASS, "Changed executable tests: 15"; - changes from
c5b9850a3: PASS, 10.
The Axiom addendum
Figures, from the parameter files at the head. standard_allowance.amount.SINGLE_OLD is £393.45 / £400.14 / £424.90 for 2024-25 / 2025-26 / 2026-27, and elements.child.first.higher_amount is £339.00 / £351.88 for 2025-26 / 2026-27. From those:
- 12 × 400.14 = £4,801.68;
- 12 × 739.14 = £8,869.68;
- 12 × 776.78 = £9,321.36;
- 12 × 424.90 = £5,098.80.
All correct.
Case 2's dates. income_support/active and JSA/income/active turn false on 2026-04-01 (so the benefit ended on 31 March 2026), and tax_credits/active turns false on 2025-04-06 (so it ended on 5 April 2025). They match.
Each case against the law, and the YAML case at the head that gives it:
| Case | Follows from the quoted law? | YAML case at the head |
|---|---|---|
| 1 | Yes: reg. 5(1)(b) then reg. 28(a), with reg. 2(1) listing income support. | "A parent's young person who reports their own Income Support attracts no child element." (£4,801.68). No case pins the £8,869.68 no-report figure in 2025. "…Child Tax Credit report after tax credits stop paying…" gives the same amount. |
| 2 | Yes: no entitlement after the benefit ends, so reg. 28 doesn't apply. | "…Income Support report after Income Support stops paying…" and "…income-based JSA report after income-based JSA stops paying…" (£9,321.36); "…Child Tax Credit report after tax credits stop paying…" (£8,869.68). |
| 3 | Yes: reg. 2(1) lists housing benefit. | "A parent's young person who reports their own Housing Benefit attracts no child element." (£5,098.80). |
| 4 | Yes: a joint claim (TCA s.3(3)(a), (8)) gives both members the entitlement. | "A couple's Working Tax Credit award reported by one partner excludes both 18-year-old partners." The no-award half ([true, true]) has no case of its own. |
| 5 | Yes: a single claim (s.3(3)(b)) gives only the dependant the entitlement. | "A dependant's own Child Tax Credit award leaves an 18-year-old claimant a qualifying young person." |
| 6 | Yes: the partner fails reg. 5(1)'s age condition. | "A partner's own Housing Benefit report leaves an 18-year-old claimant a qualifying young person." |
Checked and sound
All runs: uv sync --frozen --extra dev, HUGGING_FACE_TOKEN and POLICYENGINE_UK_DEFAULT_DATASET unset.
Tests named in the body's Tests section, at the head:
| Set | Result |
|---|---|
uc_qualifying_young_person_existing_benefits.yaml |
17/17 |
qualifying_young_person_reported_own_awards.yaml |
23/23 |
council_tax_reduction_young_person.yaml |
13/13 |
| Readers' YAML | 1,071 passed: child_qualifying_young_person.yaml 8, family/universal_credit/ 574, qualifying_young_person_no_education_route.yaml 14, benefit_cap.yaml 18, benefit_cap/ 116, means_test_income_members.yaml 7, council_tax_reduction/ 334 |
| Seven Python files (below) | 114 passed, 0 failed (1,569 s, -n 4) |
policyengine_uk/tests/code_health |
3,008 passed |
The seven Python files: test_uc_qualifying_young_person_neutralised_benefits.py, test_uc_qualifying_young_person_existing_benefits_properties.py, test_pension_credit_young_person_income_support.py, test_benefit_cap_specified_benefits_properties.py, test_council_tax_reduction_young_person_properties.py, test_council_tax_reduction_severe_disability_family.py and test_legacy_award_readers_properties.py.
Lint:
uvx ruff@0.17.2 format --check .: 2,099 files already formatted.ruff check .: all checks passed.ruff check --select F811,F821 policyengine_uk: all checks passed.git diff --check c5b9850a3..d5d32074d: clean.
Against #2279's head (the head's tests on c5b9850a3's code):
- the three YAML files: 9/17, 5/23 and 3/13 fail;
- 18 of 19 neutralisation tests fail;
- 3 of 5 properties fail (
test_any_own…,test_zero_…,test_the_predicate_is_reg_5_less_reg_28); - the Pension Credit reform test fails.
All match the body.
Households D and G (in the neutralisation tests and my probe):
- G: false / £5,098.80, and true / £9,321.36 under
housing_benefit. - D: false / £4,721.40, and true / £8,721.36 under
child_tax_creditortax_credits.
Generator frequency (scripts/freq.py). I collected 10,000 derandomised qualifying_families draws (Phase.generate only) and ran them in one 2025 simulation. Reg. 5 holds in 59.3%, 61.4%, 65.3% and 66.1% of slots, against the docstring's 59/62/65/69, and the predicate in 31–43%, against the hand-off's 32–44%. The last slot differs by three points, so the exact figures depend on the draw procedure (P3-d).
Code read:
- The predicate reads only person-level reports,
is_claimant_or_partner, the reg. 5 variable, parameters and the system's neutralisation flags. - The
paying(True, …)terms return Python booleans that broadcast correctly.
What I couldn't check
- Encode UC Regs 2013 regs 12-14 and SI 2014/1230 regs 28 and 60: who receives education, and the exceptions, for the WRA 2012 s.4(1)(d) basic condition TheAxiomFoundation/rulespec-uk#514: this session's GitHub access covers only
policyengine-uk. So I couldn't check that Remove y axis title from validation chart #514 is a dispatch-ready issue labelledpe-parity, or that the posted addendum matches it. - Who made SI 2014/1230 and UC Regs 2013: no input quotes their introductory texts, so the body's "the Secretary of State" for regs. 2, 5 and 28 rests on the rulings.
disable_simulated_benefits(P3-c): it needs a dataset, and I have no dataset access.- The cycle check and the Python suites on P2-b's optional
person_legacy_benefits_closedvariant. - Model the 2026 income-related ESA abolition and its UC claims #2813's full test suite on the merged tree: I ran only
esa_income_abolition.yamlandperson_legacy_benefits_closed.yaml. - The full YAML and Python suites, and CI, which doesn't run because the PR's base is Count 16-year-olds outside education as UC and Pension Credit qualifying young persons until the 1 September after their 16th birthday #2279's branch.
Generated by Claude Code
|
Picking this up for the UK hub. Reply here if you're mid-change and I'll stop. Generated by Claude Code |
The model ends a person's own Income Support, income-based JSA and income-related ESA when person_legacy_benefits_closed holds, and the family's Housing Benefit and tax credits when legacy_benefits_closed holds outside the Pension Credit age conditions. Neither closure reads a Universal Credit variable, so the predicate follows both: nobody is entitled under SI 2014/1230 reg. 28 to an award the model has ended. Under disable_simulated_benefits the model pays each benefit as reported after its year gate closes, so the year gates also read that switch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6KY1QwmCKh9DpfvfPk
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6KY1QwmCKh9DpfvfPk
|
Generated by Claude Code |
|
Generated by Claude Code |
|
Generated by Claude Code |
|
Ready for hub review Head: Start check. The last push to Above this comment are the hub documents P2-b: the predicate follows the model's closure (
|
| Benefit | How the model pays it on a person's report | Closure other than the year gate | Reads UC? | What I did |
|---|---|---|---|---|
| Income Support | Family award, on the claimant's and partner's reports only (income_support_eligible.py:15-17, would_claim_IS.py). A dependant's own report is never paid. |
legacy_benefits_closed (income_support.py:18-19) |
No | Gated on person_legacy_benefits_closed (:99-101). This equals legacy_benefits_closed for a claimant or partner. Judgment call: for a dependant the model pays no Income Support in any year, so there is no closure to follow. I applied the model's own closure of that person's legacy awards, as for JSA and ESA. I didn't drop a dependant's Income Support exclusion altogether, since the earlier ruling and Axiom case 1 keep it. |
| income-based JSA | Couple's claim, plus each other member's own claim (jsa_income.py:35-63) |
person_legacy_benefits_closed (jsa_income.py:82-84, jsa_income_eligible.py:43-45) |
No | Gated on it (:102). |
| Housing Benefit | Family award on every member's reports (housing_benefit_eligible.py:35) |
legacy_benefits_closed (:40-41), except for a family on the Pension Credit age route (:26-28, :55). Also claims_universal_credit (:36). |
legacy_benefits_closed: no. claims_universal_credit and left_pension_route_at_legacy_closure (left_pension_route_at_legacy_closure.py:41-45, reads is_uc_entitled): yes. |
Gated on ~(legacy_benefits_closed & ~meets_pension_credit_age_conditions) (:92-97). The UC limbs are left alone: they read the predicate. So a family on the Pension Credit age route keeps the exclusion. A new YAML case pins that. |
| Child Tax Credit | Family award on every member's reports (is_CTC_eligible.py:12-17) |
legacy_benefits_closed and claims_universal_credit (:15-16) |
legacy_benefits_closed: no. claims_universal_credit: yes. |
Gated on ~legacy_benefits_closed (:93, :105-112). |
| Working Tax Credit | Family award (is_WTC_eligible.py:33-35, :78-80) |
legacy_benefits_closed |
No | As for Child Tax Credit. |
Two consequences to check:
- The tax credit closure never bites in the baseline.
legacy_benefits_closedholds only once a benefit has closed, and tax credits close first. Only a parameter reform that keeps tax credits paying reaches it.test_uc_qualifying_young_person_legacy_closure.pycovers that case. - A changed invariant (judgment call). The family closure for Housing Benefit and tax credits means a claimant's or partner's reports can now change a dependant's predicate: a parent's own closed Income Support report lifts the young person's Housing Benefit exclusion. That follows the ruling ("where the model ends payment on a person's report through a closure that reads no Universal Credit variable"). It replaces the old invariant "a claimant's or partner's reports never change a dependant's predicate". The new one says they change it only by closing the family's legacy awards, and then only by lifting an exclusion. I rewrote the property to match. The review read Housing Benefit as having no such closure, because nothing ends it on the dependant's own report. I read the ruling as covering payment on the report whoever's report triggers the closure. If the hub wants only the person's own closure, drop
:92-97and the tax credit gate. The P2-b ESA change stands either way. - A model observation, not a closure. In the issue's household the parent claims Universal Credit, so
claims_universal_creditalready ends the family's Housing Benefit and Child Tax Credit. The model pays £0 of Housing Benefit on household G's report, yet the predicate excludes on it. That is the UC-reading closure the predicate can't follow.
Cases (uc_qualifying_young_person_legacy_closure.yaml, 9 cases). Each comment works the figures from standard_allowance/amount.yaml SINGLE_OLD (£400.14 in 2025-26, £424.90 in 2026-27) and elements/child/first/higher_amount.yaml (£339.00, £351.88). The predicate result is first, then the parent's UC:
- own ESA alone, 2026: false, £5,098.80. This matches the review's figure.
- own ESA + own Income Support, 2026: true, £9,321.36.
- own ESA + own income-based JSA, 2026: true, £9,321.36.
- own ESA + own Child Tax Credit, 2025: true, £8,869.68.
- own Income Support + own Child Tax Credit, 2025: true, £8,869.68.
- own income-based JSA + own Child Tax Credit, 2025: true, £8,869.68.
- own Housing Benefit + parent's own Income Support, 2026: true, £9,321.36.
- own Housing Benefit + parent's own Child Tax Credit, 2025: true, £8,869.68.
- own Housing Benefit, parent aged 70 with own Working Tax Credit, 2025: false, £4,801.68 (the Pension Credit age exception).
At d5d32074d, 7 of the first 8 cases fail; the ESA-alone case passes.
Property. test_a_closed_legacy_award_never_excludes (test_uc_qualifying_young_person_existing_benefits_properties.py:418) draws households in 2025 and 2026 from a new closure_families strategy, whose qualifying members report any benefit one time in three. It clears the ESA report of everyone with person_legacy_benefits_closed, and the Housing Benefit report of everyone in a closed family outside the Pension Credit age conditions, then checks that no predicate changes. reg_28_exclusions now reads the model's closure variables through open_awards (:318). Two judgment calls: the oracle reads the model's closure variables rather than recomputing the proxy, and I renamed the dependant property (:393). The old name isn't in shard_durations.toml.
Mutants (a scratch worktree of e7fa06eb, which has the same predicate as the head). Each ran against both UC YAML files, the property file, the tax credit closure tests and the disable_simulated_benefits test:
| Mutant | What failed |
|---|---|
| Drop the ESA closure term (the brief's mutant) | 3 closure cases, test_a_closed_legacy_award_never_excludes and test_the_predicate_is_reg_5_less_reg_28 |
| Drop the Income Support closure | 1 case |
| Drop the JSA closure | 1 case and the exact-equality property |
| Drop the Housing Benefit closure | 2 cases and both properties |
| Drop the Pension Credit age exception | No test failed at first. I added the last closure case, which now kills it (1 failed). |
| Drop the tax credit family closure | Both tax credit tests |
Drop the disable_simulated_benefits switch |
The reform test in all 3 years |
I restored the code after each mutant.
Step 2: cycle check (cycle.py, rebuilt from the review's description)
The 15 readers the review names were each calculated first, then universal_credit, in a fresh simulation, and compared with a fresh UC. I used North East and Scotland.
At my head: 15 households (A–G with E under the income_support reform, H, the three Step 1 households and four more closure households). Result: 450 orders, 0 errors, 0 mismatches. The parent's UC, the same in both regions:
- A £9,321.36, B £4,801.68, C £8,869.68, D £4,721.40, E £8,869.68, F £9,321.36, G £5,098.80. These match the review.
- H (2027, no Model the 2026 income-related ESA abolition and its UC claims #2813) £5,318.52.
- The Step 1 households £9,321.36, £9,321.36 and £8,869.68.
- The extra households: Income Support + CTC 2025 £8,869.68, JSA + CTC 2025 £8,869.68, HB with parent's Income Support 2026 £9,321.36, HB with parent's CTC 2025 £8,869.68.
On the #2813 merged tree (17 households, adding "H saved" and "H 2026"): 510 orders, 0 errors, 0 mismatches. H £9,688.86, H saved £5,318.52, H 2026 £5,098.80.
P2-a: MERGE_2813.md
The scratch tree merges dc69f3ada into my head, cleanly, and isn't committed to my branch. It adds the review's & (esa_income_payable_share > 0) term and comment at :103, and edits the docstring.
A finding the review didn't have. On the merged tree, #2813's legacy_award_is_closed sets person_legacy_benefits_closed for an owner whose own claim has nothing payable. So Step 1's term alone already makes the review's 2027 unsaved case pass, and all three of the review's cases pass without the payable-share term. To show the term matters, I added a fourth case: a young person who also reports Pension Credit. moves_working_age_awards then leaves their legacy awards open while #2813 has ended the claim. Without the term that case fails (1 failed, 3 passed); with it, 4 passed.
The merged tree also changes one of my cases: esa_income in 2026 becomes £706.85 (3,000 × 86 / 365). The note gives that YAML diff and a header edit.
Merged-tree runs:
- my closure YAML 9/9 with the edit (1 fails without it);
uc_qualifying_young_person_existing_benefits.yaml17/17;qualifying_young_person_reported_own_awards.yaml23/23;council_tax_reduction_young_person.yaml13/13;- Model the 2026 income-related ESA abolition and its UC claims #2813's
esa_income_abolition.yaml41/41 (the review counted 40 at3c9890a19;dc69f3adahas 41); person_legacy_benefits_closed.yaml3/3;- the neutralised-benefits and Pension Credit reform files: 29 passed.
The 2027-28 rates come from the model's uprating on the merged tree: SINGLE_OLD £443.2099 and the first-child higher amount £364.1952, giving £9,688.86 and £5,318.52. Both match the review.
P3
-
P3-a. I reran each row's UC at
c5b9850a3,d5d32074dand my head:Row c5b9850a3d5d32074dand headIS 2025 £8,869.68 £4,801.68 HB 2024 £8,721.36 £4,721.40 CTC 2024 £8,721.36 £4,721.40 IR-ESA and IB-JSA 2025 £4,801.68 £4,801.68 cJSA 2025 £4,801.68 £4,801.68 none 2025 £8,869.68 £8,869.68 These match the review. No row triggers a closure, so the CTR figures hold at my head. I replaced the sentence with the review's text. My own probe household (
region: SCOTLAND,council_tax1,500) doesn't reproduce the table's CTR amounts: it gives £0 where a young person reports a benefit. I didn't rely on those amounts or change them. -
P3-b. I added the sentence to the docstring and to the body's second paragraph.
-
P3-c. I ran the reform with a situation built the way
test_disable_simulated_benefits.pybuilds its datasets (dataset year 2025, reports entered for 2025 only). Under the reform the model pays a dependant's own report after the year gate, at £3,000 in 2025, 2026 and 2027, for income-based JSA, income-related ESA, Housing Benefit, CTC and WTC. It pays nothing on a dependant's own Income Support report, because the reform sets Income Support from the claimant's and partner's reports only (disable_simulated_benefits.py:58), the same as without the reform. The predicate can read the switch as a parameter with no cycle, so the year gates now readactive or disable_simulated_benefits(:59-71). The Income Support gate reads it too, because the reform pays a claimant's or partner's own Income Support after closure. The reform also holds both closure variables false, so the Step 1 gates lift automatically. The new test istest_a_young_person_on_a_benefit_the_reform_pays_is_no_qualifying_young_person(:853); without the reform, it asserts the closed-year flags as before. One limit: the reform sets values for the dataset year and nine years after it, but the parameter stays on beyond that, where the predicate still treats the benefits as paid. The docstring doesn't say this; tell me if you want it there. -
P3-d. I applied all six rows. In the body, the merge notes use
ee0482e75anddc69f3ada, and also namec5b9850a3.
Body, docs and other notes
-
Text I updated: the docstring, the second paragraph, the Law bullet, the headers of
uc_qualifying_young_person_existing_benefits.yamlandqualifying_young_person_reported_own_awards.yaml, and the changelog fragment. The CTR YAML header doesn't make the claim, so I left it. -
Invariants: I added the closure invariant and restated the dependant invariant. The mutation record is labelled
f379377f, and the earlier records are labelledd5d32074d. -
New body table rows: I ran every row at
c5b9850a3and at my head. -
Apply the Universal Credit education condition #2806: the note holds at my head. I merged
4d3639206intof379377fand applied the posted diff (applies cleanly).uc_receiving_education.yaml: 35/35 with the diff, 3 failed and 32 passed without it.test_uc_receiving_education_properties.py: 3/3.- Apply SI 2014/1230 reg 28 to the UC qualifying young person test #2845's YAML files: 17, 9, 23 and 13, all passing.
The diff and its counts don't change. One caveat: the note's oracle filters by paying year and neutralisation but not by the legacy closure. A 2026 draw with a closing report next to an HB or ESA report would disagree with the predicate. These draws didn't reach one. If the hub wants, the oracle can read
person_legacy_benefits_closedthe wayopen_awardsdoes. -
Axiom: the addendum doesn't change. Every new case rests on the model's migration proxy, on a parameter reform or on
disable_simulated_benefits, not on the law quoted in the body alone.
Tests at f379377f (clean runs, uv sync --frozen --extra dev)
-
YAML:
uc_qualifying_young_person_existing_benefits.yaml17/17;uc_qualifying_young_person_legacy_closure.yaml9/9;qualifying_young_person_reported_own_awards.yaml23/23;council_tax_reduction_young_person.yaml13/13.
-
Readers' YAML: 1,080 passed. That breaks down as
child_qualifying_young_person.yaml8,family/universal_credit/583 (574 plus my 9),qualifying_young_person_no_education_route.yaml14,benefit_cap.yaml18,benefit_cap/116,means_test_income_members.yaml7 andcouncil_tax_reduction/334. -
Python (
-n 4, one run per file, none near the 40-minute cap):test_uc_qualifying_young_person_neutralised_benefits.py19;test_uc_qualifying_young_person_existing_benefits_properties.py6;test_uc_qualifying_young_person_legacy_closure.py4 (new);test_pension_credit_young_person_income_support.py10;test_benefit_cap_specified_benefits_properties.py46 (12 min);test_council_tax_reduction_young_person_properties.py6;test_council_tax_reduction_severe_disability_family.py8;test_disable_simulated_benefits.py52;test_legacy_award_readers_properties.py20.
All passed, 171 in total.
-
policyengine_uk/tests/code_health: 3,008 passed. -
Lint:
uvx ruff@0.17.2 format --check .reports 2,100 files already formatted.ruff check .andruff check --select F811,F821 policyengine_ukpass.git diff --check c5b9850a3..HEADis clean. -
check_fix_pr.py(frommain, run the waypr_fix_evidence.yamlruns it, on the posted title and body, no labels): PASS from the merge basedaec84d5b("Changed executable tests: 18") and PASS fromc5b9850a3(13). -
git merge-tree --write-tree --name-only HEAD <tree>: exits 0 with no conflicts formain52b30809f(061a5516), Count 16-year-olds outside education as UC and Pension Credit qualifying young persons until the 1 September after their 16th birthday #2279c5b9850a3(f66fb18e), Place the UC and Pension Credit QYP terminal date from the birth date #2300ee0482e75(b5a3f6a9), Apply the Universal Credit education condition #28064d3639206(2a0e4185) and Model the 2026 income-related ESA abolition and its UC claims #2813dc69f3ada(6538e675).
Skipped
- No test needed a survey dataset.
- I rebuilt the review's witness scripts (
closure.py,cycle.py,ctr.pyfor UC only, and the P2-a probe) from its descriptions; they're in my scratch space, not the repo. - I didn't run Model the 2026 income-related ESA abolition and its UC claims #2813's full suite on the merged tree, only the files the brief lists.
- I didn't run the full CI.
Generated by Claude Code
|
Ready for hub review Verdict: REQUEST_CHANGES at This is the delta review of What holds.
Why I'm requesting changes.
FindingsP1 –
|
| Extra variable the predicate reads | Result |
|---|---|
claims_universal_credit |
10/10 no error, UC equal to the head's |
esa_income_report_closed |
10/10 no error, UC equal |
is_CTC_eligible |
10/10 no error, UC equal |
is_WTC_eligible |
10/10 no error, UC equal |
housing_benefit_eligible |
10/10 CycleError: Circular definition detected on formula universal_credit_pre_benefit_cap… |
left_pension_route_at_legacy_closure |
10/10 the same CycleError |
Which limbs are circular. Only the Pension Credit route's left_pension_route_at_legacy_closure (left_pension_route_at_legacy_closure.py:41-45, through is_uc_entitled) is circular. The predicate can read the claims_universal_credit limbs of esa_income_report_closed.py:45-48 (claimant or partner), housing_benefit_eligible.py:74-79 (continuing_award) and is_CTC_eligible.py:14-17.
What following them would change. Same script, with a reform that also applies three claims_universal_credit limbs:
esa_income_report_closed's, for a claimant or partner;housing_benefit_eligible'scontinuing_award;is_CTC_eligible's.
Every family here claims UC (would_claim_uc defaults to true):
| Household | Head: predicate / parent's UC | Following: predicate / parent's UC |
|---|---|---|
| D, owned outright | false / £4,721.40 | true / £8,721.36 |
| D, renting (£6,000) | false / £10,601.40 | true / £14,601.36 |
| G, owned outright | false / £5,098.80 | true / £9,321.36 |
| G, renting | false / £10,978.80 | true / £15,201.36 |
| B, own IR-ESA, own IB-JSA (either tenure) | unchanged | unchanged |
The flipped rows are the body table's "Child Tax Credit £1,000, 2024 (paying)" and "Housing Benefit £3,000, 2026" rows, and the YAML case "A parent's young person who reports their own Housing Benefit attracts no child element."
This needs a hub ruling. The ruling's exception rests on a cycle that doesn't exist for these three limbs.
- (a) Follow them. The predicate would stop excluding on a young person's own Housing Benefit or Child Tax Credit report in any family that claims Universal Credit. It would also stop excluding on a claimant's or partner's own income-related ESA report once the family claims. That changes body rows and YAML cases (see the run above).
- (b) Keep the exception on a stated, true ground. For example, the ruling's words "reads no Universal Credit variable" could be read as treating
claims_universal_creditas a Universal Credit variable. Under (b), replace the reason everywhere it appears:- Docstring
:30-33:It doesn't follow the limbs that end an award on a Universal Credit claim: claims_universal_credit in esa_income_report_closed (for a claimant or partner), housing_benefit_eligible and is_CTC_eligible, and left_pension_route_at_legacy_closure in housing_benefit_eligible. The last reads is_uc_entitled, which reads this variable through the child element. The others read claims_universal_credit, which doesn't read this variable, and the formula leaves them out because they turn on the family's Universal Credit claim.
- Comment
:76-80:esa_income_report_closed also ends a claimant's or partner's income-related ESA when the family claims Universal Credit (claims_universal_credit). The formula doesn't follow that limb, because it turns on the family's Universal Credit claim.
- Body:
It doesn't follow the limbs that end an award on a Universal Credit claim:
claims_universal_creditinesa_income_report_closed(for a claimant or partner),housing_benefit_eligibleandis_CTC_eligible, andleft_pension_route_at_legacy_closureinhousing_benefit_eligible. Only the last reads the predicate (throughis_uc_entitled). - Case 9 and
MERGE_2813.md: the same correction.
- Docstring
P2-a – The docstring, body, changelog and test headers say the predicate reads a report "only while the model pays that benefit on it"; the body's own reproducer contradicts that
Where: is_qualifying_young_person_for_universal_credit.py:16-18; the body's second paragraph; changelog.d/2804.fixed.md; uc_qualifying_young_person_existing_benefits.yaml:7; qualifying_young_person_reported_own_awards.yaml:6-10; test_uc_qualifying_young_person_existing_benefits_properties.py:8-9.
At d5d32074d these said "only in a year the model pays that benefit". The delta widened the claim to every report. The code follows three things: the year gates, neutralisation and the legacy closures that read no UC variable, as the ruling asks. It applies none of the model's other conditions of payment. So the predicate excludes on reports that the model pays nothing on, including two rows of the body's own table.
me/payment.py: a parent aged 45 (claimant) and a 17-year-old on POST_SECONDARY, North East. Each row ran with would_claim_uc true and false, and the payment is the same both ways:
| Young person's own report | Year | Predicate | Model's payment on it | Parent's UC | Why the model pays nothing |
|---|---|---|---|---|---|
| IS £3,000, owned outright (body row 1, the reproducer) | 2025 | false | income_support £0 |
£4,801.68 | would_claim_IS.py:18-24 and income_support_eligible.py:73-75 read only the claimant's and partner's reports |
| HB £3,000, owned outright (body row 8, household G) | 2026 | false | housing_benefit £0, housing_benefit_eligible false |
£5,098.80 | housing_benefit_eligible.py:80-81, 92-96 needs social or LHA renting |
IB-JSA £3,000, renting, jsa_income_assessable_capital £20,000 |
2025 | false | jsa_income £0 |
£10,681.68 | own claim needs capital within the limit (jsa_income.py:39-43) |
IR-ESA £3,000, renting, esa_income_assessable_capital £20,000 |
2026 | false | esa_income £0 |
£10,978.80 | same (esa_income.py:31-35) |
| Control: IB-JSA £3,000, renting, no capital | 2025 | false | jsa_income £3,000 |
£10,681.68 | – |
| Control: IR-ESA £3,000, renting, no capital | 2026 | false | esa_income £3,000 |
£10,978.80 | – |
An independent search found the same pattern for every benefit (see "The closure terms" below): JSA and ESA hours and tariff income, Housing Benefit's rent and means test, CTC's need for a CTC child or qualifying young person, and WTC hours.
The code follows the ruling. Only the description is wrong. The new test already says so for Income Support (test_disable_simulated_benefits.py:859-862): "the model pays none on a dependant's own report … the predicate reads that report in the years the model pays Income Support to anyone".
Replacements:
- Docstring,
:16-24. Replace from "The Secretary of State ties the exclusion…" through "…or income-related ESA." with:The Secretary of State ties the exclusion to entitlement (reg. 28), so the formula reads a report only in a year the model pays that benefit, only until the model's legacy closure ends the award on it, and only if no reform neutralises it. It doesn't apply the model's other conditions of payment, so a report can exclude a person even where the model pays nothing on that report. For example, the model pays Income Support only on the claimant's and partner's reports, Housing Benefit only to a family that rents, and income-based JSA and income-related ESA only after their capital and work screens. The model pays Income Support, income-based JSA and tax credits only while gov.dwp.income_support.active, gov.dwp.JSA.income.active and gov.dwp.tax_credits.active hold, and has no closure date for Housing Benefit or income-related ESA. The formula also reads those three benefits' reports while gov.contrib.policyengine.disable_simulated_benefits is on. The reform that switch turns on pays them at their reported amounts in the dataset year and the nine years after it, and the formula still reads them in later years.
- Body, second paragraph. Replace its first two sentences with:
The Secretary of State ties reg. 28's exclusion to entitlement, so the predicate reads a report only in a year the model pays that benefit, only until the model's legacy closure ends the award on it, and only if no reform neutralises it. It doesn't apply the model's other conditions of payment, so a report can exclude a young person even where the model pays nothing on it. In the table's household, the model pays no Income Support on the young person's own report, because it pays Income Support only on the claimant's and partner's reports. It pays no Housing Benefit on it either, because the family owns its home. The model pays Income Support, income-based JSA and tax credits only while
gov.dwp.income_support.active,gov.dwp.JSA.income.activeandgov.dwp.tax_credits.activehold, and has no closure date for Housing Benefit or income-related ESA. The predicate also reads those three benefits' reports whilegov.contrib.policyengine.disable_simulated_benefitsis on, the switch for a reform that pays them at their reported amounts in the dataset year and the nine years after it. - Body table. The "(paying)" labels on the Income Support 2025 and Child Tax Credit 2024 rows describe reports the model pays nothing on: CTC is £0 there because the family claims UC. Use "(open)", to pair with "(closed)".
- Changelog. Replace "while the model pays it on that report (its year gate and the model's legacy closure) and no reform neutralises it" with "in a year the model pays that benefit, unless the model's legacy closure has ended the award on that report or a reform neutralises it".
uc_qualifying_young_person_existing_benefits.yaml:7-8. Replace "It reads a report only while the model pays that benefit on it, and only if no reform neutralises it." with "It reads a report only in a year the model pays that benefit, only until the model's legacy closure ends the award on it, and only if no reform neutralises it."qualifying_young_person_reported_own_awards.yaml:6-10. Replace "The UC predicate reads a report only while the model pays that benefit on it (income-based JSA and Income Support until 31 March 2026, and income-related ESA, income-based JSA and Income Support only until the person's legacy awards close, person_legacy_benefits_closed), and only if no reform neutralises it." with "The UC predicate reads a report only in a year the model pays that benefit (income-based JSA and Income Support until 31 March 2026), reads an income-related ESA, income-based JSA or Income Support report only until the person's legacy awards close (person_legacy_benefits_closed), and reads none that a reform neutralises."test_uc_qualifying_young_person_existing_benefits_properties.py:8-9. Replace "It reads a report only while the model pays that benefit on it," with "It reads a report only in a year the model pays that benefit and until the model's legacy closure ends the award on it,".:19-20of the same file. Replace "any positive own report of an existing benefit the model pays on it that year" with "any positive own report of an existing benefit whose year gate and legacy closure leave it open".
P2-b – The body's invariant "A parent's Universal Credit never rises when their young person's own existing benefit report turns positive" no longer holds
Where: the body's Invariants section, and test_uc_qualifying_young_person_existing_benefits_properties.py:28-29.
The closure makes the predicate non-monotone in the young person's own reports. A closing report lifts the exclusion on another report:
- 2026, from the new file.
uc_qualifying_young_person_legacy_closure.yamlcase 1 (own IR-ESA £3,000) gives the parent £5,098.80. Case 2 (the same report plus own IS £3,000) gives £9,321.36. Both pass at the head. - 2025, from
me/also.py. Own IS £3,000 alone gives £4,801.68, and adding own CTC £1,000 gives £8,869.68. Own IB-JSA £3,000 alone gives £4,801.68, and adding own CTC £1,000 gives £8,869.68.
test_a_young_persons_report_never_raises_the_parents_award (:447-484) turns one report positive with every other report at zero, so it tests only the narrower claim. At d5d32074d a report could only add an exclusion; the closure is what lets one report lift another's.
Replacements:
- Body.
A parent's Universal Credit never rises when one existing benefit report of their young person turns positive while the young person reports no other existing benefit. With another report present it can rise, because a closing report lifts the other report's exclusion. In 2026, adding the young person's own Income Support report to their income-related ESA report raises the parent's Universal Credit from £5,098.80 to £9,321.36. In 2025, adding their own Child Tax Credit report to their Income Support or income-based JSA report raises it from £4,801.68 to £8,869.68.
:28-29. "- a parent's Universal Credit never rises when one existing benefit report of their young person turns positive and the young person reports no other;".
P2-c – MERGE_2813.md: a test the note doesn't list fails on the merged tree, and the note's term overrides disable_simulated_benefits
I checked the note on my own detached worktree at the scratch merge 1837b193 (tree 0b5baf36, which equals git merge-tree --write-tree --name-only f379377f… dc69f3ada…).
- Without the note's change,
pytest policyengine_uk/tests/test_disable_simulated_benefits.py -k young_persongives 1 failed, 2 passed. The 2027 case fails at:897:At index 2 diff: True != False. Unreformed in 2027, Model the 2026 income-related ESA abolition and its UC claims #2813 closes the ESA young person's legacy awards, so the predicate is true where the test expects false. - With the note's diff, the same run gives 1 failed, 2 passed, now at
:891([False] * 6, again index 2). The reform pays the young person £3,000 of income-related ESA in 2027 (thepaidassertion just above passes). But the payable-share term reads the claim as unpayable, because the reform doesn't setesa_income_payable_share.
Change for the note. Run on a worktree carrying the note's other changes, the following gives 3 passed for the same command and 4 passed for the note's new abolition YAML. ruff format --check passes on both files.
- & (person("esa_income_payable_share", period) > 0)
+ & ((person("esa_income_payable_share", period) > 0) | paid_as_reported)@@ test_a_young_person_on_a_benefit_the_reform_pays_is_no_qualifying_young_person
legacy_closed,
legacy_closed,
- False,
+ year >= 2027,
False,Add test_disable_simulated_benefits.py to the note's run table.
The note's scope. The note's claim "only the Pension Credit case needs the payable-share term" holds as a mechanism: on the merged tree the term matters only where a Pension Credit report keeps the proxy open (moves_working_age_awards). The report needn't be the young person's own, though. A merged-tree probe found the term changes the predicate, from 2027 only, in each case below:
- a dependant aged 17 or 19 with their own Pension Credit report;
- an 18-year-old single claimant with their own report;
- an 18-year-old claimant whose partner reports Pension Credit;
- a 19-year-old partner of a 70-year-old claimant where the couple reports it, with or without the mixed-age saving.
Only the dependant cases change the parent's UC (by £4,370.34). Suggested wording: "The payable-share term covers a person whose legacy awards the migration proxy leaves open because a Pension Credit report sits in their award (moves_working_age_awards): their own, or for a claimant or partner the couple's."
Everything else the note reports holds. The P3-h list below gives its text fixes.
P3-a – After the reform's ten years, the predicate still reads tax credit reports that the model no longer pays
me/dsb.py, dataset year 2025, the young person's own report in each family, parent's UC in brackets:
| Year | Model pays (IS, JSA, ESA, HB, CTC, WTC) | Predicate, reform on | Predicate, reform off |
|---|---|---|---|
| 2034 | 0, 3,000, 3,000, 3,000, 3,000, 3,000 | all false (UC £0, as reported) | – |
| 2035 | 0, 0, 3,755.98, 0, 0, 0 | T, T, F, F, F, F (£10,926.20, £10,926.20, £6,433.77 ×4) | T, T, F, F, T, T (CTC and WTC rows £10,926.20) |
The hand-off's claim is confirmed for Child Tax Credit and Working Tax Credit. The switch stays on, the reform sets nothing in 2035, the model pays £0, and the predicate still excludes, so the parent loses £4,492.43 in 2035.
It's refuted for Income Support and income-based JSA. Outside its ten years the reform no longer holds person_legacy_benefits_closed false, so the young person's own closure lifts those exclusions, as with the reform off.
ESA is paid, so payment and predicate agree. Housing Benefit pays £0 in this household either way (P2-a).
A switch that turns on after the first year does the same, sooner (me/dsb_late.py). Take the reform dict {"gov.contrib.policyengine.disable_simulated_benefits": {"2026-01-01.2100-12-31": True}} on a situation keyed to 2025. The model decides whether to apply the reform from the switch's value in the simulation's input year (default_input_period, simulation.py:421-426), so here it never applies.
- 2026: the model pays £0 of JSA, CTC and WTC on the young people's reports, yet the predicate excludes the CTC and WTC young people. The parent's UC is £5,098.80 in place of £9,321.36.
- With the switch on from 2025, both are paid at £3,000 and UC is the reported £0.
The documentation should say so:
- Comment
:60-61:The reform pays each listed benefit at its dataset-year report in that year and the nine after it, closure dates included, and holds both legacy closures false in those years. The formula reads the switch, not those years, so in a later year it still reads a tax credit report that the model no longer pays.
- Docstring: P2-a's replacement covers it.
P3-b – Closure cases 7 to 9 assert or explain the wrong thing
me/hbcases.py, same households as the file:
- Cases 7 and 8. These assert
housing_benefit_eligible: false. It's also false without the parent's report, in 2026 and 2025 alike, because the family owns its home and claims UC. So the assertion can't fail on the closure, and the comments' "so the model pays no Housing Benefit on the young person's report (housing_benefit_eligible)" gives the wrong cause.- Drop
housing_benefit_eligiblefrom both outputs. Alternatively, move them to a renting family that doesn't claim UC, where Housing Benefit falls from £6,000 to £0 (family closure table above). - Case 7 comment:
The parent's own Income Support report from 2026 closes the family's legacy awards (legacy_benefits_closed), and the family doesn't meet the Pension Credit age conditions, so the predicate drops the young person's Housing Benefit report. This family owns its home and claims Universal Credit, so the model pays it no Housing Benefit either way. UC: 12 x (424.90 + 351.88) = 9,321.36.
- Case 8: the same, for Child Tax Credit from 2025.
- Drop
- Case 9. Here the family claims UC (£4,801.68) and leaves the Pension Credit route:
left_pension_route_at_legacy_closureis true,housing_benefit_eligiblefalse andhousing_benefit£0. Renting gives the same, with UC £10,681.68. The comment reads as if the model keeps paying. Replacement for:295-301:A parent aged 70 reports their own Working Tax Credit in 2025, after tax credits closed, which closes the family's legacy awards. The family meets the Pension Credit age conditions, so the model keeps its Housing Benefit unless it claims Universal Credit at the closure and is entitled to it (left_pension_route_at_legacy_closure). This family does, so the model pays it no Housing Benefit. The predicate can't read left_pension_route_at_legacy_closure, whose is_uc_entitled reads the predicate through the child element, so it keeps the Housing Benefit report. UC: 12 x 400.14 = 4,801.68.
- Comment
:88-91. This also needs "and is entitled to it (left_pension_route_at_legacy_closure)" after "claiming Universal Credit".
P3-c – The invariant holds against the reports cleared, not report by report
me/pc.py, 2026, the young person reports their own Housing Benefit:
| Parent's own reports | legacy_benefits_closed |
Predicate | UC |
|---|---|---|---|
| none | false | false | £5,098.80 |
| Income Support | true | true | £9,321.36 |
| Income Support and Pension Credit | false | false | £5,098.80 |
The parent's Pension Credit report restores an exclusion that their Income Support report lifted.
Body invariant:
Compared with the same family without the claimant's and partner's reports, those reports change a dependant's predicate only where the proxy closes the family's legacy awards on them, and then only to lift an exclusion. One of their reports can undo another's closure: a parent's own Pension Credit report keeps the family's awards open, so beside the parent's closed Income Support report it keeps the young person's Housing Benefit exclusion.
Make the same change to test_uc_qualifying_young_person_existing_benefits_properties.py:23-24 and to the body's "A dependant's predicate depends on their own reports, and on the claimant's and partner's only where those close the family's legacy awards."
P3-d – A dependant's Income Support: the predicate and is_on_income_support disagree
For a dependant's own Income Support report, the predicate applies the dependant's own closure (:99-101), though the model pays no Income Support on that report in any year. is_on_income_support ignores the closure. The reported-awards header says the Pension Credit young person route reads it (SPC reg. 4A(5)).
me/e3.py, 2025:
- Own Income Support alone:
person_legacy_benefits_closedfalse,is_on_income_supporttrue, predicate false. - Plus own Child Tax Credit: closed true,
is_on_income_supporttrue, predicate true.
Under the reform the predicate also excludes a dependant on their own Income Support report in 2026 and 2027, which the new test pins ([False] * 6). Yet the reform pays Income Support only on the claimant's and partner's reports (CLAIMANT_OR_PARTNER_ONLY).
The hand-off flags the Income Support choice as a judgment call, so it's for the hub to confirm. If the hub keeps it, case 5's comment (:162-164) should say what the code does:
The model pays Income Support in 2025, though never on a dependant's own report. The young person's own Child Tax Credit report closes their legacy awards (person_legacy_benefits_closed), and the predicate then drops their Income Support report too. UC: 12 x (400.14 + 339.00) = 8,869.68.
P3-e – uc_qualifying_young_person_existing_benefits.yaml:13-14: "In these cases no report closes the young person's legacy awards"
Three cases do: own Income Support in 2026, own income-based JSA in 2026 and own Child Tax Credit in 2025 each give person_legacy_benefits_closed: [false, true] (me/e3.py). Replacement:
In these cases a young person's own report closes their legacy awards only where that benefit's year gate has already closed, so the closure decides no case.
P3-f – Tax credits have no Pension Credit age exception, but three texts read as if they did
The docstring :27-29, the body's second paragraph and the property docstring :14-16 say the model ends "the family's Housing Benefit and tax credits when legacy_benefits_closed holds and the family doesn't meet the Pension Credit age conditions".
is_CTC_eligible.py:14-17andis_WTC_eligible.py:46-48apply no age test.- Nor does the predicate (
:93,:105-112). - Only Housing Benefit has one (
:94-97).
Replacement:
…and the family's tax credits when legacy_benefits_closed holds, and its Housing Benefit when legacy_benefits_closed holds and the family doesn't meet the Pension Credit age conditions.
P3-g – Two of the hub's voice edits
Paragraph 3. "If the young person also reports their own Income Support or income-based JSA in 2026, or their own tax credits in 2025, the migration proxy closes their legacy awards. The model then pays no income-related ESA on their report, and that report no longer removes the child element."
- "Also" now attaches to the previous sentence's Housing Benefit and Income Support reports, and "that report" has no ESA antecedent.
- Read that way it's false. Own Housing Benefit plus own Income Support in 2026 closes the young person's awards, but the predicate stays false (£5,098.80,
me/also.py), because the Housing Benefit gate reads only the family's closure. - Replacement:
If the young person reports their own income-related ESA together with their own Income Support or income-based JSA in 2026, or their own tax credits in 2025, the migration proxy closes their legacy awards, the model pays no income-related ESA on their report, and the parent keeps the child element (£9,321.36 in 2026, £8,869.68 in 2025).
Law bullet. "…a modelling choice that no regulation sets" makes a regulation the actor. Replacement: "The predicate follows that proxy, which is a modelling choice, not law."
P3-h – MERGE_2813.md text
-
The Law-bullet quote under "Text to update" quotes the author's sentence ("unless the person's legacy awards close"). The live body now reads "unless the proxy closes the person's legacy awards", so a search for the quoted sentence finds nothing. Quote the live text.
-
The new code comment names the Secretary of State for both orders. The body says the Department for Communities appointed 1 October 2026 in Northern Ireland (SR 2025/176 art. 3A). Replacement:
The Secretary of State (SI 2025/1148 art. 3A) and, in Northern Ireland, the Department for Communities (SR 2025/176 art. 3A) end old style income-related ESA claims on the appointed date unless an appointee saving keeps them (esa_income_payable_share).
-
In the docstring diff, "…has no closure date for Housing Benefit. It reads an income-related ESA report only while…" makes the model the subject of "reads". Replacement:
…has no closure date for Housing Benefit, and pays an income-related ESA claim only while it stays payable (esa_income_payable_share), so the formula reads its report only then.
Use the same words in the note's bullet for the body's second paragraph.
-
The cycle reason: see P1.
P3-i – Two gates have no deterministic test (mutants M3 and M4)
- M3 rests on one Hypothesis draw. M3 makes the family closure read any member's own closure, and only a single draw kills it. Add a YAML case: the body's household in 2026, with the young person's own
income_support_reported: 3_000andhousing_benefit_reported: 3_000. Expectperson_legacy_benefits_closed: [false, true],legacy_benefits_closed: false, predicate[false, false]anduniversal_credit: 5_098.80. M3 gives[false, true]and £9,321.36. - M4 survives everything. M4 drops the family closure from the couple's joint tax credit sum. Add a test to
test_uc_qualifying_young_person_legacy_closure.pyunderTAX_CREDITS_PAYING:- Household: an 18-year-old claimant on
POST_SECONDARY; a partner aged 45 reportingincome_support_reported3,000 andworking_tax_credit_reported1,000; a child aged 1; North East; owned outright; 2026. - Assert:
legacy_benefits_closed[True]and the claimant's predicateTrue. At the head this gives[True, False, False]; M4 gives[False, False, False]. - UC can't show the difference (£11,650.92 either way), so the test has to assert the predicate.
- Household: an 18-year-old claimant on
P3-j – Smaller text items
test_disable_simulated_benefits.py:856-859. "…so the young person stays entitled to it and reg. 28 excludes them from the UC qualifying young person test in every year" makes a regulation the actor and overreaches (P3-a). Replacement:…so the young person stays entitled to it, and the Secretary of State excludes them from the UC qualifying young person test (reg. 28), in each of the reform's years.
uc_qualifying_young_person_legacy_closure.yaml:13. "The closure reports exclude nobody, as the benefit has closed." Replacement:The predicate excludes nobody on those reports, as each benefit's year gate has closed.
test_uc_qualifying_young_person_legacy_closure.py:13-14. "These tests keep tax credits paying in 2026 by a parameter reform". The reform keeps the scheme open, but this household gets no tax credits: withwould_claim_ucleft true, CTC is £0 (me/family.py). Replacement:These tests keep the tax credit scheme open in 2026 by a parameter reform (gov.dwp.tax_credits.active), so the family's closure decides.
test_uc_qualifying_young_person_existing_benefits_properties.py:424-425. "Neither report closes a legacy award, so clearing it leaves the closures as they were." HB and ESA reports do reach the closures, through the mixed-age Pension Credit saving.me/e3.py, 2025: a claimant aged 72 with own HB; a partner aged 60 with own IR-ESA and CTC; the young person with own HB. Clearing only the partner's ESA report creates the saving and reopens the family, and the young person's predicate goes from true to false. The property clears the claimant's HB too, so it doesn't hit this. Replacement:Neither report enters legacy_award_is_closed. They reach the closures only through a mixed-age couple's Pension Credit saving, which clearing both reports of a closed family can't create.
P3-k – The #2806 note: the posted diff no longer applies to #2806's head, and its oracle skips the legacy closure
The diff. #2806 is now at f06a9c63, five commits after 4d3639206. One of them, 55eae0b5, adds a Northern Ireland sentence to the docstring the diff edits. On a merge with the head (clean, 68b757b7), git apply --check of the diff in comment 6594856385 fails: patch failed: …/is_qualifying_young_person_for_uc_education.py:19. The YAML and test hunks apply with offsets. With the docstring hunk resolved by hand, uc_receiving_education.yaml gives 3 failed, 38 passed before and 41 passed after, and the property file gives 5 passed. So the body's "35/35 … 3/3" holds only for 4d3639206.
The oracle. The note's oracle filters reports by year and neutralisation only, so it disagrees with the predicate wherever the closure applies, as the hand-off warned. On the merged and patched tree (agents/m2806/scripts/probe.py, rerun by me), take a single 18-year-old claimant on POST_SECONDARY in 2026, reporting HB £6,000 and WTC £1,000, as a council tenant.
- The model:
uc_receiving_educationtrue andis_uc_eligiblefalse. - The oracle: expects false and true, so
check()fails at:628. - The same happens for a dependant with their own IS and IR-ESA, and for a dependant with their own HB beside a parent's own IS.
- Why 3/3 passes: the 19 derandomised draws include none of these.
Change for the note.
- Give the oracle the predicate's closures, as
open_awardsdoes (test_uc_qualifying_young_person_existing_benefits_properties.py:318):person_legacy_benefits_closedfor IS, IB-JSA and IR-ESA reports;legacy_benefits_closedfor tax credits, including the couple's;legacy_benefits_closedwithmeets_pension_credit_age_conditionsfor HB.
- Base
reference()on the reg. 5 variable, not on the predicate. The predicate already applies reg. 28, which contradictsreference()'s "status before reg. 28". - With both changes the file passed 3/3 at
4d3639206and 5/5 atf06a9c63. Without them, adding the first person as an@examplegives 1 failed, 2 passed.
Body. Replace "the same paying-year and neutralisation filter" with "the predicate's paying-year, neutralisation and legacy-closure filters, on the reg. 5 variable", and quote the counts for the current head.
Checked and sound
Review 822's findings
| Finding | At f379377f |
Evidence |
|---|---|---|
| P2-a (#2813 merge note) | Partly closed | MERGE_2813.md applies cleanly and its nine counts and figures hold. It misses a failing test and the reform conflict (P2-c), and repeats the cycle reason (P1). |
| P2-b (follow the model's closure) | Closed | :84, :103. Closure cases 2 to 4 (the three bold rows) pass at the head (£9,321.36, £9,321.36, £8,869.68) and fail against d5d32074d's code. |
| P3-a (CTR sentence) | Closed | The body has the replacement (the hub split it into two sentences). The parent's UC in each row, at c5b9850a3 and at the head, matches review 822's table, so the sentence is accurate (see "The body"). |
| P3-b (zero-formula reforms) | Closed | Docstring :37-39; body, second paragraph. |
P3-c (disable_simulated_benefits) |
Closed by a code change, with a new P3-a | The year gates read the switch (:62, :68). Test at test_disable_simulated_benefits.py:853 (3 years, passes). Payments as the hand-off reports (see below). |
| P3-d (six writing rows) | Closed | All six rows are applied: :16-17; test_benefit_cap_specified_benefits_properties.py:286-288; test_legacy_award_readers_properties.py:690-692; the reg. 5 comment :51-54; the property docstring :186-188; the body's merge notes use ee0482e75 and dc69f3ada. |
The closure terms, benefit by benefit
Each row sets the model's ending of payment on a person's report against the predicate's gate, apart from the year gate and neutralisation:
| Benefit | The model's closure on a report | UC-reading limb in the model | Predicate (:84-112) |
Same, apart from the UC limbs? |
|---|---|---|---|---|
| Income Support | income_support.py:13-19: nothing once inactive or legacy_benefits_closed; paid only on the claimant's and partner's reports |
none | ~person_legacy_benefits_closed |
claimant or partner: yes (person_legacy_benefits_closed.py:33-37); dependant: never paid (P2-a, P3-d) |
| Income-based JSA | jsa_income.py:60-66 and jsa_income_eligible.py:43-45: reports times ~person_legacy_benefits_closed |
none | ~person_legacy_benefits_closed |
yes |
| Income-related ESA | esa_income.py:56-58 through esa_income_report_closed.py:46-48 |
claimant or partner on a UC claim | ~person_legacy_benefits_closed |
yes |
| Housing Benefit | housing_benefit_eligible.py:64-79: Pension Credit route unless left_pension_route_at_legacy_closure; otherwise any member's report, not on a UC claim and not legacy_benefits_closed |
claims_universal_credit (:74); left_pension_route_at_legacy_closure (via is_uc_entitled) |
~(legacy_benefits_closed & ~meets_pension_credit_age_conditions) |
yes |
| Child Tax Credit | is_CTC_eligible.py:12-17: any member's report, not on a UC claim and not legacy_benefits_closed |
claims_universal_credit |
~legacy_benefits_closed |
yes |
| Working Tax Credit | is_WTC_eligible.py:46-48: any member's report, not legacy_benefits_closed |
none | ~legacy_benefits_closed |
yes |
So the predicate follows every non-UC closure, and the hand-off's table is right on the closures. The disagreements I found all come from conditions of payment that aren't closures (P2-a), from the dependant's Income Support (P3-d), and from the UC-reading limbs (P1).
An independent search (agents/disagree/) covered 52 hand-built households and 2,040 random ones: couples, 18- and 19-year-old claimants and partners, pension-age and mixed-age families, Northern Ireland, Scotland and non-dependent adults, across 2024–2027. Each household ran with and without would_claim_uc and would_claim_uc_at_legacy_closure, giving 52,152 checks. It found no case where the predicate misses a closure it could follow without reading a UC variable. Every disagreement came from a UC-claim limb (P1), from a condition of payment that isn't a closure (P2-a), or from a reform or input that zeroes a benefit upstream of its own variable, which the docstring's zero-formula sentence covers. The P2-a conditions are who may claim Income Support; JSA and ESA hours, capital and tariff income; Housing Benefit's tenure, rent, capital and means test; CTC's need for a CTC child or qualifying young person; and WTC hours.
The family closure
The build's reading follows both the ruling's words and the model's payment.
-
The model. It pays Housing Benefit and tax credits to the family on any member's report:
housing_benefit_eligible.py:73(add(benunit, period, ["housing_benefit_reported"])),is_CTC_eligible.py:12-17andis_WTC_eligible.py:46-48. It ends that payment whenlegacy_benefits_closedholds, which reads only the claimant's and partner's reports and no UC variable. -
The ruling's words. "where the model ends payment on a person's report through a closure that reads no Universal Credit variable" names where the model pays, not whose report closes it. Review 822 looked only at the dependant's own closure.
-
The run (
me/family.py, North East, 2026). A parent aged 45 and a 17-year-old who reports £3,000 of their own Housing Benefit; renting at £6,000;would_claim_ucfalse so the UC limb stays out of the way:Parent's own report legacy_benefits_closedhousing_benefitPredicate (young person) none false £6,000 false Income Support £3,000 (closed in 2026) true £0 true The same holds for CTC under a parameter reform that keeps
gov.dwp.tax_credits.activetrue in 2026: the young person's own £1,000 report gives £4,191.49 of CTC, which falls to £0 once the parent's own Income Support report closes the family's awards.
The invariant. I tested it with my own random search (me/invariant.py). The families are singles and couples, with claimants aged 18 to 75, in all three nations and three tenures, from 2024 to 2027. Each run compares every dependant's predicate with the same family with the claimant's and partner's reports cleared, either all of them or only the existing benefits. Where the family's awards are closed it checks predicate(with) ≥ predicate(cleared); otherwise it checks equality.
- Run 1 (seed 20261103, 400 families, every report kind): 620 dependants, 186 in closed families, 6 lifted, 0 violations.
- Run 2 (seed 7, 600 families, dependants who meet reg. 5): 918 dependants, 283 in closed families, 91 lifted, 0 violations.
It holds against cleared reports, which is how the property tests it, but not report by report (P3-c).
Cycles
Formulas. I read person_legacy_benefits_closed (:25-37), legacy_benefits_closed (:76-89) and meets_pension_credit_age_conditions (:27-61). Between them they read reports, ages, is_claimant_or_partner, is_mixed_age_couple, has_mixed_age_couple_pension_credit_saving, parameters and the input universal_credit_reported. On the #2813 merge they also read esa_income_payable_share.
Transitive static walk (agents/cycles/static_walk.py). It follows every variable name quoted in each module, and in each helper function a formula names, plus adds/subtracts/defined_for:
| Start | Variables reached, head | Variables reached, merge with #2813 |
|---|---|---|
person_legacy_benefits_closed |
25 | 29 |
legacy_benefits_closed |
24 | 28 |
meets_pension_credit_age_conditions |
21 | 21 |
esa_income_payable_share |
– | 15 |
- The only UC-related name reached is the input
universal_credit_reported. No reached variable reaches the predicate. - As a positive control, the same walk flags
claims_universal_creditfromesa_income_report_closed,is_CTC_eligibleandhousing_benefit_eligible. So the walk finds UC variables when they're there.
Traces. sim.trace over 5 households × 2019 and 2024–2027 gave 100 runs at the head and 125 on the merge, with 0 errors. No calculated UC variable appears in any closure variable's trace, and every traced variable lies inside the static closure.
Cycle-order check, my households (agents/cycles/cycle_check.py). The 15 readers review 822 names, North East and Scotland. Each order builds a fresh simulation, calculates the reader, then universal_credit, and compares it with a fresh universal_credit.
- Head: 8 households, 240 orders, 0 errors, 0 mismatches. Largest difference £0.00.
- Merge with the note's predicate diff: 10 households, 300 orders, 0 errors, 0 mismatches.
- Coverage: in R1 and R4, 11 of the 15 readers computed the predicate and all three closure variables.
| Household (YP = the young person; own reports) | Year | YP's predicate | Parent's UC |
|---|---|---|---|
| R1: couple, partner's IB-JSA; children 8, 11, 13; YP 17 HB + CTC; renting | 2026 | true | £17,821.00 |
| R2: parent 50, WTC; YP 18 HB + IR-ESA; renting | 2026 | false (ESA excludes) | £10,978.80 (Scotland £12,298.80) |
| R3: parent 45; YP 16 IB-JSA + IR-ESA, disabled | 2026 | true | £11,298.84 |
| R4: parent 30; YP 17 WTC + IR-ESA; non-dependant aged 25; renting | 2025 | true | £14,353.44 (Scotland £15,753.44) |
| R5: couple, parent's IS; YP 19 HB; council tenant | 2027 | false (fails reg. 5) | £13,348.53 |
| R6: parent 70, WTC; YP 17 HB | 2026 | false (Pension Credit age) | £5,098.80 |
| R7: two 18-year-olds, one with CTC; child aged 1; renting | 2026 | both true; only the child counted | £15,987.36 |
| R8: YP 17 IS + HB, nothing closed | 2024 | false | £10,601.40 (Scotland £11,721.40) |
| R9 (merge): YP 17 IR-ESA unsaved + Pension Credit | 2027 | true | £9,688.86 |
| R10 (merge): parent's IS; YP 17 IR-ESA saved | 2027 | false | £5,318.52 |
R1 to R8 are identical at the head and on the merge.
disable_simulated_benefits
- Coverage. The parameter has one value,
2000-01-01: false, and no end date. At the head the predicate runs from 2015 to at least 2100 (me/years.py). It fails in 2014 and earlier ongov.dwp.pension_credit.mixed_age_couples.saving_latest_birth_date, which the new closure reads reach throughmeets_pension_credit_age_conditions. In 1999 and earlier it fails on the contrib switch. - Was anything lost? At
d5d32074dthe predicate ran from 2004, but every reader I tried already failed there in 2010 and 2014 (lha_renter_has_non_dependant,housing_benefit,is_uc_benefit_cap_exempt_specified_benefit,household_net_income,child_benefit;me/early.py). So nothing that ran before stops running. - Payments (
me/dsb.py, built as the test file builds its families: dataset year 2025, reports entered for 2025 only, other inputs for every year). Under the reform, the young person's own report of IS / IB-JSA / IR-ESA / HB / CTC / WTC is paid £0 / £3,000 / £3,000 / £3,000 / £3,000 / £3,000 in 2025, 2026, 2027 and 2034. The predicate excludes all six. This matches the hand-off. - Within the ten years the reform also sets
universal_creditfromuniversal_credit_reported(BENEFITSincludes it). So the parent's UC is the reported £0 whatever the predicate says. - After the ten years (P3-a). Confirmed for tax credits, refuted for IS and JSA.
- A parameter dict instead of the
Scenario(reform={"gov.contrib.policyengine.disable_simulated_benefits": {...: True}}): the reform still applies. CTC is £1,000 in 2026, UC £0, predicate excludes.
Tests (clean runs at f379377f, uv sync --frozen --extra dev, HUGGING_FACE_TOKEN and POLICYENGINE_UK_DEFAULT_DATASET unset)
| File | Result |
|---|---|
uc_qualifying_young_person_legacy_closure.yaml |
9 passed |
uc_qualifying_young_person_existing_benefits.yaml |
17 passed |
qualifying_young_person_reported_own_awards.yaml |
23 passed |
council_tax_reduction_young_person.yaml |
13 passed |
test_uc_qualifying_young_person_existing_benefits_properties.py (-n 4) |
6 passed (53 s) |
test_uc_qualifying_young_person_legacy_closure.py (-n 4) |
4 passed (25 s) |
test_disable_simulated_benefits.py (-n 4) |
52 passed (158 s) |
test_uc_qualifying_young_person_neutralised_benefits.py (-n 4) |
19 passed (66 s) |
- The property file at a stronger setting. I ran a scratch copy with
max_examples=150,derandomize=Falseand--hypothesis-seed=20261103: 6 passed (303 s). - The head's YAML against
d5d32074d's code. The closure file gives 7 failed, 2 passed. Cases 2 to 8 fail; case 1 (own IR-ESA alone) and case 9 (Pension Credit age) pass. The body's "7 of the first 8 … only the case with the income-related ESA report alone passes" holds. The other three files pass (17, 23, 13). - Against
c5b9850a3: 9/17, 5/23 and 3/13 fail, as the body says, and the closure file gives 5 failed, 4 passed. - The predicate file is byte-identical at
e7fa06ebdandf379377f9(git diff e7fa06ebd f379377f9 -- <predicate>is empty).f379377fonly adds closure case 9.
Mutants
Setup: agents/mutants/, my own worktree at f379377f, with the file restored and git status clean after each run.
The test set is T1 existing_benefits.yaml, T2 the closure YAML, T3 the property file, T4 test_uc_qualifying_young_person_legacy_closure.py, T5 test_disable_simulated_benefits.py -k young_person and T6 the neutralisation file. Unmutated, it gives 17, 9, 6, 4, 3 and 19 passed.
| Mutant | Result |
|---|---|
M1 (author's): housing_benefit_open = ~family_closed |
Killed. T2 fails 1 (case 9: [0. 1.] differs from [0. 0.]), as the hand-off says. |
M2 (author's): drop & family_award_open from both tax credit terms |
Killed. T4 fails 2 (test_the_parents_closed_legacy_award_ends_the_young_persons_tax_credits[child_tax_credit_reported] and [working_tax_credit_reported]), as reported. |
M3 (new): family_closed = person.benunit.any(person("person_legacy_benefits_closed", period)) |
Killed by one Hypothesis draw only, in test_any_own_existing_benefit_report_excludes_the_person. Every deterministic test passes. |
| M4 (new): drop the family closure from the couple's joint tax credit sum only | Survived all six. |
P3-i gives a household for each, and the test that would catch it.
MERGE_2813.md (apart from P2-c and P3-h)
Applying it. On a detached worktree at the scratch merge 1837b193 (tree 0b5baf36), both diffs pass git apply --check with no offsets. The post-image blobs are the note's 607e7ca6, 7c88488c and 820bb2e8, and the patched predicate passes ruff format --check and ruff check.
Counts. Every count matches the note:
- New abolition YAML: 4 passed; with only the payable-share term removed, 1 failed (the Pension Credit case) and 3 passed.
- Closure YAML: 9 passed; without the
esa_income: 706.85edit, 1 failed (esa_income@2026: [706.84937] differs from 3000.0). existing_benefits,reported_own_awardsand CTR young person: 17, 23 and 13.- Model the 2026 income-related ESA abolition and its UC claims #2813's
esa_income_abolition.yamlandperson_legacy_benefits_closed.yaml: 41 and 3. - The neutralisation and Pension Credit reform files (
-n 2): 29 passed.
Figures, from the merged tree's parameters at the model's instant for each year:
SINGLE_OLD: 424.90 in 2026 and 443.2098657 in 2027.- First-child higher amount: 364.1952186 in 2027.
esa_income_payable_sharein 2026: 0.2356164 (86/365) in Great Britain and 0.4876712 (178/365) in Northern Ireland.- 3,000 × 86/365 = 706.849…, which gives £706.85.
- 12 × (443.2099 + 364.1952) = £9,688.86 and 12 × 443.2099 = £5,318.52. Full precision gives the same pennies.
The finding. "On the merged tree, the model closes the legacy awards of a person whose own income-related ESA claim has nothing payable" holds for every 16- to 19-year-old except where a Pension Credit report sits in their award unit (moves_working_age_awards). That's why only the Pension Credit cases need the term (scope in P2-c). A dependant working 20 hours shows the same P2-a gap on the merged tree as at the head: the model pays nothing on their report in 2026, and both versions exclude them.
Other checks.
esa_income_payable_sharereads onlycountry,esa_income_reported,esa_income_appointee_savingandis_claimant_or_partner(esa_income_payable_share.py:44-64). Its trace adds onlyregion.- The note's households H, H saved, H 2026 and the Pension Credit case give £9,688.86, £5,318.52, £5,098.80 and £9,688.86. Their 15-reader order check gave 120 orders, 0 errors and 0 mismatches. My own merged-tree check of 300 orders is under Cycles.
The body
The hub's voice edits. I diffed PR_BODY.author.md, as posted in comment 6600889841, with the live body (updated 10:20:25Z), sentence by sentence. I count 13 changed sentence groups, or 14 if the Tests bullet's restructure counts twice, plus a trailing newline.
- None changes a figure, case, provision or condition.
- Two change meaning or voice: paragraph 3's "also" and the Law bullet's "no regulation sets" (P3-g).
- The Model the 2026 income-related ESA abolition and its UC claims #2813 edit ("a person whose own income-related ESA claim has nothing payable") is as broad as the author's "an unsaved claimant's", and the next sentence qualifies both.
The main table. I reran all 16 rows at the head and at c5b9850a3 (me/table.py; neutralising reforms built with Reform.neutralize_variable). Every predicate and every UC figure matches both columns to the penny, including the six new closure rows:
| Row | c5b9850a3 |
Head |
|---|---|---|
| IR-ESA + IS, 2026 | false / £5,098.80 | true / £9,321.36 |
| IR-ESA + IB-JSA, 2026 | false / £5,098.80 | true / £9,321.36 |
| IR-ESA + CTC, 2025 | false / £4,801.68 | true / £8,869.68 |
| IS + CTC, 2025 | true / £8,869.68 | true / £8,869.68 |
| IB-JSA + CTC, 2025 | false / £4,801.68 | true / £8,869.68 |
| HB, 2026, parent's own IS | true / £9,321.36 | true / £9,321.36 |
The paragraph's figures (£5,098.80 against £9,321.36 in 2026; £4,801.68 against £8,869.68 in 2025) match too.
The Scotland CTR table. Every flag matches. The parent's UC per row is:
- IS 2025: £8,869.68 at Count 16-year-olds outside education as UC and Pension Credit qualifying young persons until the 1 September after their 16th birthday #2279, £4,801.68 now.
- HB 2024 and CTC 2024: £8,721.36 at Count 16-year-olds outside education as UC and Pension Credit qualifying young persons until the 1 September after their 16th birthday #2279, £4,721.40 now.
- The other four rows: unchanged.
So the replacement sentence is accurate.
The CTR amounts reproduce only with would_claim_council_tax_reduction: true on the benefit unit. Without it, would_claim_council_tax_reduction is false in the five rows where the young person reports an existing benefit, and CTR is £0 (me/ctr2.py). That's probably why the hand-off's household gave £0. With it, all seven rows match at both commits:
- IS: £412.46 / £1,226.06;
- HB and CTC: £430.69 / £1,230.68;
- IR-ESA, IB-JSA and contribution-based JSA: £1,226.06 / £1,226.06;
- none: £412.46 / £412.46.
A second, independent rerun (agents/body/) got the same 16 rows and the same CTR amounts. It also confirms:
- the sentence on
would_claim_ucfalse: £1,500 in six rows, £1,374.96 in the CTC row; - Case 13: £1,305.44 under Count 16-year-olds outside education as UC and Pension Credit qualifying young persons until the 1 September after their 16th birthday #2279, £1,512.40 now;
- the nine Python files the body names collect 171 tests under
pytest --collect-only(19/6/4/10/46/6/8/52/20).
Backticked names (me/names.py). Every variable, parameter, file, directory and test name in the body exists at the head. The five names the body gives to #2806 (is_qualifying_young_person_for_uc_education, uc_receiving_education.yaml, test_uc_receiving_education_properties.py) and #2813 (gov.dwp.ESA.income.awards_payable, esa_income_payable_share) exist on 4d3639206 and dc69f3ada.
The #2806 note at 4d3639206 holds as written.
- The merge with the head is clean (
13c2a78e). - Before the diff,
uc_receiving_education.yamlgives 3 failed, 32 passed (cases 31, 33 and 34). - The diff (comment 6594856385, 227 lines, extracted independently twice) applies cleanly.
- After it: 35 passed,
test_uc_receiving_education_properties.py(-n 2) gives 3 passed, and this PR's four YAML files give 17, 9, 23 and 13.
For #2806's new head, see P3-k.
Merges, lint and fix evidence
git merge-tree --write-tree --name-only f379377f… <tree>exits 0 with one line (no conflicted paths) for:main52b30809f(75396971);- Count 16-year-olds outside education as UC and Pension Credit qualifying young persons until the 1 September after their 16th birthday #2279
c5b9850a3(0983d0a4); - Place the UC and Pension Credit QYP terminal date from the birth date #2300
ee0482e75(ecb64c4a); - Apply the Universal Credit education condition #2806
4d3639206(13c2a78e); - Model the 2026 income-related ESA abolition and its UC claims #2813
dc69f3ada(0b5baf36).
- Heads that moved during this review.
mainis now1d4d020aa(Disregard WTC before the Housing Benefit childcare deduction #2776), which merges cleanly (c98b4de2); on that scratch merge the PR's four YAML files pass (9, 17, 23, 13). Apply the Universal Credit education condition #2806 is nowf06a9c63, which also merges cleanly (68b757b7). - The hand-off's tree OIDs (
061a5516,f66fb18e,b5a3f6a9,2a0e4185,6538e675) are the trees ate7fa06ebd, not atf379377f, so that run predates the last commit. The conclusion still holds at the head. Atd5d32074dI get review 822's OIDs. - Lint:
uvx ruff@0.17.2 format --check .: 2,100 files already formatted.ruff check .: all checks passed (the project's ruff is 0.17.2).ruff check --select F811,F821 policyengine_uk: all checks passed.git diff --check c5b9850a3..HEAD: clean.
.github/check_fix_pr.py(the same atmainand the head), run aspr_fix_evidence.yamlruns it on the live title and body, with no labels:- from
c5b9850a3: PASS, "Changed executable tests: 13"; - from the merge base
daec84d5b: PASS, 18.
- from
What I couldn't check
- Python files outside the brief's list, and the readers' YAML. I didn't rerun
test_pension_credit_young_person_income_support.py(10),test_benefit_cap_specified_benefits_properties.py(46),test_council_tax_reduction_young_person_properties.py(6),test_council_tax_reduction_severe_disability_family.py(8),test_legacy_award_readers_properties.py(20), the readers' YAML (1,080) orcode_health(3,008). The body's counts for them rest on the hand-off. On the Model the 2026 income-related ESA abolition and its UC claims #2813 merge, the two Python files the note names gave 29 passed. - CI and the full suites. CI doesn't run while the base is Count 16-year-olds outside education as UC and Pension Credit qualifying young persons until the 1 September after their 16th birthday #2279's branch, and I ran neither the full YAML nor the full Python suite.
- Encode UC Regs 2013 regs 12-14 and SI 2014/1230 regs 28 and 60: who receives education, and the exceptions, for the WRA 2012 s.4(1)(d) basic condition TheAxiomFoundation/rulespec-uk#514. This session's GitHub access covers only
policyengine-uk. P1's ruling could change the parity cases if the hub follows the UC-claim limbs. - Law. I used only law quoted in the brief and the body.
legislation.gov.ukanswered HTTP 200 here, but I didn't use it. One sentence that predates this delta, the Scotland section's "only for the purposes of the UC Regulations and WRA 2012 Part 1 (reg. 28(a) and (b))", gives limb (b) a purpose limit that the Law bullet's paraphrase doesn't state. No input quotes reg. 28(b) in full, so I haven't ruled on it. - Legal entitlement. Whether, in law, a parent's Universal Credit claim ends a dependant's own Housing Benefit or tax credit entitlement isn't something the quoted law settles. P1 leaves that to the hub.
Generated by Claude Code
|
Picking this up for the UK hub. Reply here if you're mid-change and I'll stop. Generated by Claude Code |
…the UC qualifying young person test A claim for Universal Credit ends the claimant's and partner's own awards (SI 2014/1230 reg. 8(2) and (2A); SI 2013/983 art. 4), not a dependant's. The predicate now reads esa_income_report_closed for income-related ESA, and for a claimant or partner applies housing_benefit_eligible's and is_CTC_eligible's claims_universal_credit terms to Housing Benefit and Child Tax Credit reports. Under disable_simulated_benefits it leaves the claim limbs out, as that reform pays each report claim or none. Moves closure cases 7 and 8 to a renting family that claims no Universal Credit, adds the claim-limb cases, the couple's joint tax credit closure test, the dependant Housing Benefit closure case, two claim invariants and the reform tests, and corrects the predicate's stated scope. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6JkWi1uaASqaobKahB
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6JkWi1uaASqaobKahB
…ner who claims Universal Credit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6JkWi1uaASqaobKahB
|
Picking this up for the UK hub. Reply here if you're mid-change and I'll stop. Generated by Claude Code |
…eport The reform behind disable_simulated_benefits pays a claimant's or partner's own income-related ESA, Housing Benefit and Child Tax Credit report as reported only in its ten years. From then on the model ends those awards on the family's Universal Credit claim again, while the formula, which reads the switch, still leaves the claim limbs out. The docstring and comments now say so, and cite SI 2014/1230 reg. 8 and SI 2013/983 art. 4. A new case pins that the predicate follows no claim limb for Working Tax Credit: the model keeps a lone parent's award beside a Universal Credit claim in 2024, and their own report still excludes them. The reform test now also checks that the model pays nothing on each claimant's report without the reform. Three YAML headers name the model, not the claim, as the actor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H3DAwvqayXkSWRJ17kcEaE
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H3DAwvqayXkSWRJ17kcEaE
…or reg. 60(1) #2806 merged first, so this PR carries the change the composition note gives (comment 6605782269 on #2806), applied unchanged. is_qualifying_young_person_for_uc_education returns is_qualifying_young_person_for_universal_credit, and a new is_qualifying_young_person_on_uc_migration_day applies reg. 28 to every reported existing benefit, the award a closure claim ended, for the reg. 60(1) protection in uc_is_excepted_from_education_condition. The note's YAML and property-test changes come with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H3DAwvqayXkSWRJ17kcEaE
The composition makes is_qualifying_young_person_for_uc_education return the UC predicate. With the claim limbs, that predicate gives true for an 18-year-old who claims Universal Credit while reporting their own Housing Benefit, so the model would treat them as a qualifying young person receiving education (UC Regs 2013 reg. 12(1A)) and deny them Universal Credit (#2806's case 31). The Secretary of State excludes a person entitled to an existing benefit (SI 2014/1230 reg. 28) and a person receiving Universal Credit (UC Regs 2013 reg. 5(5)), and the model's reg. 5 test reads only reported Universal Credit. So the education status now reads reg. 28 against the awards a claimant or partner held when they claimed: the predicate's formula moves into qualifying_young_person, whose claim_limbs switch the education status turns off. #2806's own assertions for cases 31 and 34 and for people 9 and 10 of test_examples_reach_the_cases come back: those claimants claim Universal Credit, which ends their own Housing Benefit, so the predicate gives true as #2806 asserted. The changelog names the education condition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H3DAwvqayXkSWRJ17kcEaE
…against the closures An independent review of 4545062 found three things, which this commit acts on. - A claim ends a tax credit award the day before the Universal Credit award starts (SI 2014/1230 reg. 8(2)(a)), but Housing Benefit only two weeks after (reg. 8(2A)), and esa_income_report_closed describes the same run-on for income-related ESA. So qualifying_young_person's switch is now at_claim_start: it leaves out the Housing Benefit and income-related ESA claim limbs and keeps the Child Tax Credit one, as #2806's case 13 reads reg. 5(5) for a claimant with no existing award. - is_qualifying_young_person_for_uc_education reads the predicate variable again and removes only those the two run-on limbs readmit, so an input or reform that sets the predicate reaches the education condition too. - The composition's property oracle read every paying report, without the model's legacy closures, and no Child Tax Credit claim limb. It now reads the reg. 5 variable, the closures and the family's claim (excluded_on_first_day), with two more examples: a claimant whose family's legacy awards closed before the claim, and a Child Tax Credit claimant with no child. The comments on cases 31 and 34 and on people 9 and 10 say that the predicate reads reg. 5(5) only from reported Universal Credit. The documentation names the nil-award limit, and the changelog the first day. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H3DAwvqayXkSWRJ17kcEaE
|
Picking this up for the UK hub. Reply here if you're mid-change and I'll stop. Generated by Claude Code |
SI 2018/65 inserted SI 2014/1230 reg. 8(2A) from 11 April 2018, and SI 2019/1152 reg. 5 reaches claims made from 22 July 2020. The education status reads the first day of the award in every year, so its documentation now says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6HmavpLcsnXLwhKe5k
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6HmavpLcsnXLwhKe5k
|
Generated by Claude Code |
|
Generated by Claude Code |
|
Generated by Claude Code |
|
Ready for hub review Head: Start checks. At 05:51Z the two The eight commits above
|
| Commit | Items it covers | Holds? |
|---|---|---|
ad19029f6 |
P1 formula (three claim limbs for a claimant or partner; dependant untouched; no WTC/IS/IB-JSA limb; no read of housing_benefit_eligible or left_pension_route_at_legacy_closure), the reform's paid_as_reported bypass, docstring and comments (P1, P2-a, P3-a, P3-f), P3-b (cases 7 and 8 moved to a renting family without a claim, case 9 comment), P3-d case 5 comment, P3-e, P3-i (M3 YAML case, M4 test), P3-j, item 1's cases, item 6's two Hypothesis properties, P2-b's property line |
Holds. I read the formula against esa_income_report_closed.py, housing_benefit_eligible.py (continuing_award) and is_CTC_eligible.py, and worked each new case by hand (ESA with a claim 12 × 338.58 = £4,062.96; HB with a claim 4,062.96 + 6,000 = £10,062.96; lone parent CTC with a claim 12 × (311.68 + 287.92) = £7,195.20; couple 12 × (617.60 + 287.92) = £10,866.24; CTC without a claim the £3,455 child element; HB without a claim £6,000, the full rent). |
cdd497084 |
Sets would_claim_uc_at_legacy_closure false in the no-claim cases, so claims_uc_at_legacy_closure can't bring a claim back |
Holds: claims_universal_credit = would_claim_uc | claims_uc_at_legacy_closure. |
7b9e55695 |
Mixed-age couple on the Pension Credit route: the 18-year-old partner's Housing Benefit keeps excluding them though the couple claims | Holds, and it kills the "HB limb at Pension Credit age" mutant. |
a25cc40d0 |
Says what the reform pays on a claim; adds test_a_claimant_on_a_benefit_the_reform_pays_beside_universal_credit_is_no_qualifying_young_person (item 1's reform cases); pins the WTC-beside-a-claim case; P3-j's test wording |
Holds. The reform sets esa_income, housing_benefit and child_tax_credit from the dataset-year report in its ten years (disable_simulated_benefits.py), whatever the claim, so the predicate leaves the limbs out while the switch is on. After the ten years the model ends those awards on the claim again, and the predicate still leaves the limbs out; the docstring says so, as P3-a's text does for the year gates. |
4ee6644b5 |
Merge of main at 95dfc62f8 |
--remerge-diff lists no file. |
c8e31269a |
Restart step 2: COMPOSITION.md's diff |
Holds: its tree equals the note's diff applied to 4ee6644b5. |
45450629f, cc191bb52 |
Move the formula into qualifying_young_person(..., at_claim_start=False); the education status removes those the HB and IR-ESA claim limbs readmit; oracle excluded_on_first_day and two CLAIM_EXAMPLES |
Holds as a proposal; see next section. |
The at_claim_start proposal
- Against the law. Reg. 8(2)(a) ends a tax credit award "on the day before the first date" of entitlement to UC, but reg. 8(2A) ends Housing Benefit only on "the last day of the period of two weeks" beginning the day after that day, and SI 2019/1152 reg. 5 does the same for income-related ESA on claims from 22 July 2020. So on the first day of the award a claimant still has the HB or ESA award (reg. 28 excludes them, reg. 12(2) decides education), but no longer the tax credit (reg. 28 doesn't). After the run-on, reg. 5(5) takes a person "receiving universal credit" out of the definition. The note's one-line formula would deny such a claimant UC through reg. 12(1A) (case 31: predicate true → receiving education), which the law doesn't support on day one. So I kept the proposal.
- Case 31 (own HB, claims): predicate true, education status false, not receiving education, eligible. Matches the law above.
- Case 33 (joint WTC, 2024): no claim limb for WTC; false/false, holds.
- Case 34 (case 31 with maintenance funding, claims at the closure): status false, receives education under 12(2)(b), reg. 60(1) excepts. Holds.
- People 9 and 10 of
test_examples_reach_the_cases: predicate true, status false, as case 31. - M (case 42): WTC, no WTC limb, so status true; migration-day status false; excepted under reg. 60(1); £4,062.96. Holds.
- W: same flags, £5,112.96. In law W met the condition on the migration day, so reg. 60(1) doesn't reach W (as the note says). Reg. 5(5), quoted in the brief: "A person who is receiving universal credit, an employment and support allowance or a jobseeker's allowance is not a qualifying young person." Once the tax credit award has ended, W counts as a qualifying young person only while W receives no UC. While W receives it, reg. 12(2) decides and W doesn't receive education, which gives the model's £5,112.96 by another route. Judgment call / open: the condition and 5(5) read each other; a reading where W never starts receiving UC (so stays a qualifying young person and receives education under 12(1A)) is also self-consistent, and gives £0. The quoted law doesn't choose between them. I left the formula alone.
- The move changes nothing else. (a) Structurally: with
at_claim_start=False,(paid_as_reported or at_claim_start)reduces topaid_as_reportedandclaim_ends_own_award & (not at_claim_start)toclaim_ends_own_award, and the rest of the function matchesc8e31269a's formula line for line (diffed). (b) Empirically: I ran one script (random households of 1 to 4 people, 40 benefit units per draw, singles and couples aged 16 to 72 as claimant or partner, dependants aged 1 to 25, each of the seven reports with probability 1/4, randomuniversal_credit_reported,would_claim_uc,would_claim_uc_at_legacy_closure, region and tenure, inputs set in every year 2019 to 2027) in a worktree atc8e31269aand at the head, for 6 seeds × 2019, 2024, 2025, 2026, 2027 × with and withoutdisable_simulated_benefitson from 2000: 60 simulations, 5,910 predicate values, 428 of them true, 0 differences. (c) The YAML and property suites at the head give the same predicate expectations as atc8e31269a(no predicate output changed in45450629forcc191bb52, apart from case 31/34 and people 9 and 10, whichad19029f6's limbs set and45450629frecords). - Dates.
426c72fcadds tois_qualifying_young_person_for_uc_education's documentation: SI 2018/65 inserted reg. 8(2A) from 11 April 2018, SI 2019/1152 reg. 5 reaches claims from 22 July 2020, and the annual model applies both run-ons in earlier years too. The body says the same. No date gate. - I didn't rely on the review
cc191bb52's message cites.
Readers of the predicate
git grep -n is_qualifying_young_person_for_universal_credit -- policyengine_uk ':!policyengine_uk/tests':
is_child_or_qualifying_young_person_for_universal_credit, and through ituc_child_index,uc_child_limit_*,uc_is_child_*,uc_individual_(severely_)disabled_child_element,is_responsible_for_child_or_qualifying_young_person_for_universal_credit: each masks out the claimant (~is_uc_claimantoris_uc_assessed_claimant), so none reads a claimant's or partner's predicate.lha_renter_has_non_dependant: reads it only for members who aren't the claimant or partner.is_uc_benefit_cap_exempt_specified_benefit(:111): masks with~couple(is_uc_assessed_claimant).is_qualifying_young_person_for_uc_educationandis_qualifying_young_person_on_uc_migration_day: read it for the claimant or partner (the education condition and reg. 60(1)).
Existing cases whose result the claim limbs change (I ran f379377f9's YAML, and main's uc_receiving_education.yaml at 4ee6644b5, against the head):
uc_qualifying_young_person_existing_benefits.yaml: "An 18-year-old claimant … who reports Housing Benefit" (predicate true on the claim), "A young person's own Child Tax Credit or Working Tax Credit report excludes them" (person 1, a single claimant, true on the claim), "A couple's Child Tax Credit award does not exclude a dependant who reports none" (the couple true on the claim).ad19029f6setswould_claim_ucfalse in each, which keeps what the case tests.qualifying_young_person_reported_own_awards.yaml: "A reported award also excludes the education route", "An explicit false own-right flag does not erase a reported own ESA award", "A report of one penny of a listed award other than Income Support excludes the young person": in each the claimant's own income-related ESA ends on the claim; the commit setswould_claim_ucfalse.uc_receiving_education.yamlcases 31 and 34 and people 9 and 10: predicate true again (as at Apply the Universal Credit education condition #2806 before the composition), education status still false.- The legacy-closure file and CTR file: unchanged.
The #2806 composition (restart items)
62fa2678mergesmainat47a2817e2(git merge-treereported no conflict first).- At the head, against the note's tables:
uc_receiving_education.yaml42 passed (note: 42 passed with its diff);test_uc_receiving_education_properties.py5 passed (note: 5 passed). W £5,112.96 and M £4,062.96 (note: the same). Nothing moved. What changed underneath: cases 31 and 34 and people 9 and 10 now read predicate true (the claim limbs), and the education status, not the predicate, keeps them out of the definition; case 42 and W/M keep the note's flags, since Working Tax Credit has no claim limb. - W and reg. 5(5): above. The model's amount matches the law on the reading where W receives UC; I didn't change the formula.
- The body's Apply the Universal Credit education condition #2806 bullet now says Apply the Universal Credit education condition #2806 merged at
596142446and this PR carries the composition, with the counts above; the "35/35" and "3/3" counts and every sentence that had Apply the Universal Credit education condition #2806 landing later are gone.
#2813 (P2-c)
On a scratch merge of the head with dc69f3ada (tree e7ef1d49, equal to git merge-tree's), I applied the updated note's diff (MERGE_2813.md, git apply --check passes on a clean checkout). Finding: #2813's own esa_income_report_closed already reads esa_income_payable_share <= 0, and since ad19029f6 the predicate's ESA gate reads ~esa_income_report_closed. So on the merged tree all four of the earlier note's cases pass without the payable-share term (the Pension Credit case too). The term still matters where the gate bypasses esa_income_report_closed: the education status's at_claim_start path, and the reform (| paid_as_reported, P2-c's fix). I added a fifth case that only the term passes: a single 18-year-old in 2027 with unsaved ESA and a Pension Credit report who claims UC (education status true, no UC; without the term status false and £4,238.04). The merged tree also needs esa_income: 706.85 in the new claimant ESA no-claim case (it fails without it), which the note now carries. Runs: see the note's table. Cycle check: 25 households (the head's 24 plus H saved), North East and Scotland, 17 readers: 850 orders, 0 errors, 0 mismatches; H £9,688.86, H saved £5,318.52, H 2026 £5,098.80. Two jobs ran alone after a pool stalled.
Tests at the head (clean runs, uv sync --frozen --extra dev)
- YAML:
uc_qualifying_young_person_existing_benefits.yaml17/17,uc_qualifying_young_person_legacy_closure.yaml10/10,uc_qualifying_young_person_universal_credit_claim.yaml12/12,qualifying_young_person_reported_own_awards.yaml23/23,uc_receiving_education.yaml42/42,council_tax_reduction_young_person.yaml13/13. I didn't rerun the readers' broader YAML directories (1,080 cases atf379377f9); CI covers them. - Python (
-n 4, none near the 40-minute cap; longest 11 min under load): existing-benefits properties 8, legacy closure 5,test_disable_simulated_benefits.py61, neutralised 19,test_uc_receiving_education_properties.py5, Pension Credit IS 10, CTR young person properties 6, CTR severe disability 8, legacy award readers 20, benefit cap specified benefits 46: 188 passed. code_health: 3,164 passed.- Lint:
uvx ruff@0.17.2 format --check .2,232 files already formatted;ruff check .and--select F811,F821 policyengine_ukpass;git diff --check f379377f9..HEADon the PR's files clean (the full range includesmain's merged files; restricted to the PR's files it is clean). check_fix_pr.pyon the posted title and body, no labels: PASS fromc5b9850a3("Changed executable tests: 189", since the range now includesmain) and PASS frommain47a2817e2(16).git merge-tree --write-tree --name-only HEAD: no conflict withmain47a2817e2(tree62b0b8f8, equal to the head's), Count 16-year-olds outside education as UC and Pension Credit qualifying young persons until the 1 September after their 16th birthday #2279c5b9850a3, Apply the Universal Credit education condition #2806596142446andf06a9c636, Place the UC and Pension Credit QYP terminal date from the birth date #2300ee0482e75(5c1db8bc), Model the 2026 income-related ESA abolition and its UC claims #2813dc69f3ada(e7ef1d49).- Cycle check at the head: For households A to G (the issue's table, with E under a reform that neutralises
income_support), H (own income-related ESA in 2027), H 2026, the seven closure households, the six claimant households of the claim table (each limb with and without a claim), and W and M, in the North East and in Scotland, I calculated each of 17 readers (the 15 that read the qualifying young person status,is_qualifying_young_person_for_uc_educationandis_qualifying_young_person_on_uc_migration_day) first, thenuniversal_credit, in a fresh simulation (816 orders). None raised an error, and Universal Credit matched a fresh calculation in every order. At the head H gives £5,318.52 (no Model the 2026 income-related ESA abolition and its UC claims #2813). One pool worker hung on household E (North East); I killed it and ran that household alone (17 orders, 0 errors). The households' UC and predicates match the body's tables. - Old expectations against the new code:
f379377f9's four YAML files andmain'suc_receiving_education.yamlat the head: 3 + 3 + 2 cases fail, listed under Readers; the closure and CTR files pass.
Mutants and revert check (worktree at the head, restored after each)
- Mutants of the predicate at this head, each run against
uc_qualifying_young_person_existing_benefits.yaml,uc_qualifying_young_person_legacy_closure.yaml,uc_qualifying_young_person_universal_credit_claim.yaml,qualifying_young_person_reported_own_awards.yaml,uc_receiving_education.yaml, the property file,test_uc_qualifying_young_person_legacy_closure.py, the two predicate tests intest_disable_simulated_benefits.pyandtest_uc_receiving_education_properties.py; every mutant fails at least one:- the claim limbs applied to every member, not only the claimant or partner: 9 YAML cases (among them "A parent's claim for Universal Credit leaves their young person's own Housing Benefit report excluding them"),
test_a_claim_changes_no_dependants_predicate, two more properties, one closure test and the young person reform test in 3 years; - the income-related ESA limb dropped: the claimant ESA claim case and the claimant reform test for ESA in 3 years;
- the Housing Benefit limb dropped: the claimant HB claim case,
uc_receiving_education.yamlcases 31 and 34, the claimant reform test for HB in 3 years,test_the_predicate_is_reg_5_less_reg_28andtest_examples_reach_the_cases; - the Child Tax Credit limb dropped: the lone parent and couple claim cases and
test_education_condition_invariants; - a claim limb added for Working Tax Credit: the WTC-beside-a-claim case, the couple WTC case in the existing-benefits file and
uc_receiving_education.yamlcase 33; - the Housing Benefit limb applied while the family meets the Pension Credit age conditions: the mixed-age couple case;
- the family closure read from any member's own closure (M3): the last closure case,
test_any_own_existing_benefit_report_excludes_the_personandtest_the_predicate_is_reg_5_less_reg_28; - the family closure dropped from the couple's joint tax credit sum (M4):
test_the_familys_closure_ends_the_couples_joint_tax_credit_award; - the education status read without
at_claim_start:uc_receiving_education.yamlcases 31, 32 and 34,test_examples_reach_the_casesandtest_education_condition_invariants.
- the claim limbs applied to every member, not only the claimant or partner: 9 YAML cases (among them "A parent's claim for Universal Credit leaves their young person's own Housing Benefit report excluding them"),
- With the predicate's formula from
f379377f9(before the claim limbs), the four claimant claim cases,uc_receiving_education.yamlcases 31 and 34 and 9 Python tests (test_the_predicate_is_reg_5_less_reg_28, the claimant reform test for ESA and HB in 3 years each,test_examples_reach_the_casesandtest_education_condition_invariants) fail; every other YAML case passes.
The two claim properties pass under that revert, since without claim limbs a claim changes no predicate; the mutant that applies the limbs to every member failstest_a_claim_changes_no_dependants_predicate, and none of these mutants failstest_a_claim_never_excludes_a_claimant_or_partner, because none adds an exclusion on a claim.
Items
- P1: formula
is_qualifying_young_person_for_universal_credit.py:140-184(claim limbs), docstring:8-69, comments:120-163; closure case 9 comment; body; note. Holds (above). - P2-a: docstring, changelog, both YAML headers and the property docstring carry the review's text with the ruling's change; the body's table says "(open)".
- P2-b: body invariant and property docstring
:39-41; the four figures (£5,098.80 → £9,321.36; £4,801.68 → £8,869.68 twice) come from closure cases 1, 2, 5 and 6 and the existing-benefits file, all passing. - P2-c: note updated (above).
- P3-a: comment
:104-109and docstring. P3-b: cases 7 and 8 moved to a renting family without a claim (HB £6,000 → £0; UC child element 12 × 351.88 = £4,222.56 and 12 × 339.00 = £4,068); case 9 comment as the review gives it, plus the dependant sentence. P3-c: body and property docstring. P3-d: case 5 comment applied. Onis_on_income_support: it reads a dependant's own Income Support report without the person's closure, while the predicate applies it; the model pays no Income Support on a dependant's own report in any year, so neither variable follows a payment. I'd leaveis_on_income_supportalone unless the Pension Credit young person route (SPC reg. 4A(5)) should follow the same proxy; that's a ruling, and I made no change. P3-e, P3-f, P3-g, P3-h, P3-j: applied (P3-h in the note). P3-i: the M3 YAML case (closure case 10) and the M4 test exist; M3 is killed by closure case 10 and two properties, M4 bytest_the_familys_closure_ends_the_couples_joint_tax_credit_awardalone. P3-k: superseded by the restart (Apply the Universal Credit education condition #2806 merged). - Item 6: both claim invariants in the body and as properties (
test_a_claim_changes_no_dependants_predicate,test_a_claim_never_excludes_a_claimant_or_partner); mutants above. No mutant here kills the second, since none adds an exclusion on a claim; it passes at the head with 8/8.
Judgment calls
- Kept
45450629f/cc191bb52'sat_claim_startdesign rather than the note's one-line formula (law above). - W: the law reaches £5,112.96 only on the reading where W receives UC (reg. 5(5)); I left it pending, formula unchanged.
- The Model the 2026 income-related ESA abolition and its UC claims #2813 note keeps the payable-share term though the predicate's own path no longer needs it, because the education status and the reform bypass
esa_income_report_closed. is_responsible_for_child_or_qualifying_young_person_for_uc_or_housing_benefit, which the review's reader list names, doesn't exist; the cycle check usesis_responsible_for_child_or_qualifying_young_person_for_universal_credit, and adds the education status and migration-day variables (17 readers).- Encode UC Regs 2013 regs 12-14 and SI 2014/1230 regs 28 and 60: who receives education, and the exceptions, for the WRA 2012 s.4(1)(d) basic condition TheAxiomFoundation/rulespec-uk#514: I posted nothing. The ruling changes no dependant case; it changes the claimant cases listed under Readers (an 18-year-old claimant's own HB, CTC or IR-ESA, and the couple's joint CTC, now read true when the family claims). If Remove y axis title from validation chart #514's parity cases include any of those with a claim, they change.
Skipped
- No test needed a survey dataset. I didn't run the full YAML or Python suites, or the readers' YAML directories; CI covers them.
- The container restarted once (about 06:30Z); I reran everything it interrupted.
Generated by Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The predicate's income-related ESA gate now requires esa_income_payable_share above 0 unless disable_simulated_benefits pays the report as reported. The term covers the education status on the first day of a claim and the reform, which bypass esa_income_report_closed. Two 2026 cases now expect 706.85 of esa_income (86 of 365 days of 3,000). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
juaristi22
left a comment
There was a problem hiding this comment.
Review status: PARTIAL at 8402aee706e41e0f2c807ea3a23aa07748b57733. No confirmed critical defect. One nonblocking source-contract finding (A1) and one material source-verification gap (G1) remain; approval is withheld.
A1 — Extend the filed Axiom #514 packet to the claim-termination rules implemented here. The actual queued packet, including both comments, covers the existing-benefit exclusion but has no filed extension for TP 2014 reg 8, SI 2013/983 art 4 and SI 2019/1152 reg 5. These rules now determine the claim-start/education results. The repository's queued-parity contract requires module/corpus targets, operative quoted provisions, required outputs and same-source companion cases. Add cases contrasting tax-credit termination, HB/ESA first-day run-on and a dependant's own award, with the applicable country counterparts. Preserve the stated corpus and approved-encoder blockers; this review requests the filed source contract, not an encoder run.
G1 — Northern Ireland claim-termination/run-on binding is not yet verified. The NI base QYP/existing-benefit exclusion was checked. The operative NI provisions for claim termination and the HB/ESA first-day/run-on distinction have not been bound to the UK-wide helper. No wrong NI award is alleged. This narrow primary-source gap prevents a COMPLETE policy review; the GB bindings and completed code checks can be retained while it is closed.
Validation: 156/156 focused checks passed (99 YAML, 57 Python) on the lock-matched Core 3.34.1 environment. They exercise report attribution, legacy closure/ESA abolition, neutralisation, education/claim-start composition and final UC/CTR outcomes without supplying final awards or eligibility outputs. A corrected post-window probe did not establish a reachable wrong-money defect; it is not a blanket certification of behaviour beyond the reported-benefit reform's ten-year window. Setup errors are retained separately from PR results.
The annual/report-based entitlement proxies and historical run-on approximations remain explicit limits. No private-data, population, full-suite/mutation or author-reported order benchmark was independently certified. The source gap is checkpointed for follow-up without repeating the completed code review. The PR also remains a draft, with hosted checks pending at the last capture.
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
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; standard reform costings (change in gov_balance); standard reforms: change in each reform's target. 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
|
SR 2016/226 reg. 6 is the twin of SI 2014/1230 reg. 8 with one difference: a claim ends a rate rebate the day before the award starts (reg. 6(2)), and only a rent rebate or allowance runs on for two weeks (reg. 6(2A)). The first-day test now keeps the Housing Benefit claim limb for a Northern Ireland family that doesn't rent, whose report the model reads as a rate rebate. The variables also cite SR 2017/190 art. 6, SR 2019/152 reg. 4 and the Housing Benefit Regulations (Northern Ireland) 2006 regs. 12 and 13. The predicate now keeps a Housing Benefit report for a family in specified or temporary accommodation, since neither a claim nor the closure ends that award (SI 2014/1230 regs. 8(3) and 46(2); SR 2016/226 regs. 6(3) and 47(2)). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
juaristi22
left a comment
There was a problem hiding this comment.
Source follow-up COMPLETE at unchanged 8402aee706e41e0f2c807ea3a23aa07748b57733. G1 is resolved; A1 remains open.
The operative Northern Ireland chain is now verified: SR 2016/226 regulation 6 distinguishes tax-credit/rate-rebate termination from the rented-HB two-week run-on; SR 2019/152 regulations 1(3), 3 and 4 establishes the IS/income-related ESA run-on from 22 July 2020; and SR 2017/190 articles 2 and 6 supplies the relevant ESA abolition/claim mechanism. The current computed rented-HB route fits that chain. A generic housing_benefit_reported input does not itself identify NI rate-only assistance, so that raw-input mapping remains a stated qualification.
A1 still requires extending the actual filed #514 packet to the adopted claim-termination/run-on provisions: module/corpus, operative rules, expected outputs and same-source companions. The current body and both addenda were rechecked and still omit this extension. No encoded parity is claimed.
This round verified 24 source originals and 24 bound extracts, including three newly acquired primary texts. The prior 156 passing code checks are retained; no model or tests were rerun. Annual first-day/report approximations and historical pre-run-on limits remain documented. Approval is withheld while A1 remains open and the PR remains Draft.
juaristi22
left a comment
There was a problem hiding this comment.
Incremental review COMPLETE at 98b56ca0802f552c2afe6ab8e94a34822f1261df. Requesting changes for one confirmed classification defect. The prior NI source gap G1 remains resolved; A1 remains open.
C1 — Preserve NI rent-allowance reports through the first-day test. The new non-renting/owner shortcut in is_qualifying_young_person_for_universal_credit.py, lines 184–190 treats an NI Housing Benefit report as a rate rebate ended by the UC claim. Dwelling tenure does not establish award form: NI HB regulation 13(1)(f),(g) permits eligible houseboat mooring and caravan site payments. A genuine rent-allowance award receives the regulation 6(2A) run-on, so its recipient remains excluded from QYP status at the first-day test under regulation 29.
The 2026 reproduction uses an 18-year-old in full-time non-advanced education, owned dwelling, £6,000 site/mooring payments and £6,000 reported HB, with ordinary claim preferences and no supplied eligibility outputs. Current code returns UC £0 and net income −£180. Restoring only the exact prior QYP helper returns UC £4,062.96 and net income £3,882.96. This £4,062.96 difference establishes the introduced model effect; it is not an independently certified statutory UC award. The legal premise is a genuine qualifying rent allowance. Computed HB is £0 on both sides under an existing owner-payment limitation; that does not negate the separately reported entitlement. Please represent the relevant award/payment form rather than infer it from tenure, and cover this counterexample.
A1 remains open: the actual #514 packet still needs the adopted claim-termination/run-on modules, operative rules, outputs and same-source companions.
Current validation: 141 passed (104 YAML, 37 Python), with no failures or skips in one bounded campaign. The native causal comparison is separate. Prior 156 passes remain historical evidence. Source review verified 27 originals and 27 bound extracts; no material evidence gaps remain in the review scope. Passing tests do not cover the counterexample above.
housing_benefit_reported doesn't show whether a Northern Ireland award takes the form of a rate rebate or of a rent rebate or allowance, and tenure doesn't settle it: SR 2006/405 reg. 13(1)(f) and (g) pays a rent rebate or allowance on mooring charges and site payments, so an owner can get one, and SR 2016/226 reg. 6(2A) runs it on. The first-day test now reads housing_benefit_report_is_rate_rebate_only, false by default, so a report takes the run-on unless the input marks it as a rate rebate alone. Case 47 covers an owner with a rent allowance, who keeps Universal Credit of £4,062.96. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
juaristi22
left a comment
There was a problem hiding this comment.
Incremental review COMPLETE at 590a2cc39c218941496c01e6e3cb8f720a5e2833. C1 is resolved; A1 remains open; G1 remains resolved.
The new explicit housing_benefit_report_is_rate_rebate_only input replaces the tenure shortcut. A genuine NI caravan-site/houseboat-mooring rent-allowance report is preserved by the default false value, while an explicitly identified rate-rebate-only report ends on the relevant first-day route. The source-defined award distinction is represented directly.
The original 2026 counterexample now returns model UC £4,062.96 and net income £3,882.96. Marking the report explicitly rate-rebate-only returns UC £0/net income −£180. Country and nonclaimant guards also pass. These are paired model-contract results, rather than independent certification of an absolute statutory UC award; population mapping of the input is not claimed.
Current validation: 81 repository cases passed (75 YAML, six Python), with no repository failures or skips, plus corrected final-result controls across five households. The earlier 141/156 passing runs remain separate historical evidence. All 27 retained source originals/extracts were hash-validated.
A1 still requires extending the actual #514 packet to the adopted claim-termination/run-on modules, operative rules, outputs and companions. The PR remains Draft; this follow-up records C1's fix without approval while A1 remains open.
C1 is resolved at590a2cc3, verified by81 repository cases and the prior NI counterexample. Follow-up5787924495 retains nonblocking A1 and the approval gates.
|
A1: I've posted the packet on TheAxiomFoundation/rulespec-uk#514, with the modules, the required outputs, the operative words with their source hashes, and 14 companion cases. The cases cover the first-day paths for tax credits, Housing Benefit and income-related ESA, a dependant's own award, and the Northern Ireland counterparts. Axiom still needs to ingest the instruments before an encoder run can bind the modules. I've also added CI's report at |
juaristi22
left a comment
There was a problem hiding this comment.
Follow-up review COMPLETE at 590a2cc39c218941496c01e6e3cb8f720a5e2833, with no open findings or material evidence gaps. A1 is resolved by the actual #514 addendum, which now supplies the adopted GB/NI claim-termination and run-on modules, operative rules, Person-by-Day outputs and 14 companion cases. The relevant clauses were compared with retained primary sources. Queued dispatch completion does not claim encoded parity or authorize a billed encoder run.
C1 and the NI primary-source gap remain resolved. The explicit award-form input preserves genuine NI site/mooring rent allowances; the default-false assumption and reported-award/payment limitations remain disclosed.
The unchanged head retains 81 passing repository cases and final-result controls across five households. These are causal model-contract results, rather than independent certification of absolute statutory awards or population input mapping. All 27 source originals/extracts were validated; the actual issue and addenda were freshly captured. No tests or model calculations were repeated. The scoped aggregate claims were checked against the primary CI report. The PR is Ready, and the latest 20 distinct exact-head checks pass. Head, description, review activity, source guards and required checks were refreshed before this approval.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #2804
Reproducer: policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_qualifying_young_person_existing_benefits.yaml::A parent's young person who reports their own Income Support attracts no child element.
axiom: TheAxiomFoundation/rulespec-uk#514 queued
What the code does now
is_qualifying_young_person_under_universal_credit_regulation_5applies UC Regs 2013 reg. 5: the age condition, either route in reg. 5(1), and reg. 5(5)'s exclusions for Universal Credit, contributory ESA and contribution-based JSA.is_qualifying_young_person_for_universal_credittakes that test and applies SI 2014/1230 reg. 28: it excludes a person who reports their own income-related ESA, income-based JSA, Income Support, Housing Benefit, Child Tax Credit or Working Tax Credit, and a claimant or partner when either member of the couple reports a tax credit award.The Secretary of State ties reg. 28's exclusion to entitlement, so the predicate reads a report only in a year the model pays that benefit, only until the model's legacy closure, or for a claimant or partner the family's Universal Credit claim, ends the award on it, and only if no reform neutralises it. It doesn't apply the model's other conditions of payment, so a report can exclude a young person even where the model pays nothing on it. In the table's household, the model pays no Income Support on the young person's own report, because it pays Income Support only on the claimant's and partner's reports. It pays no Housing Benefit on it either, because the family owns its home. The model pays Income Support, income-based JSA and tax credits only while
gov.dwp.income_support.active,gov.dwp.JSA.income.activeandgov.dwp.tax_credits.activehold, has no closure date for Housing Benefit, and pays an income-related ESA claim only while it stays payable (esa_income_payable_share), so the predicate reads its report only then. The predicate also reads the Income Support, income-based JSA and tax credit reports while a reform turns ongov.contrib.policyengine.disable_simulated_benefitsto pay them at their reported amounts in the dataset year and the nine years after it.The model also ends legacy awards through its annual migration proxy. It ends a person's own Income Support, income-based JSA and income-related ESA when
person_legacy_benefits_closedholds, the family's tax credits whenlegacy_benefits_closedholds, and its Housing Benefit whenlegacy_benefits_closedholds and the family doesn't meet the Pension Credit age conditions. The predicate follows both closures. A claim for Universal Credit ends the claimant's and partner's own Housing Benefit, Income Support and tax credits (SI 2014/1230 reg. 8(2) and (2A)) and their income-related ESA (SI 2013/983 art. 4). The model ends the family's continuing Housing Benefit award and its Child Tax Credit, and the claimant's and partner's income-related ESA, when the family claims. For a claimant or partner the predicate follows those limbs. A parent's claim doesn't end a dependant's own award, so for a dependant it doesn't: a young person's own Housing Benefit or Child Tax Credit report still removes the child element when the family claims Universal Credit. The model has no such limb for Working Tax Credit, Income Support or income-based JSA, so neither does the predicate. In its years thedisable_simulated_benefitsreform pays a claimant's or partner's own income-related ESA, Housing Benefit and Child Tax Credit report as reported whether or not the family claims, so whenever a reform turns the switch on, the predicate leaves the claim limbs out. It leaves them out after the reform's ten years too, when the model again ends those awards on the claim.The predicate can't read
housing_benefit_eligible: that variable's Pension Credit route readsleft_pension_route_at_legacy_closure, which reads the predicate throughis_uc_entitledand the child element. So the predicate keeps a Housing Benefit report while the family meets the Pension Credit age conditions. It also keeps the report while the family lives in specified or temporary accommodation (in_specified_or_temporary_accommodation), since neither a claim (SI 2014/1230 reg. 8(3)) nor the closure (reg. 46(2)) ends that award, andhousing_benefit_eligiblekeeps it.claims_universal_creditreadswould_claim_uc,would_claim_uc_at_legacy_closureandlegacy_benefits_closed, not the predicate. The formula checks whether a reform neutralises each report's own variable:income_support,jsa_income,esa_income,housing_benefit,child_tax_creditortax_creditsfor Child Tax Credit, andworking_tax_creditortax_creditsfor Working Tax Credit. A reform that zeroes a benefit's formula without neutralising its variable leaves the exclusion in place. The formula checks each credit the same way in the couple's joint award. Both qualifying young person variables read raw person-level reports. The predicate also readsperson_legacy_benefits_closed,legacy_benefits_closed,meets_pension_credit_age_conditions,esa_income_report_closed,esa_income_payable_shareandclaims_universal_credit, which read reports,is_claimant_or_partner, ages, the household's country, the family's claim inputs and parameters, and no calculated Universal Credit variable.in_specified_or_temporary_accommodationandhousing_benefit_report_is_rate_rebate_onlyhold supplied statuses with no formula. So neither variable calculates a family benefit that depends on qualifying young person status. Compared with the same family without the claimant's and partner's reports, those reports change a dependant's predicate only where the proxy closes the family's legacy awards on them, and then only to lift an exclusion.In a household of a parent aged 45 and a 17-year-old on a non-advanced course (North East, owned outright, no income), a parent whose young person reports their own Housing Benefit in 2026, or Income Support in 2025, now gets no child element: £5,098.80 in place of £9,321.36 in 2026, and £4,801.68 in place of £8,869.68 in 2025. If the young person reports their own income-related ESA together with their own Income Support or income-based JSA in 2026, or their own tax credits in 2025, the migration proxy closes their legacy awards, the model pays no income-related ESA on their report, and the parent keeps the child element (£9,321.36 in 2026, £8,869.68 in 2025). In 2027-28 the model pays nothing on an income-related ESA claim, so the young person's own report leaves the child element in place (£9,688.86); a claim saved for an appointee stays payable and removes it (£5,318.52).
income_supportchild_tax_credithousing_benefitEach cell gives the young person's predicate and the parent's annual Universal Credit. The parent claims Universal Credit in every row, and the young person is a dependant, so the claim limbs change no row.
An 18-year-old claimant on a full-time non-advanced course in North East England, who reports their own award, with and without a claim for Universal Credit (
would_claim_uc; without a claim,would_claim_uc_at_legacy_closurefalse too). Each cell gives the claimant's predicate and their annual Universal Credit:The model's reg. 5 variable reads reg. 5(5) from
universal_credit_reportedandreceives_benefits_in_own_right, not from a calculated Universal Credit award, so a claimant who claims Universal Credit still meets it here.CI's
Aggregate impactreport (run 42056786933) comparedmaine3765cb61with9f3957ef5f, its merge of this head into that main, for each fiscal year from 2025-26 to 2031-32. The change breaks no check, the report flags no move, and we estimate that no household's net income changes in any year.The Universal Credit education condition
is_qualifying_young_person_for_uc_educationgives the status the condition that a claimant isn't receiving education reads (UC Regs 2013 reg. 12(1A)). It equals the predicate, except for a claimant or partner whom the Housing Benefit or income-related ESA claim limb readmits. A claim ends a tax credit award "on the day before the first date" of entitlement to Universal Credit (SI 2014/1230 reg. 8(2)(a)), but a Housing Benefit award only on "the last day of the period of two weeks" after that day (reg. 8(2A)), and SI 2019/1152 reg. 5 gives income-related ESA the same two weeks. So on the first day of the award the Secretary of State still excludes such a claimant through reg. 28, and reg. 12(2) decides whether they receive education. Once they receive Universal Credit, reg. 5(5) excludes them: "A person who is receiving universal credit ... is not a qualifying young person." The model's reg. 5 test reads reg. 5(5) only from reported Universal Credit, so the education status removes those whom the two claim limbs readmit, and keeps the Child Tax Credit limb. SI 2018/65 inserted reg. 8(2A) from 11 April 2018, and SI 2019/1152 reg. 5 reaches claims made from 22 July 2020; the annual model applies both run-ons in earlier years too. A claimant whose Universal Credit comes to nil receives none, so after the run-on the law counts them as a qualifying young person again; the annual model keeps them outside the definition all year.is_qualifying_young_person_on_uc_migration_dayreads every reported existing benefit, the award the closure claim ended, for the reg. 60(1) protection (uc_is_excepted_from_education_condition), as #2806's composition note gives it.uc_receiving_education.yaml, 2026 unless stated)W, unlike M, met the condition on the migration day (reg. 28 applied, and with no maintenance funding on a non-advanced course reg. 12(2) didn't), so reg. 60(1) doesn't reach W. W's claim ends the tax credit award the day before the Universal Credit award would start (reg. 8(2)). From then reg. 28 no longer applies, and W doesn't yet receive Universal Credit, so reg. 5(5) doesn't apply either: W counts as a qualifying young person, and DWP treats W as receiving education (reg. 12(1A)). DWP reads reg. 5(5) as following an entitlement found on another ground (ADM E2093: a twin who has a child and gets Universal Credit stops counting, and the other twin still counts). So DWP pays W nothing unless a reg. 14(1) exception applies, such as having no parental support, for which the model has no input. The model pays W 12 × £338.58 + £6,000 − 55% × £9,000 = £5,112.96 through its reg. 60(1) rule, on
mainand here; #2878 narrows that rule. A claimant on a Housing Benefit or income-related ESA run-on takes a different path: the Secretary of State keeps them outside the definition on the first day (reg. 28), and they receive Universal Credit from then on.Scotland's working-age Council Tax Reduction
is_child_or_young_person_for_council_tax_reductionnow reads the reg. 5 variable for Scotland's working-age scheme, not the UC predicate. The Scottish Ministers define a young person by reference to reg. 5 (SSI 2021/249 reg. 6(1)), and the Secretary of State excludes a person entitled to an existing benefit only for the purposes of the UC Regulations and WRA 2012 Part 1 (reg. 28(a) and (b)). So the model no longer takes a young person out of the family for Scottish CTR when they report their own existing benefit. That includes income-related ESA and income-based JSA, which #2279 excluded there through the same reader.We estimate these flags and awards for a Scottish household of a parent aged 45 and a 17-year-old on a non-advanced course that claims CTR (
would_claim_council_tax_reduction) on council tax of £1,500:In the Income Support, Housing Benefit and Child Tax Credit rows, the CTR award rises because the parent's Universal Credit loses the child element under reg. 28. In the other rows, the parent's Universal Credit and the award stay the same. With
would_claim_ucfalse, the household gets £1,500 of CTR in every row (£1,374.96 in the Child Tax Credit row). The flag sets the applicable amount, which moves the award only where applicable income exceeds it. When a 19-year-old reports their own Housing Benefit, the model keeps the 22-year-old applicant they live with a lone parent, with an applicable amount of £4,968.60 in place of £3,933.80 (Case 11). If the 19-year-old reports income-related ESA in place of Housing Benefit and the applicant earns £5,000 in 2026 and claims CTR, the award rises from £1,305.44 under #2279 to £1,512.40 (Case 13).Northern Ireland
The model applies the same predicate in Northern Ireland, where the Secretary of State made the same provision in SR 2016/226 reg. 29 and listed the same existing benefits in reg. 2(1); the variable cites both. The reg. 5 variable cites reg. 6 of the Universal Credit Regulations (Northern Ireland) 2016 (SR 2016/216), whose reg. 6(5) repeats GB reg. 5(5): "A person who is receiving universal credit, an employment and support allowance or a jobseeker's allowance is not a qualifying young person."
A Universal Credit claim ends awards in Northern Ireland as it does in Great Britain, with one difference for Housing Benefit. SR 2016/226 reg. 6(2) ends a tax credit and "housing benefit in the form of a rate rebate" on the day before the award starts, and reg. 6(2A) runs Income Support and "a rent rebate or allowance" on for two weeks. Regs. 6(2B) and (3) match GB reg. 8(2B) and (3), and reg. 47(2) matches GB reg. 46(2). For income-related ESA, the Secretary of State appoints the first day of the period claimed for (SR 2017/190 art. 6) and adds the same two weeks for claims from 22 July 2020 (SR 2019/152 reg. 4).
housing_benefit_reporteddoesn't show which form a person gets, and tenure doesn't settle it. A person gets a rate rebate on the rates of their home (Housing Benefit Regulations (Northern Ireland) 2006 reg. 12(1)) and a rent rebate or allowance on rent and similar payments (reg. 13(1)). Those payments include mooring charges for a houseboat and site payments for a caravan or mobile home (reg. 13(1)(f) and (g)), so an owner can get a rent allowance. The new supplied inputhousing_benefit_report_is_rate_rebate_onlymarks a person's report that covers a rate rebate alone, and defaults to false. On the first day of the award, the formula reads a report with that mark as a rate rebate the claim has already ended, and any other Northern Ireland report as a rent rebate or allowance that still runs. A person with both forms leaves the input false, because their rent rebate or allowance still runs. After the first day the formula treats both forms as ended.For example, take an 18-year-old on a non-advanced full-time course in 2026 with a £6,000 report, no maintenance funding and no exception:
uc_receiving_educationis_uc_eligibleThe dataset build in policyengine-uk-data fills the report from FRS benefit code 94 (
policyengine_uk_data/datasets/frs.py) and lists no rate rebate code, and we haven't established whether code 94 covers a Northern Ireland rate rebate. It fills nohousing_benefit_report_is_rate_rebate_only, so the model reads every Northern Ireland report in the data as a rent rebate or allowance.housing_benefit_eligibleneeds social housing orLHA_eligible, which readsbenunit_is_renting, so the model computes £0 of Housing Benefit for the owner of case 47.Law
gov.dwp.income_support.active,gov.dwp.JSA.income.active,gov.dwp.tax_credits.active), and holds no closure date for Housing Benefit. Its annual migration proxy (legacy_benefits_closed,person_legacy_benefits_closed) ends a person's other legacy awards once one of their reported legacy benefits has closed. The predicate follows that proxy, which is a modelling choice, not law. The Secretary of State appointed 1 July 2026 for the abolition of the remaining old-style income-related ESA awards in Great Britain (SI 2025/1148 art. 3A), and the Department for Communities appointed 1 October 2026 in Northern Ireland (SR 2025/176 art. 3A), subject to savings. The model pays an income-related ESA claim only whileesa_income_payable_shareexceeds 0, and the predicate reads its report only then.Invariants
person_legacy_benefits_closedholds for a person, their own income-related ESA report never excludes them. Oncelegacy_benefits_closedholds for a family outside the Pension Credit age conditions and outside specified or temporary accommodation, no member's Housing Benefit report excludes them.esa_income_payable_sharereaches 0 for a person, their own income-related ESA report never excludes them, unlessdisable_simulated_benefitspays it as reported.child_tax_creditlifts Child Tax Credit reports,working_tax_creditlifts Working Tax Credit reports,tax_creditslifts both, and every other report still excludes.would_claim_ucon never changes the predicate of anyone who isn't the claimant or partner.would_claim_ucon never turns a claimant's or partner's predicate from true to false.housing_benefit_report_is_rate_rebate_onlymarks the report.income_support), H (own income-related ESA in 2027), H saved for an appointee, H 2026, the seven closure households, the six claimant households of the claim table (each limb with and without a claim), and W and M, in the North East and in Scotland, we calculated each of 17 readers (the 15 that read the qualifying young person status,is_qualifying_young_person_for_uc_educationandis_qualifying_young_person_on_uc_migration_day) first, thenuniversal_credit, in a fresh simulation (850 orders). None raised an error, and Universal Credit matched a fresh calculation in every order.Tests
uc_qualifying_young_person_existing_benefits.yaml(17 cases): the issue's household, with the Income Support and income-based JSA rows in 2025 while they pay; the same reports in 2026, and a Child Tax Credit report in 2025, after the benefit closes; own tax credit reports; a parent's reports; a couple's tax credit award; a partner's own Housing Benefit, which leaves an 18-year-old claimant in the test; and a dependant's own Child Tax Credit, which does the same. The cases where a claimant or partner reports an award they'd lose on a claim setwould_claim_ucfalse.uc_qualifying_young_person_legacy_closure.yaml(11 cases, each with its arithmetic in a comment). The young person's own report:uc_qualifying_young_person_esa_income_abolition.yaml(5 cases, each with its arithmetic in a comment). A young person's own income-related ESA report leaves the child element in 2027 after the claim stops paying (£9,688.86), removes it when saved for an appointee (£5,318.52) and removes it in 2026 (£5,098.80). A Pension Credit report that keeps the young person's legacy awards open changes none of that (£9,688.86), and an 18-year-old claimant in that position receives education and gets no Universal Credit in 2027. Without the payable-share term only that last case fails (is_qualifying_young_person_for_uc_education@2027: [0.] differs from [1.]).uc_qualifying_young_person_universal_credit_claim.yaml(12 cases): the claimant table above, with and without a claim, for each claim limb and the couple's joint Child Tax Credit; the Working Tax Credit report beside a claim; a mixed-age couple on the Pension Credit route whose 18-year-old partner keeps their Housing Benefit exclusion though the couple claims; and the dependant's own Housing Benefit with and without the parent's claim (false both ways).test_uc_qualifying_young_person_legacy_closure.py(5 tests): under a parameter reform that keeps the tax credit scheme open in 2026, a young person's own Child Tax Credit or Working Tax Credit report excludes them (£5,098.80), until the proxy closes the family's legacy awards on the parent's own Income Support report (£9,321.36); and the family's closure ends the couple's joint tax credit award for an 18-year-old claimant whose partner reports Income Support and Working Tax Credit.test_disable_simulated_benefits.py:test_a_young_person_on_a_benefit_the_reform_pays_is_no_qualifying_young_person(3 years) andtest_a_claimant_on_a_benefit_the_reform_pays_beside_universal_credit_is_no_qualifying_young_person(3 limbs × 3 years). Under the reform the model pays a young person's own income-based JSA, income-related ESA, Housing Benefit, Child Tax Credit and Working Tax Credit report, and a claimant's own income-related ESA, Housing Benefit and Child Tax Credit report beside a claim, at £3,000 in 2025, 2026 and 2027, and the predicate excludes each; without it the claim ends the claimant's award and the predicate gives true.test_uc_qualifying_young_person_neutralised_benefits.py(19 tests): in 2024, one 17-year-old per benefit reports it, and a reform neutralises each ofincome_support,jsa_income,esa_income,housing_benefit,child_tax_credit,working_tax_creditandtax_creditsin turn. Each reform lifts the exclusions on its own benefit's reports and no others. Under a reform that neutralises their benefit, the young people in households G (own Housing Benefit, 2026) and D (own Child Tax Credit, 2024) count as qualifying young people, and the parent gets £9,321.36 and £8,721.36 of Universal Credit.test_uc_qualifying_young_person_existing_benefits_properties.py(8 properties): Hypothesis properties for the invariants over 2024, 2025 and 2026, without a reform, where each family drawswould_claim_uc. The oracle reads the model's closure variables andclaims_universal_credit.test_a_claim_changes_no_dependants_predicateandtest_a_claim_never_excludes_a_claimant_or_partnercompare each family withwould_claim_ucoff and on.uc_receiving_education.yaml(47 cases) andtest_uc_receiving_education_properties.py(5 properties): cases 31, 33, 34 and 42 as in the education table; cases 43 to 45 and 47 as in the Northern Ireland table; case 46, where temporary accommodation keeps the 18-year-old of case 31 outside the definition all year; the property oracle applies reg. 28 on the first day of the award (excluded_on_first_day) and on the migration day, and three examples reach the claim: an 18-year-old whose tax credit report closed the family's awards in 2026, one who reports Child Tax Credit with no child in 2024, and three Northern Ireland families in 2026: an owner with the input true, an owner with it false and a council tenant. The generator draws the input in every region, and the oracle reads it only in Northern Ireland.council_tax_reduction_young_person.yamlCases 11 to 13: for Scotland's working-age scheme, the model keeps a young person who reports their own Housing Benefit, Income Support, income-based JSA or income-related ESA in the family, and drops one who reports contribution-based JSA; Case 13 asserts the £1,512.40 award.test_council_tax_reduction_young_person_properties.pyreads the reg. 5 variable for the Scottish branch, andtest_council_tax_reduction_severe_disability_family.pysupplies the reg. 5 status as its input.qualifying_young_person_reported_own_awards.yaml: the three income-based JSA cases run in 2025, while it pays; a member's 2026 Income Support report leaves the UC route available; the cases where a claimant reports their own income-related ESA setwould_claim_ucfalse.test_pension_credit_young_person_income_support.py: the reform that removes Income Support restores the UC status as well as the Pension Credit one.test_benefit_cap_specified_benefits_properties.py: the oracle reads a young person's income-based JSA report only in a year the model pays it, as the predicate does.62fa2678on Count 16-year-olds outside education as UC and Pension Credit qualifying young persons until the 1 September after their 16th birthday #2279's code (c5b9850a3):uc_qualifying_young_person_existing_benefits.yaml9 of 17 fail,uc_qualifying_young_person_legacy_closure.yaml6 of 10,uc_qualifying_young_person_universal_credit_claim.yaml8 of 12, the reported-awards file 5 of 23 andcouncil_tax_reduction_young_person.yaml3 of 13 (Cases 11 to 13).62fa2678ond5d32074d's code, whose predicate follows neither the legacy closure nor the claim: 7 of 10 closure cases and 4 of 12 claim cases fail; the other three files pass.62fa2678, each run againstuc_qualifying_young_person_existing_benefits.yaml,uc_qualifying_young_person_legacy_closure.yaml,uc_qualifying_young_person_universal_credit_claim.yaml,qualifying_young_person_reported_own_awards.yaml,uc_receiving_education.yaml, the property file,test_uc_qualifying_young_person_legacy_closure.py, the two predicate tests intest_disable_simulated_benefits.pyandtest_uc_receiving_education_properties.py; every mutant fails at least one:test_a_claim_changes_no_dependants_predicate, two more properties, one closure test and the young person reform test in 3 years;uc_receiving_education.yamlcases 31 and 34, the claimant reform test for HB in 3 years,test_the_predicate_is_reg_5_less_reg_28andtest_examples_reach_the_cases;test_education_condition_invariants;uc_receiving_education.yamlcase 33;test_any_own_existing_benefit_report_excludes_the_personandtest_the_predicate_is_reg_5_less_reg_28;test_the_familys_closure_ends_the_couples_joint_tax_credit_award;at_claim_start:uc_receiving_education.yamlcases 31, 32 and 34,test_examples_reach_the_casesandtest_education_condition_invariants.f379377f9(before the claim limbs), the four claimant claim cases,uc_receiving_education.yamlcases 31 and 34 and 9 Python tests (test_the_predicate_is_reg_5_less_reg_28, the claimant reform test for ESA and HB in 3 years each,test_examples_reach_the_casesandtest_education_condition_invariants) fail; every other YAML case passes.The two claim properties pass under that revert, since without claim limbs a claim changes no predicate; the mutant that applies the limbs to every member fails
test_a_claim_changes_no_dependants_predicate, and none of these mutants failstest_a_claim_never_excludes_a_claimant_or_partner, because none adds an exclusion on a claim.would_claim_ucandwould_claim_uc_at_legacy_closuredrawn, 2019 and 2024 to 2027, with and withoutdisable_simulated_benefits), the predicate gives the same 5,910 values as before its formula moved into the module functionqualifying_young_personthat the education status shares.62fa2678, before the merge withmain:uc_qualifying_young_person_existing_benefits.yaml17/17,uc_qualifying_young_person_legacy_closure.yaml10/10,uc_qualifying_young_person_universal_credit_claim.yaml12/12,qualifying_young_person_reported_own_awards.yaml23/23,uc_receiving_education.yaml42/42 andcouncil_tax_reduction_young_person.yaml13/13; ten Python files 188 passed (-n 4, each file inside 12 minutes):test_uc_qualifying_young_person_existing_benefits_properties.py8,test_uc_qualifying_young_person_legacy_closure.py5,test_disable_simulated_benefits.py61,test_uc_qualifying_young_person_neutralised_benefits.py19,test_uc_receiving_education_properties.py5,test_pension_credit_young_person_income_support.py10,test_council_tax_reduction_young_person_properties.py6,test_council_tax_reduction_severe_disability_family.py8,test_legacy_award_readers_properties.py20 andtest_benefit_cap_specified_benefits_properties.py46;policyengine_uk/tests/code_health3,164 passed.62fa2678merged with Model the 2026 income-related ESA abolition and its UC claims #2813's head (dc69f3ada), with the income-related ESA term and the case changes above:uc_qualifying_young_person_esa_income_abolition.yaml5/5,uc_qualifying_young_person_legacy_closure.yaml10/10,uc_qualifying_young_person_universal_credit_claim.yaml12/12,uc_qualifying_young_person_existing_benefits.yaml17/17,qualifying_young_person_reported_own_awards.yaml23/23,council_tax_reduction_young_person.yaml13/13,uc_receiving_education.yaml42/42,esa_income_abolition.yaml41/41 andperson_legacy_benefits_closed.yaml3/3;test_disable_simulated_benefits.py61 passed,test_uc_qualifying_young_person_neutralised_benefits.pywithtest_pension_credit_young_person_income_support.py29 passed, andtest_uc_qualifying_young_person_existing_benefits_properties.pywithtest_uc_qualifying_young_person_legacy_closure.py13 passed. CI runs the full suite at590a2cc39.590a2cc39withmaine3765cb61merged in, and all 20 checks pass.Merge notes
mainat040e9a5f0, which holds Count 16-year-olds outside education as UC and Pension Credit qualifying young persons until the 1 September after their 16th birthday #2279, Apply the Universal Credit education condition #2806 and Model the 2026 income-related ESA abolition and its UC claims #2813.git merge-tree --write-tree --name-onlyfinds no conflict with Place the UC and Pension Credit QYP terminal date from the birth date #2300 (25cb0070e). Add CB and CTC qualifying young person routes for school leavers #2301 (78d4d5070) and Test PC, CB and CTC QYP exclusion of young Income Support recipients #2302 (0d934a836) each conflict in one docstring intest_benefit_cap_specified_benefits_properties.py, which needs both PRs' sentences.596142446without the change its composition note (the comment on Apply the Universal Credit education condition #2806 headed "with the reg 60(1) gate on the migration day") gives whichever of the two lands second, so this PR carries it.is_qualifying_young_person_for_uc_educationreads the predicate, andis_qualifying_young_person_on_uc_migration_dayholds Apply the Universal Credit education condition #2806's former formula for the reg. 60(1) gate;uc_receiving_education.yamlsets cases 31, 33, 34 and 42 and the property file gives its oracle the predicate's filters. This PR goes further than the note: the education status reads the claimant's status on the first day of their award (see the education section). At8402aee70uc_receiving_education.yamlpasses 42/42 (the note: 42 passed) andtest_uc_receiving_education_properties.py5/5 (the note: 5 passed); W gets £5,112.96 and M £4,062.96, as in the note.🤖 Generated with Claude Code