Skip to content

Test PC, CB and CTC QYP exclusion of young Income Support recipients - #2302

Open
MaxGhenis wants to merge 95 commits into
mainfrom
qyp-income-support-exclusion
Open

MaxGhenis wants to merge 95 commits into
mainfrom
qyp-income-support-exclusion

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2292

Reproducer: policyengine_uk/tests/test_qualifying_young_person_income_support_properties.py::test_a_reform_paying_no_income_support_restores_the_severe_disability_addition

axiom: SPC Regs 2002 reg 4A(5) TheAxiomFoundation/rulespec-uk#360 queued (its Case F and addendum cases A3 and A4 cover income support); Child Benefit (General) Regulations 2006 reg 8 TheAxiomFoundation/rulespec-uk#356 queued (addendum); CTC Regs 2002 reg 5(4)(c) TheAxiomFoundation/rulespec-uk#361 queued (addendum, Case J). Each states its blocker: rulespec-uk's corpus pin holds SPC reg 4A's and CTC reg 5's rows and no row for Child Benefit reg 8, and a billed encoder run needs approval.

Stack. The merge order is #2300, #2301, then #2302. This branch carries both: 919cc37 merges #2301's head 78d4d50, which lands the Child Benefit commits from the closed #2299, and 0d934a8 merges #2300's head 25cb007. So landing this PR lands both. #2279 and #2387 have merged into main, which both merged heads carry at 47a2817. Until #2300 and #2301 merge, the diff here also shows their commits. This PR's own commits:

git log -p --no-merges origin/qyp-income-support-exclusion ^origin/main ^origin/ctc-qyp-school-leaver-routes ^origin/qyp-terminal-date-19

Summary

Pension Credit, Child Benefit and Child Tax Credit law each excludes a young person who receives Income Support in their own right from qualifying young person (QYP) status. Universal Credit's does not. The model's own-right input, receives_benefits_in_own_right, covers Universal Credit, Employment and Support Allowance and Jobseeker's Allowance, the benefits Universal Credit's own rule lists.

The exclusions themselves now reach main from the PRs below this one: #2279 gave Pension Credit is_on_income_support, #2301 gives CTC the same reader, and #2301 also brings the Child Benefit gate and receives_income_support_in_own_right from #2299's commits. This PR's own change narrows to tests that the three programmes exclude and Universal Credit does not, in the baseline and under reforms that remove Income Support, a comment in the Pension Credit test, and its changelog fragment.

How each programme reads receipt.

Programme Reader A claimant or partner Another member of the benefit unit
Pension Credit (reg. 4A(5)) is_on_income_support (#2279) the benefit unit's calculated award, for whichever of the claimant and partner receives it their own reported award, while Income Support runs and no reform neutralises it
Child Tax Credit (reg. 5(4)(c)) is_on_income_support (#2301) the same the same
Child Benefit (reg. 8(2)(a)) receives_income_support_in_own_right (#2301, from #2299's commits) their own reported award, on the same terms the same
Universal Credit (reg. 5(5)) none: the regulation does not list Income Support

What a reform must do. A reform that sets gov.dwp.income_support.active to false, or neutralises income_support, ends every programme's exclusion for everyone. A reform that only replaces income_support's formula with one returning zero ends the Pension Credit and Child Tax Credit exclusion for a claimant or partner, because both read the calculated award. For any other member, and for Child Benefit, that reform leaves a reported award standing.

Why Child Benefit reads the report. The benefit unit's Income Support depends on which young people count for the family's Child Benefit. In fresh simulations in 2016, 2019 and 2024 of a parent claiming Income Support with a 17-year-old at school reporting their own, reading is_on_income_support from the Child Benefit test raised a CycleError on calculating child_benefit first:

child_benefit → child_benefit_entitlement → child_benefit_respective_amount
  → is_child_benefit_payable_for_person → is_qualifying_young_person_for_child_benefit
  → is_on_income_support → income_support → income_support_entitlement
  → income_support_applicable_amount
  → is_responsible_for_child_or_young_person_for_legacy_benefits
  → is_child_or_young_person_for_legacy_benefits
  → is_child_or_qualifying_young_person_for_child_benefit
  → is_qualifying_young_person_for_child_benefit

The case the reproducer pins. In 2019, a pensioner aged 70 on PIP daily living with £5,000 of State Pension shares a household with an unrelated 18-year-old in their own benefit unit: an orphan with no parental substitute, in qualifying full-time non-advanced education, with an existing Income Support claim (IS Regs 1987 reg. 13(2)(c)), caring for the pensioner 35 hours a week. Neither claims Carer's Allowance or Universal Credit. The household net income column leaves out cost-of-living payments, which DWP first paid in 2022:

Student's Income Support Student a PC QYP Pension Credit Household net income
Baseline, Income Support in payment £3,010.80 (£57.90 × 52) no (reg. 4A(5)) £3,697 £14,805.70
Reform replacing income_support's formula with zero, at 98a4739 £0 no: the report still counted £3,697 not rerun
The same reform, at this head £0 yes £7,121.20 £15,219.10
income_support neutralised, at this head £0 yes £7,121.20 £15,219.10

Once the student receives no Income Support, they count as a qualifying young person (reg. 4A(5) no longer applies), DWP does not count them as residing with the pensioner (Schedule I para. 2(2)(f)), and the pensioner gets the severe disability addition, £65.85 × 52 = £3,424.20: £167.25 × 52 = £8,697 − £5,000 + £3,424.20 = £7,121.20. Household net income: State Pension £5,000 + PIP £58.70 × 52 = £3,052.40 + Pension Credit + Income Support + winter fuel payment £200 − TV licence £154.50.

The law

  • Pension Credit. SPC Regs 2002 reg. 4A(5): "A person who is receiving universal credit, a contributory employment and support allowance, a contribution-based jobseeker's allowance, an income-related employment and support allowance, an income-based jobseeker's allowance or income support is not a qualifying young person." Schedule I para. 2(2)(f) ignores "a person who is a qualifying young person within the meaning of regulation 4A or child" residing with the claimant.
  • Child Benefit, and through SSCBA 1992 s.142 the Housing Benefit and legacy young person. Child Benefit (General) Regulations 2006 reg. 8(2): "The condition is that the person is not in receipt, in a week, of— (a) income support, [...]".
  • Child Tax Credit. CTC Regs 2002 reg. 5(4)(c): the period must not be "a period in respect of which that person receives income support, [...]".
  • Universal Credit does not list it. UC Regs 2013 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."
  • IS Regs 1987 reg. 13(2) lets a qualifying young person in relevant education claim Income Support where they have "no parent nor any person acting in the place of his parents" ((c)), among other cases.

Change

is_qualifying_young_person_for_pension_credit and is_qualifying_young_person_for_child_tax_credit read is_on_income_support; is_qualifying_young_person_for_child_benefit reads receives_income_support_in_own_right; is_qualifying_young_person_for_universal_credit reads neither. Their docstrings note two gaps: Child Benefit reg. 8 also lists incapacity benefit and tax credits, and CTC reg. 5(4)(c) lists only the income-related and income-based allowances, where the own-right input also catches the contributory ones.

Invariants

  1. Over every combination of age, education, training, the other own-right benefits and the 16th-birthday fact, for a member who is not the claimant or partner: reporting Income Support of their own while Income Support runs ends the Pension Credit, Child Benefit and Child Tax Credit statuses; without a report each status equals the status with the report left alone; Universal Credit status never depends on it; and Pension Credit and Universal Credit agree without it.
  2. By default receives_income_support_in_own_right holds when the person reports Income Support of their own while Income Support runs (to 2025-26) and no reform neutralises it, and not otherwise.
  3. Under a reform that neutralises income_support, no Pension Credit, Child Tax Credit or Child Benefit status depends on reported Income Support. Under a reform that replaces income_support's formula with zero, no Pension Credit or Child Tax Credit status of a claimant or partner depends on it; another member's report and the Child Benefit reader still exclude, as their documentation says.
  4. Universal Credit status never depends on reported Income Support, in the baseline or under either reform.
  5. Setting a young person's reported Income Support to zero never lowers their household's Pension Credit.

Tests

  • Properties, test_qualifying_young_person_income_support_properties.py:
    • invariant 1 over 512 combinations (8 ages, every education value, training, own-right benefits, the 16th-birthday fact), each young person in a 70-year-old claimant's benefit unit, with and without a £3,000 report, and a check that there is a status to lose; the same grid with the report under a reform that neutralises income_support gives every programme's status without the report (invariant 3 for a member who is not the claimant or partner);
    • invariant 2 on Hypothesis-drawn ages, reported amounts and years from 2021 to 2028, and under neutralisation;
    • invariants 3 and 4 on Hypothesis-drawn households in 2016, 2019 and 2024, under each reform: a pensioner with a young person in their benefit unit or in the young person's own (where the young person claims, so the model calculates their Income Support), or a CTC claimant with a young person; the test compares each status with the same reformed simulation with every report set to zero;
    • invariants 4 and 5 on the same draws in the baseline;
    • the case above under both reforms (Pension Credit £7,121.20, the addition £3,424.20, household net income less the cost-of-living payment £15,219.10), and Child Benefit's status under each: still excluded under the zero-formula reform, a QYP once neutralised.
  • YAML, qualifying_young_person_income_support.yaml, eight cases hand-computed:
Case Facts Expected
1 2025-26; pensioner 70, 17-year-old at school, no Income Support every programme's QYP; Pension Credit first-child addition £78.10 × 52 = £4,061.20; Child Benefit £26.05 × 52 = £1,354.60
2 as 1, the 17-year-old reports £3,000 of Income Support not a Pension Credit or Child Benefit QYP, nor a legacy young person; addition and Child Benefit £0; still a Universal Credit QYP
3 2024-25; parent 40 with a CTC award, same 17-year-old on Income Support; not a CTC QYP; child element £0
4 as 3 without Income Support child element £3,455
5 2025-26; a 16-year-old school leaver reporting Income Support excluded from Pension Credit and Child Benefit; still a Universal Credit QYP
6 2026-27; a report of Income Support after it stopped paying does not count
7 2025-26; receives_income_support_in_own_right set directly not a Child Benefit QYP; still a Pension Credit and CTC QYP, which read is_on_income_support
8 2019; the case above, Income Support in payment, the cost-of-living payment set to zero student's Income Support £3,010.80; not a PC QYP; addition £0; Pension Credit £3,697; household net income £14,805.70 (State Pension £5,000 + PIP £58.70 × 52 = £3,052.40 + £3,697 + £3,010.80 + winter fuel payment £200 − TV licence £154.50)

Merges into this head

Each is its own commit.

Differential against 98a4739

A generated population of 400 households a year (pensioners with a young person in their benefit unit or in the young person's own, CTC claimants with a young person and sometimes a child, and young people alone; reported Income Support, income-related ESA and income-based JSA drawn for the young person), run on 98a4739 and 6db2b6d. Households whose Pension Credit, CTC or Child Benefit differ:

Year Differ Young person reports their own income-related ESA or income-based JSA Young claimant reports Income Support the model does not calculate
2016 12 11 1
2019 15 14 1
2024 9 9 0
  • ESA or JSA reported (34 households): at 6db2b6d the young person is not a Pension Credit QYP (Count 16-year-olds outside education as UC and Pension Credit qualifying young persons until the 1 September after their 16th birthday #2279's report block) or a Child Benefit QYP (Add Child Benefit qualifying young person routes for school leavers #2299's reg. 8 reports). Child Benefit falls by the young person's rate: £1,076.40 (eldest) or £712.40 (additional) in 2016 and 2019, £1,331.20 or £881.40 in 2024. In 10 households the young person belongs to a pensioner's benefit unit, and Pension Credit falls by the child addition: £3,319.68 in 2019 (5 households) and £3,993.08 in 2024 (5). In two of the 2024 ones (231 and 317) the pensioner receives PIP and the young person is 18, so the young person, no longer a QYP, counts as a resident under Schedule I para. 2 and the pensioner also loses the severe disability addition, £81.50 × 52 = £4,238: £8,231.08 in all. The model counts only residents aged 18 or over, so where the young person is 16 or 17 (2019 household 134, 2024 household 103) the addition stays.
  • Income Support reported, none calculated (2 households): an 18-year-old in their own benefit unit, at school, reporting Income Support. The model calculates no award for them (in 2016 their family claims Universal Credit), so at 6db2b6d they are a Pension Credit QYP, and the pensioner's Pension Credit rises by the severe disability addition: £8,091.20 to £11,307.40 in 2016 (£61.85 × 52 = £3,216.20) and £8,697 to £12,121.20 in 2019 (£3,424.20).
  • A third reason appears where the population includes 15-year-old carers who claim Carer's Allowance: 98a4739 pays them Carer's Allowance, which withholds the pensioner's severe disability addition, and 6db2b6d does not, so the pensioner gains it. That change comes from the commits merged in at 30fe1a8's first parent, not from this PR's reader. The table above does not count such households.
  • No household's CTC differs.

Mutation checks

At 4e1e017 (6db2b6d changes only the property file's formatting), each applied alone in a copy of the branch (qualifying_young_person_income_support.yaml, child_tax_credit_school_leaver_routes.yaml, qualifying_young_person_terminal_date.yaml, qualifying_young_person_reported_own_awards.yaml and three property files):

Mutation YAML cases that fail (of 65) Property tests that fail Result
The Income Support bar dropped from the CTC test this file's Case 3; CTC Cases 18 and 20 test_income_support_ends_three_statuses_and_not_universal_credit, test_reforms_that_remove_income_support_end_the_exclusions; in the CTC file test_income_support_ends_every_route, test_income_support_does_not_end_the_status_once_neutralised, test_family_members_in_any_benefit_unit killed
Pension Credit reads the report again (adds receives_income_support_in_own_right to its own-benefit block, so a claimant's report counts) this file's Case 7; #2279's "A claimant's report without a calculated award leaves the Pension Credit status available" test_a_reform_paying_no_income_support_restores_the_severe_disability_addition, test_reforms_that_remove_income_support_end_the_exclusions killed
The claimant branch dropped from the reader (is_on_income_support reads the report for everyone) #2279's "A claimant's report without a calculated award leaves the Pension Credit status available" test_a_reform_paying_no_income_support_restores_the_severe_disability_addition, test_reforms_that_remove_income_support_end_the_exclusions killed

test_qualifying_young_person_terminal_date_properties.py (8 tests) passes under all three.

At 0d934a8, is_on_income_support keeping another member's report under a reform that neutralises income_support ((not removed) dropped from own_award) fails test_income_support_ends_three_statuses_and_not_universal_credit (120 of 512 statuses differ from the unreported grid under neutralisation): killed. 05095a1 added that check.

Earlier mutations, at the commits named:

Mutation YAML cases Property tests
At c6c46e6: Pension Credit ignores Income Support 2, 5, 7 the grid
At c6c46e6: Child Benefit ignores Income Support 2, 5, 7 the grid
At c6c46e6: Child Tax Credit ignores Income Support 3 the grid
At c6c46e6: Universal Credit excludes for Income Support too 2, 5, 7 the grid
At c6c46e6: a report counts after Income Support stopped paying 6 the default test
At c6c46e6: receipt that ignores a reform removing Income Support test_no_receipt_once_a_reform_removes_income_support (removed at 4e1e017)

Run locally

The full suites run in CI.

Microsimulation impact

Aggregate impact report (CI)

The report runs main 040e9a5 against this PR merged into it (14701f0, head 0d934a8) on the certified Enhanced FRS 2024-25 (policyengine-uk-data 1.58.1), one full run per side for each year 2025-26 to 2031-32. Result: pass, 0 checks broken, 1 failing on both sides, 0 moves flagged. Child Benefit rises by £5.2m to £6.2m a year (about 1,000 more awards) and income tax by £1.8m to £2.2m. The report matches #2301's at 78d4d50 line for line, so the Child Benefit routes that #2301 lands make these moves, and the income tax comes from the High Income Child Benefit Charge on that Child Benefit, as #2301's local runs show; this PR's own change and #2300's move nothing. The standard reform costing "Raise child benefit by 25GBP/week per additional child" moves by −£1.5m to −£0.9m a year, and the other standard costings by £8 or less.

Local runs

These cover c6c46e6 only. Real runs on Enhanced FRS 2024-25 (uk-data 1.58.0, sha256 5ba399ea5ab9a6c7), one run each for 2024-25 to 2030-31, exact comparison of every record. Base: local merge 565b29c04 (on main e01f2f5 with #2279 and #2387). Branch: c6c46e6.

No record changes in any year. In 2024-25 and 2025-26, 60 records (about 36,000 weighted people) receive Income Support in their own right, and none of them is 16 to 19; no one aged 16 to 19 reports any Income Support (on the earlier base). From 2026-27 Income Support no longer runs. The change shows in household calculations (the tables above).

🤖 Generated with Claude Code

MaxGhenis and others added 30 commits October 10, 2026 19:52
…eavers

Child Benefit (General) Regulations 2006 regs 4, 5 and 7 were not modelled:
is_qualifying_young_person_for_child_benefit was reg. 3 (education or
training) alone, so a 16-year-old who had left school was not a qualifying
young person for Child Benefit, nor a young person for Housing Benefit, the
legacy benefits or Council Tax Reduction (SSCBA 1992 s.142).

- Reg. 4: until the 31 August after the 16th birthday, read from
  is_before_first_september_following_16th_birthday.
- Reg. 5: the 20-week extension period for a 16- or 17-year-old not in
  remunerative work, from the new input is_in_child_benefit_extension_period.
- Reg. 7: until the terminal date after leaving, from the new input
  is_before_child_benefit_terminal_date.
- Reg. 7(3): no Child Benefit for a young person who qualifies through
  reg. 7 alone and works 24 hours or more a week
  (is_child_benefit_payable_for_person); they stay a qualifying young person.

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

Child Tax Credit Regulations 2002 reg. 4 makes a person a qualifying young
person from their 16th birthday until the 31 August that next follows, with
no education condition, and reg. 5(3)(b) adds the 20 weeks after leaving for
an under-18 registered for work or training. The model had only the
education condition (reg. 5(3)(a), (ab)).

- Reg. 4 reads is_before_first_september_following_16th_birthday.
- Reg. 5(3)(b) reads the new input is_in_child_tax_credit_extension_period.
- Reg. 5(4)(a): a young person who has left education or training and works
  24 hours a week or more is not a qualifying young person through either.

Tax credits ended on 5 April 2025, so this changes years to 2024-25.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UC Regs 2013 reg. 5(1)(b) and SPC Regs 2002 reg. 4A(1)(b) keep a young
person in education or training a qualifying young person up to, but not
including, the 1 September following their 19th birthday.

- Universal Credit read the date from an input that defaulted to false, so
  a 19-year-old at school was not a qualifying young person in a household
  calculation, while the microdata sets the input true for every
  19-year-old in education.
- Pension Credit did not model the date at all.

Add is_before_first_september_following_19th_birthday, which counts a year
that starts before the date, as the 16th-birthday fact does.
is_before_universal_credit_qualifying_young_person_terminal_date now
follows it unless set, and Pension Credit's new terminal date condition
reads it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SPC Regs 2002 reg. 4A(5), Child Benefit (General) Regulations 2006
reg. 8(2)(a) and CTC Regs 2002 reg. 5(4)(c) each say a person receiving
income support is not a qualifying young person. The own-right benefits
input covers Universal Credit, Employment and Support Allowance and
Jobseeker's Allowance, which is Universal Credit's list (reg. 5(5)).

Add receives_income_support_in_own_right: the person's own reported Income
Support while Income Support is in payment (to 2025-26), settable directly.
It does not read the benefit unit's calculated award, which depends on who
the family's young persons are. The three programmes' qualifying young
person tests exclude it; Universal Credit's does not.

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

A disabled 16-year-old outside education is now a young person until the 31 August after their 16th birthday (Child Benefit reg. 4; HB reg 19(1)), so HB reg 28(6) counts them. Keep the original point, that the disability age extension does not itself create young-person status, as a second case with the reg. 4 period ended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-qyp-school-leaver-routes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nefit reg. 4 period

test_excluded_member_never_changes_is_eligibility adds a member aged 16 or over who is not in education, on the premise that they are not a qualifying young person. With reg. 4 a 16-year-old who has left education is one until the 31 August after their 16th birthday, so the generator now puts the member past that date, as #2279 does for the same reason in two other property tests.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p-terminal-date-19

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mutation that opened the leaver routes to a student who fails the
entry condition (reg. 5(3A)) passed every test: the grid fixed the entry
age at 17. The grid now also draws a course begun at 19, with the date
fact set directly, and asserts that such a student never qualifies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mutation that read a 19th birthday on 1 September as its own
"1 September following" was caught by the calendar property test only.
Case 11 pins the edge in YAML too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nto cb-qyp-school-leaver-routes

Brings in #2285's review round 2 head (212db88).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o ctc-qyp-school-leaver-routes

Brings in #2285's review round 2 head through the item 1 branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…to qyp-income-support-exclusion

Brings in the item 2 grid fix and #2285's review round 2 head.

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

Brings in the 1 September YAML case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in main with #2285 and #2288 merged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in main with #2285 and #2288 merged.

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

Brings in main with #2285 and #2288 merged.

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

Brings in main with #2285 and #2288 merged, through items 1 and 2.

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

Review round 1 of #2301: the formula took failing the non-advanced
education test as having left education, so a 16-year-old in full-time
advanced education working 24 hours a week lost the reg. 4 period. Reg.
5(4)(a) bars work only for a person "having ceased to receive full-time
education or approved training", and full-time education at a school or
college counts at any level (reg. 5(5)). Reg. 4(2) lifts the first
condition only. The 20 weeks of reg. 5(3)(b) are for a person who has
ceased. The property oracle had the same conflation and is fixed with it;
the grid now draws a course begun at 19, as its comment says. YAML Cases
15 and 16 pin the advanced student.

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

Review round 1 of #2300: setting the 19th-birthday fact for one person
gave everyone else in the situation false (the default), so another
19-year-old student silently lost the Pension Credit and Universal Credit
status. The formula gives true for everyone under 20 on 6 October, which
is everyone the age limit lets be a qualifying young person, so true is
now the default for the fact and for the two terminal-date variables that
read it. A partial setting then leaves everyone else where the formula
puts them. Tested from age alone for ages 0 to 25 and all months, and in a
mixed household (property test and YAML Case 12).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 1 of #2302: receives_income_support_in_own_right stayed true
under a reform that neutralizes income_support, so a young person's
report still excluded them from the Pension Credit, Child Benefit and CTC
qualifying young person status. It now also needs Income Support not to
be neutralized, as is_on_income_support already does for another member's
own claim. New test: the same student is excluded with Income Support and
qualifies for all three once a reform removes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Case 3's parent, not set, now takes the default (true): harmless, since
every reader is behind the qualifying young person age limit. The older
child_qualifying_young_person.yaml case left its after-terminal-date
person unset and relied on the old false default; it now sets the input
explicitly. The docstrings now say the default is the formula's value
under 20 and that older people are excluded before it is read.

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

Brings in #2387 (only the claimant's and partner's income in the legacy
means tests), which this PR needs, and main e01f2f5 with it. The one
conflict, the means-test property docstring, keeps both changes.

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

Copy link
Copy Markdown
Collaborator Author

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


Generated by Claude Code

The fragment said Child Benefit "is not paid" for a young person who
qualifies only through the terminal date and works 24 hours or more a
week. It now says the model pays none (reg. 7(3)).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H3DAwwvSTEwBJa5rD7bHvZ
No conflicts. main now holds #2279's head c5b9850 (merged as
4499481) and #2855, which starts the cost-of-living payments in 2022.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H3DAwwvSTEwBJa5rD7bHvZ
No conflicts. main now holds #2279's head c5b9850 (merged as
4499481) and #2855, which starts the cost-of-living payments in 2022.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H3DAwwvSTEwBJa5rD7bHvZ
No conflicts. It brings the Child Benefit changelog wording (dd8cd3e)
and main 95dfc62 (f5fd079).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H3DAwwvSTEwBJa5rD7bHvZ
No conflicts, and no change to the tree: this branch already holds
ee0482e (merged at f7e5fde) and main 95dfc62 (through #2301's head).

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

Copy link
Copy Markdown
Collaborator Author

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


Generated by Claude Code

No conflicts.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6HKeDniUizUaZcFw4Q
is_on_income_support gives the benefit unit's calculated award only to
whichever of the claimant and partner receives it, so the fragment says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6HKeDniUizUaZcFw4Q
It brings main 47a2817. No conflicts.

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

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

Copy link
Copy Markdown
Collaborator Author

PR_2301_TITLE.txt

Add CB and CTC qualifying young person routes for school leavers

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

PR_2301_BODY.md

Fixes #2290
Fixes #2287

Reproducer: policyengine_uk/tests/test_child_benefit_school_leaver_routes_properties.py::test_own_income_support_ends_the_status_on_every_route, policyengine_uk/tests/test_child_tax_credit_school_leaver_routes_properties.py::test_income_support_ends_every_route

axiom: Child Benefit (General) Regulations 2006 regs 2, 4, 5, 7 and 8 TheAxiomFoundation/rulespec-uk#356 queued ([addendum](https://github.com/TheAxiomFoundation/rulespec-uk/issues/356#issuecomment-6103738864), which states the blocker: the rows sit in an unreleased ingest, axiom-corpus#765, and a billed encoder run needs approval); CTC Regs 2002 reg 4, reg 5(3)(b), reg 5(4)(a) and reg 5(4)(c) TheAxiomFoundation/rulespec-uk#361 queued ([addendum](https://github.com/TheAxiomFoundation/rulespec-uk/issues/361#issuecomment-6103743610), whose Case J covers reg 5(4)(c) and which states the blocker: rulespec-uk's corpus pin holds reg 5's row and no row for reg 4, and a billed encoder run needs approval)

**Stack.** The merge order is #2300, #2301, then #2302. This PR lands the Child Benefit school-leaver commits from the closed #2299 as well as its own Child Tax Credit (CTC) commits. #2302 carries this PR and #2300, so landing #2302 lands both. #2279 and #2387 have merged into `main`, and 78d4d5070 merges `main` 47a2817e2 into this branch. This PR's own commits:

```bash
git log -p --no-merges origin/ctc-qyp-school-leaver-routes ^origin/main
```

## Summary

Two qualifying young person (QYP) tests modelled only the education and training condition: Child Benefit's (Child Benefit (General) Regulations 2006 reg. 3) and CTC's (Child Tax Credit Regulations 2002 reg. 5(3)(a) and (ab)). A young person who had left education was never a QYP for either. Through SSCBA 1992 s.142, the Child Benefit QYP is also the "young person" for Housing Benefit, Income Support, income-based JSA, income-related ESA and Council Tax Reduction in England and Wales.

**Child Benefit.** This PR adds the three conditions for young people who have left:

| Regulation | Who | How the model reads it |
|---|---|---|
| 4 | under 17, left education, until the 31 August after the 16th birthday | `is_before_first_september_following_16th_birthday` (from #2279) |
| 5 | under 18, registered with the careers service, not in remunerative work, for a 20-week extension period the responsible person asked for | new input `is_in_child_benefit_extension_period`, false by default |
| 7 | under 20, from leaving until the terminal date (end of February, May, August or November) | new input `is_before_child_benefit_terminal_date`, false by default |

Reg. 7(3) pays no Child Benefit "by virtue of this regulation" in a week of remunerative work. The model applies it to the Child Benefit amount only, so the young person stays a QYP for the legacy schemes.

The Child Benefit test also reads reg. 8, on every route, including the education route `main` already modelled (reg. 8(1) "applies in the case of a person who has not attained the age of 20", whatever their route). It reads the young person's own reported Universal Credit, Employment and Support Allowance of either kind and income-based Jobseeker's Allowance (reg. 8(2)(b), (e), (f)), as #2279's Universal Credit and Pension Credit tests do, and their own reported Income Support while DWP pays it (reg. 8(2)(a)). Without the first reports, reg. 4 made a 16-year-old outside education who reports their own income-related ESA or income-based JSA a Child Benefit young person, and so a legacy young person in the family's Council Tax Reduction. Without the Income Support limb, in 2018 a 16-year-old school leaver who cares for a parent on PIP and reports their own Income Support got the family **£1,076.40** of Child Benefit (£20.70 × 52) through the reg. 4 route; HMRC pays **£0** under reg. 8(2)(a), and the model now agrees: YAML Case 19, with Case 20 as its control.

**Child Tax Credit.** This PR adds two of the rules that make a young person who has left education a QYP:

| Rule | Who | How the model reads it |
|---|---|---|
| Reg. 4 | from the 16th birthday until the 31 August that next follows, with no education condition | `is_before_first_september_following_16th_birthday` (from #2279) |
| Reg. 5(3)(b) | under 18, for 20 weeks after leaving, while registered for work or training with a qualifying body | new input `is_in_child_tax_credit_extension_period`, false by default |

The second condition (reg. 5(4)) applies to both, and to the education route. A young person who has left education or training and works 24 hours a week or more qualifies through neither (reg. 5(4)(a)), and a young person who receives Income Support qualifies through no route (reg. 5(4)(c)); reg. 4(2) keeps both for the 16th-birthday period.

**The CTC Income Support bar.** The CTC test reads `is_on_income_support` for reg. 5(4)(c): the benefit unit's calculated award, for whichever of the claimant and partner receives it, and the person's own reported award for any other member of the benefit unit while Income Support runs and no reform neutralises it. Without the bar, in 2018 a parent aged 40 on PIP daily living with an existing CTC claim, and their 16-year-old (born 15 January 2002) outside education and paid work, caring 35 hours a week and reporting their own Income Support, got **£3,325** of CTC (child element £2,780 + family element £545) through the reg. 4 route. HMRC pays **£0** under reg. 5(4)(c), and the model now agrees: CTC YAML Case 18.

Against its base (`main` 4d80b2941 with #2279 merged), 2026-27, the Child Benefit YAML cases below run as plain simulations on both (the base has no input for the reg. 5 and reg. 7 routes, so its run drops them). This comparison ran at an earlier head and does not cover adc6e7822 or later:

| Case | Variable | Base | This PR |
|---|---|---:|---:|
| 1: parent 40, 16-year-old who has left school | young person's `is_qualifying_young_person_for_child_benefit` | false | true |
| | `child_benefit_entitlement` | £0 | £1,406.60 |
| | `is_lone_parent` | false | true |
| 7: 17-year-old in an extension period | `child_benefit_entitlement` | £0 | £1,406.60 |
| 10: 18-year-old before the terminal date, 23 hours | `child_benefit_entitlement` | £0 | £1,406.60 |
| 11: as 10, 24 hours | young person's QYP status | false | true |
| | `child_benefit_entitlement` | £0 | £0 (reg. 7(3)) |

Against #2299's branch at 2575e9e11 (which carries `main` 4d80b2941 and #2279), 2024-25, the CTC YAML cases run as plain simulations on both. This comparison ran before the Income Support bar and before `main`'s eligibility gate on the child element (#2631), which the cases now satisfy with a reported award:

| Case | Variable | Base | This PR |
|---|---|---:|---:|
| 1: parent 40, 16-year-old who has left school | young person's `is_qualifying_young_person_for_child_tax_credit` | false | true |
| | `CTC_child_element` | £0 | £3,455 |
| 2: as 1, working 24 hours | young person's CTC QYP status | false | false (reg. 5(4)(a)) |
| | young person's Child Benefit QYP status | true | true |
| 11: 17-year-old in the 20 weeks | `CTC_child_element` | £0 | £3,455 |

The CTC change affects years up to 2024-25: [tax credits ended on 5 April 2025](https://www.gov.uk/child-tax-credit). This PR follows up #2279. #2285, in `main` since 11 October, stops the model paying Child Benefit for a young person who heads their own benefit unit, and stops such a young person counting as their own QYP.

## The law

[Child Benefit (General) Regulations 2006](https://www.legislation.gov.uk/uksi/2006/223):

- [Reg. 2(2)](https://www.legislation.gov.uk/uksi/2006/223/regulation/2): "Where more than one of those regulations apply to a person, he is a qualifying young person until the last of them ceases to be satisfied."
- [Reg. 4](https://www.legislation.gov.uk/uksi/2006/223/regulation/4): "(1) This regulation applies in the case of a person who has not attained the age of 17 and who has left relevant education or training. (2) Subject to paragraph (3), the condition is that the 31st August next following the person's 16th birthday has not passed. (3) In the case of a person who attains the age of 16 on 31 August in any year, the condition is that the 1st September immediately following has not passed."
- [Reg. 5](https://www.legislation.gov.uk/uksi/2006/223/regulation/5): "(1) This regulation applies in the case of a person who has not attained the age of 18. (2) The condition is that— (a) the person has ceased to be in education or training; (b) the person is registered for work, education or for training with a qualifying body; (c) the person is not engaged in remunerative work; (d) the extension period which applies in the case of that person has not expired; (e) immediately before the extension period begins, the person who is responsible for him is entitled to child benefit in respect of him without regard to this regulation; and (f) the individual who is responsible for that person, within three months of that person's ceasing education or training, has made a request for the payment of child benefit during the extension period [...]". The period "ends 20 weeks after it started" (reg. 5(3)).
- [Reg. 7](https://www.legislation.gov.uk/uksi/2006/223/regulation/7): "(1) This regulation applies in the case of a person who has not attained the age of 20. (2) The condition is that the period found in accordance with Cases 1 and 2 has not expired in that person's case". Case 1 runs "from the date on which he ceases to receive relevant education or approved training, up to and including— (a) the week including the terminal date", which is "(a) the last day in February, (b) the last day in May, (c) the last day in August, (d) the last day in November, whichever first occurs after the date on which the person's relevant education or approved training ceased". "(3) Child benefit is not payable in respect of a qualifying young person by virtue of this regulation for any week in which he is engaged in remunerative work."
- [Reg. 1(3)](https://www.legislation.gov.uk/uksi/2006/223/regulation/1): "“remunerative work” means work of not less than 24 hours a week— (a) in respect of which payment is made; or (b) which is done in expectation of payment".
- [Reg. 8(2)](https://www.legislation.gov.uk/uksi/2006/223/regulation/8): "The condition is that the person is not in receipt, in a week, of— (a) income support, (b) income-based jobseeker's allowance [...], (c) incapacity benefit [...], (d) tax credit under the Tax Credits Act 2002, [...] (e) employment and support allowance payable under Part 1 of the Welfare Reform Act 2007 [...] (f) universal credit under Part 1 of the Welfare Reform Act 2012".
- [HB Regs 2006 reg. 19(1)](https://www.legislation.gov.uk/uksi/2006/213/regulation/19): a young person is "a person who falls within the definition of qualifying young person in section 142 of the Act". [IS Regs 1987 reg. 14(1)](https://www.legislation.gov.uk/uksi/1987/1967/regulation/14) says the same of "section 142 of the Contributions and Benefits Act".

[Child Tax Credit Regulations 2002](https://www.legislation.gov.uk/uksi/2002/2007):

- [Reg. 4](https://www.legislation.gov.uk/uksi/2002/2007/regulation/4): "(1) Subject to paragraph (1A), a person who attains the age of sixteen is a qualifying young person from the date on which that person attained that age until 31st August which next follows that date. (1A) A person who attains the age of sixteen on 31st August is a qualifying young person from the date on which that person attained that age. (2) Paragraph (1) is subject to regulation 5 but as if there were no requirement to satisfy the first condition specified in paragraph (3) of that regulation. (2A) Paragraph (1A) is subject to regulation 5."
- [Reg. 5(3)(b)](https://www.legislation.gov.uk/uksi/2002/2007/regulation/5): the first condition is also met by a person "under the age of eighteen years and— (i) he ceased to receive full-time education or to undertake approved training (the date of that event being referred to as “the relevant leaving date”); (ii) within 3 months of the relevant leaving date, he has notified the Board [...] that he is registered for work or training with a qualifying body, and (iii) not more than 20 weeks has elapsed since the relevant leaving date."
- Reg. 5(4): "The second condition is that the period in question is not (and does not include)— (a) a week in which he (having ceased to receive full-time education or approved training) becomes engaged in remunerative work [...] (c) a period in respect of which that person receives income support, income-related employment and support allowance payable under Part 1 of the Welfare Reform Act 2007, income-based jobseeker’s allowance within the meaning of section 1(4) of the Jobseekers Act 1995 or universal credit under Part 1 of the Welfare Reform Act 2012."
- [Reg. 2(1)](https://www.legislation.gov.uk/uksi/2002/2007/regulation/2): "“remunerative work” means work which is— (a) done for payment or in expectation of payment, (b) undertaken for not less than 24 hours a week [...]".

HMRC's [TCTM02220](https://www.gov.uk/hmrc-internal-manuals/tax-credits-technical-manual/tctm02220), which the entry-condition variable already cites, says: "There is no requirement for the qualifying young person to satisfy the condition of being in full-time non-advanced education or approved training up to and including 31 August following their 16th birthday. However, during this period, they have to satisfy the other conditions relating to qualifying young people."

## Change

### Child Benefit

- `is_qualifying_young_person_for_child_benefit`: aged 16 to 19, not receiving benefits in their own right (reg. 8), and any of: education or training begun before 19 (reg. 3, unchanged); the reg. 4 date fact; the reg. 5 extension condition; the reg. 7 input. For reg. 8 it reads the own-right input, the person's own reported Universal Credit, Employment and Support Allowance of either kind and income-based Jobseeker's Allowance (reg. 8(2)(b), (e), (f)), and `receives_income_support_in_own_right` (reg. 8(2)(a)), on every route. A positive report for the year ends the status for the whole year, where reg. 8(2) tests receipt "in a week".
- New `receives_income_support_in_own_right` (Person, year): the person's own reported Income Support (`income_support_reported` above zero) while Income Support pays awards (`gov.dwp.income_support.active`, true to 31 March 2026) and no reform neutralises `income_support`. A dataset or situation can set it directly. The Child Benefit test reads the report, not `is_on_income_support`: the family's Income Support reads the Child Benefit test through the legacy young person, so the calculated award would make a cycle (below).
- New `meets_qualifying_young_person_extension_period_condition_for_child_benefit`: under 18, the extension input, and not in remunerative work (reg. 5(1), (2)(c)).
- New `is_in_remunerative_work_for_child_benefit`: average weekly hours of 24 or more, with a new parameter `remunerative_work_hours`.
- New `is_child_benefit_payable_for_person`: the unit's child or QYP, unless they qualify through reg. 7 alone and do remunerative work (reg. 7(3) with reg. 2(2)). `child_benefit_child_index` and `child_benefit_respective_amount` read it, so a barred young person does not take the eldest-child rate.
- This PR leaves the legacy flag `is_child_or_young_person_for_legacy_benefits` as it was. It follows the Child Benefit QYP, so Housing Benefit, the legacy benefits, lone-parent status and Council Tax Reduction pick the routes and the reg. 8 exclusions up.

**Why Child Benefit reads the report.** In fresh simulations in 2016, 2019 and 2024 of a parent claiming Income Support with a 17-year-old at school reporting their own, reading `is_on_income_support` from the Child Benefit test raised a `CycleError` on calculating `child_benefit` first:

```
child_benefit → child_benefit_entitlement → child_benefit_respective_amount
  → is_child_benefit_payable_for_person → is_qualifying_young_person_for_child_benefit
  → is_on_income_support → income_support → income_support_entitlement
  → income_support_applicable_amount
  → is_responsible_for_child_or_young_person_for_legacy_benefits
  → is_child_or_young_person_for_legacy_benefits
  → is_child_or_qualifying_young_person_for_child_benefit
  → is_qualifying_young_person_for_child_benefit
```

At 78d4d5070, with the test reading `receives_income_support_in_own_right`, `child_benefit`, `household_net_income`, `income_support` and `is_qualifying_young_person_for_child_benefit`, each calculated first and alone in a fresh simulation, raised no `CycleError` in 2016, 2018, 2019 or 2024, for two households: YAML Case 19's family, and a lone parent claiming Income Support for a 4-year-old with a 16-year-old school leaver who reports their own Income Support (32 calculations).

**What a reform must do.** A reform that sets `gov.dwp.income_support.active` to false, or neutralises `income_support`, ends the Child Benefit exclusion for everyone. A reform that only replaces `income_support`'s formula with one returning zero leaves a reported award standing, so the Child Benefit exclusion stays.

Not modelled: reg. 6 (interruptions), reg. 7 Case 2 beyond the input, and the incapacity benefit and tax credit limbs of reg. 8.

### Child Tax Credit

`is_qualifying_young_person_for_child_tax_credit` is now: aged 16 to 19, not receiving benefits in their own right and not on Income Support (reg. 5(4)(c)), and any of
- in education or training begun before 19 (reg. 5(3)(a), (ab), (3A); unchanged);
- before the 31 August after the 16th birthday (reg. 4), unless the person has ceased full-time education or approved training and does remunerative work (reg. 4(2) lifts the first condition only; reg. 5(4)(a));
- under 18, having ceased, not in remunerative work, in the 20 weeks (reg. 5(3)(b), 5(4)(a)).

New: `is_in_child_tax_credit_extension_period` (input), `is_in_remunerative_work_for_child_tax_credit` (average weekly hours of 24 or more), and the parameters `remunerative_work_hours` and `extension_period_age_limit`.

**The Income Support reader.** The CTC test reads `is_on_income_support`, which the Housing Benefit and Council Tax Reduction non-dependant deductions and #2279's Pension Credit test already read. In fresh simulations in 2016, 2018, 2019 and 2024, calculating `household_net_income`, `child_tax_credit`, `income_support` and `is_qualifying_young_person_for_child_tax_credit` first, one at a time, raised no `CycleError` for three households: the family above with the parent also claiming Income Support, a 17-year-old lone parent claiming Income Support and CTC for their own benefit unit, and a pensioner with an 18-year-old student-carer in their own unit. `is_on_income_support` gives the benefit unit's calculated award to whichever of the claimant and partner receives it, so a reform that replaces Income Support's formula with zero ends the bar for that person. For any other member the reform must set `gov.dwp.income_support.active` to false, neutralise `income_support` or set their reported award to zero, as `is_on_income_support` documents.

**The other own-benefit gate.** The CTC test also reads `receives_benefits_in_own_right`, an input that covers Universal Credit, Employment and Support Allowance and Jobseeker's Allowance. Reg. 5(4)(c) lists Universal Credit and only income-related ESA and income-based JSA, so the input also catches contributory ESA and contribution-based JSA, which the regulation does not list. Unlike the Universal Credit, Pension Credit and Child Benefit tests, this test does not read the person's own reported awards, so a young person who reports their own Universal Credit, income-related ESA or income-based JSA still counts for CTC unless the input is set. This PR does not change that gate.

**Having ceased.** Reg. 5(4)(a) applies to a person "having ceased to receive full-time education or approved training", so a student's hours do not matter (CTC YAML Case 4). Full-time education at a school or college counts at any level (reg. 5(5)): a 16-year-old in full-time advanced education has not ceased it, so can work 24 hours and keep the reg. 4 period (Case 15), although advanced education fails the first condition. An apprentice still on a course has not ceased either (Case 5): reg. 5(3)(a)(ii) leaves education received by virtue of employment out of the first condition, which reg. 4(2) lifts, and reg. 5(5) counts a course at a school or college without that exclusion. An apprentice who has left every course has ceased (Case 17). Reg. 5(4)(a) does not say whether an employment-linked course counts as "full-time education", so the model applies its words as they stand. The reading affects 2024-25 and earlier only, since tax credits ended in April 2025.

Not modelled: interruptions (reg. 5(7)); the Working Tax Credit rules reg. 2(1) points to for counting hours.

## The annual approximation

**Child Benefit reg. 4** ends on 31 August (1 September for a 16th birthday on 31 August). Universal Credit's route in #2279 ends "up to, but not including" 1 September. Both dates fall in the same fiscal year, so a year that starts (6 April) before one starts before the other, and reg. 4 reads #2279's fact unchanged. As there, a year counts if any of it falls in the period: every 16-year-old (age on 6 October), and a 17-year-old born from 1 September to 6 October. #2279 sets out the bias of that rule; this PR follows it so that Child Benefit, Universal Credit and Pension Credit agree on a 16-year-old.

**CTC reg. 4** reads the same fact. Reg. 4(1A) gives a 16th birthday on 31 August no period without education. It stops the period running to the next year's 31 August. The date fact already ends that person's period in the year of the birthday, so the formula needs no separate exclusion. That year still counts, as the one in which they are a child until 30 August (CTC YAML Case 8), and the year after does not (Case 7).

## Invariants

**Child Benefit** (`test_child_benefit_school_leaver_routes_properties.py`, which numbers 1 to 7; 8 comes from #2279):

1. Reg. 4 against calendar arithmetic that shares no code with the model: a person outside education counts as a QYP for a year when, and only when, they are 16 to 19 on 6 October and the year starts on or before the reg. 4 end date, for every date of birth.
2. Over every combination of age, education, training, own-right benefits, the three leaver facts and hours, the status is the union of the four conditions within ages 16 to 19 and without own-right benefits. So each condition ends at its age limit, own-right benefits exclude every condition, and with the leaver facts off the status is reg. 3's alone, as before.
3. Switching a leaver fact on never removes the status.
4. Child Benefit is payable for the unit's child or QYP unless they qualify through reg. 7 alone and do remunerative work; they stay a QYP and a legacy young person.
5. Child Benefit entitlement is the eldest payable member at the eldest rate and each other at the additional rate, for 52 weeks.
6. Reg. 8(2)(a): in a year when DWP pays Income Support, a person who reports Income Support of their own is never a QYP, on any route, whoever else in the family claims it.
7. Without a report, setting `receives_income_support_in_own_right` false as an input leaves the status unchanged; with it set false, a report changes nothing, so the status is the one without the report.
8. A member who reports their own income-related ESA or income-based JSA leaves every Universal Credit, Pension Credit and legacy reader of a young single claimant's family unchanged (#2279's `test_sixteen_year_old_own_award_leaves_young_single_readers_unchanged`).

**CTC** (`test_child_tax_credit_school_leaver_routes_properties.py`):

1. Reg. 4 against calendar arithmetic that shares no code with the model: a person outside education, not working, counts as a QYP for a year when, and only when, they are 16 to 19 on 6 October and some of the year falls on or before the last day they are a child or in the reg. 4 period (30 August for a 16th birthday on 31 August).
2. Over every combination of age, education, training, apprenticeship, own-right benefits, the age the course began, the two leaver facts and hours: the status is the education condition; or the reg. 4 period, unless the person has ceased education or training and does remunerative work; or the 20 weeks for a person under 18 who has ceased and does no remunerative work; within ages 16 to 19 and without own-right benefits. So remunerative work ends both routes for a person who has ceased and never a student's status, an advanced student keeps the reg. 4 period whatever their hours, the 20 weeks end at 18 and never reach a student whose course began at 19, and with the leaver facts off the status is the education condition's alone, as before.
3. Switching a leaver fact on never removes the status.
4. The benefit unit's child or QYP is a child or QYP who is not its claimant or partner and not looked after.
5. Reg. 5(4)(c), which reg. 4(2) keeps for the reg. 4 period: a member who is not the claimant or partner and reports Income Support of their own while Income Support runs is never a QYP, on any route; once Income Support has stopped paying (from April 2026) or a reform neutralises it, the report does not change the status.

## Tests

### Child Benefit

- **Properties**, new `test_child_benefit_school_leaver_routes_properties.py`:
  - invariant 1 for every date of birth from 2006 to 2011 (2,191 people, ages 14 to 20 on 6 October 2026), with the 31 August, 1 September and 6/7 October edges asserted, and again on Hypothesis-drawn dates of birth and years;
  - invariants 2 to 4 over 6,144 combinations (8 ages, every education value, training, own-right benefits, hours of 0, 23 and 24, and the 8 combinations of the three facts);
  - invariant 5 on generated benefit units, from the inputs and the weekly rates alone;
  - invariants 6 and 7 on Hypothesis-drawn families in 2016, 2019 and 2024 (`test_own_income_support_ends_the_status_on_every_route`): parents who may claim Income Support as carers or lone parents, and young people and children with every education value, the three leaver facts, hours, the other own-award reports and their own Income Support. Every draw also holds YAML Case 19's family, so each example has a status to lose, and the test calculates the family's `income_support` first.
- **Oracles.** `test_child_and_adult_definitions_properties.py`: Child Benefit and CTC join Universal Credit and Pension Credit in the 16-year-old route (invariant 5 there; #2279's supplied-roles invariant is 6). `test_benefit_cap_specified_benefits_properties.py`: the reference oracle counts the 16-year-old school-leaver role as a young person for both schemes, keeps #2279's Universal Credit filter for an older young person who reports their own award, and, for Housing Benefit, drops a young person who reports their own Universal Credit, ESA or income-based JSA, as the Child Benefit test does (HB Regs 2006 reg. 19).
- **YAML**, new `child_benefit_school_leaver_routes.yaml`, 28 cases hand-computed (2026-27 unless the row says otherwise; eldest rate £27.05 × 52 = £1,406.60; in 2018-19, £20.70 × 52 = £1,076.40). Cases 19 to 24 and 26 set the cost-of-living payment, which DWP first paid in 2022, to zero, and no family claims tax credits or Universal Credit:

| Case | Facts | Expected |
|---|---|---|
| 1 | parent 40, 16-year-old who has left school | QYP; Child Benefit **£1,406.60**; legacy young person; lone parent |
| 2 | 16-year-old apprentice, 30 hours | QYP and paid: reg. 4 has no education or work condition |
| 3 | born 15 September 2009 (17 on 6 October) | the period runs to 31 August 2026: QYP |
| 4 | born 31 August 2009 | the period ended on 1 September 2025: not a QYP |
| 5 | 17 and 18, left school | neither is a QYP by default |
| 6 | 16, receiving a benefit in own right | not a QYP (reg. 8) |
| 7 | 17, in an extension period | QYP (reg. 5) |
| 8 | as 7, working 24 hours | not a QYP (reg. 5(2)(c)) |
| 9 | 18, extension input set | not a QYP (reg. 5(1)) |
| 10 | 18, before the terminal date, 23 hours | QYP and paid (reg. 7) |
| 11 | as 10, 24 hours, earning £20,000 | still a QYP and a legacy young person; Child Benefit **£0** (reg. 7(3)); the parent's HB, IS and CTR applicable income **£0**: the young person's earnings are not the parent's (#2387) |
| 12 | 16, before the terminal date, 40 hours | paid: reg. 4 also applies (reg. 2(2)) |
| 13 | parent, working 18-year-old before the terminal date, child of 10 | the child of 10 is the eldest paid child: **£1,406.60** |
| 14 | 20, terminal input set | not a QYP |
| 15 | two 16-year-olds, one at school, one left | (£27.05 + £17.90) × 52 = **£2,337.40** |
| 16 | lone 16-year-old who has left school | a QYP by status, not their own |
| 17 | 16, left school, reports £5,000 of their own income-related ESA | not a QYP nor a legacy young person (reg. 8(2)(e)); Child Benefit **£0**; not a lone parent |
| 18 | as 17 with income-based JSA | the same (reg. 8(2)(b)) |
| 19 | 2018: parent 40 on PIP daily living; 16-year-old born 15 January 2002, left education, caring 35 hours, reporting £3,000 of their own Income Support | in the reg. 4 period, on Income Support, not a QYP nor a legacy young person (reg. 8(2)(a)); Child Benefit **£0**; household net income **£6,188.30** (PIP £57.30 × 52 = £2,979.60 + Carer's Allowance £64.60 × 52 = £3,359.20 − TV licence £150.50) |
| 20 | as 19 without the report | QYP; Child Benefit **£1,076.40**; household net income **£7,264.70**, £1,076.40 higher |
| 21 | 2018: as 19, aged 17 in an extension period | not a QYP (reg. 5, reg. 8(2)(a)); Child Benefit **£0**; household net income **£6,188.30** |
| 22 | 2018: as 19, aged 18 before the terminal date | not a QYP (reg. 7, reg. 8(2)(a)); Child Benefit **£0**; household net income **£6,188.30** |
| 23 | 2018: parent 40 on PIP daily living; 17-year-old at school reporting their own Income Support | meets reg. 3; not a QYP (reg. 8(2)(a)); Child Benefit **£0**; household net income **£2,829.10** (£2,979.60 − £150.50) |
| 24 | 2018: Case 19 under a reform that neutralises `income_support` | the report does not count; QYP; Child Benefit **£1,076.40**; household net income **£7,264.70** |
| 25 | 2026-27: Case 19's family, the 16-year-old born 15 January 2010 | `gov.dwp.income_support.active` reads false for the year (the model reads a year's parameters on 30 April, and DWP made the last payments on 31 March 2026), so the report does not count; QYP; Child Benefit **£1,406.60**; household net income **£9,810.40** (PIP £76.70 × 52 = £3,988.40 + Carer's Allowance £86.45 × 52 = £4,495.40 + £1,406.60 + Household Energy Payment £100 − TV licence £180) |
| 26 | 2018: lone parent 40 claiming Income Support, a child of 4, a 16-year-old school leaver with none of their own | the parent is on Income Support (£73.10 × 52 = **£3,801.20**); the 16-year-old stays a QYP; Child Benefit (£20.70 + £13.70) × 52 = **£1,788.80**; household net income **£5,439.50** |
| 27 | 17 at school, reporting £5,000 of their own contributory ESA | not a QYP (reg. 3 with reg. 8(2)(e)); Child Benefit **£0** |
| 28 | 16, left school, reporting £5,000 of contribution-based JSA | still a QYP: reg. 8(2)(b) lists income-based JSA only; **£1,406.60** |

- **Changed expectations from `main`'s benefit cap work.** Two cases in `benefit_cap_specified_benefits.yaml` asserted that a 16-year-old who has left education is not a Housing Benefit young person, so their DLA or armed forces independence payment does not lift the Housing Benefit cap; the exemption's docstring named #2287 as the gap. Under reg. 4 they are a Child Benefit qualifying young person, so a Housing Benefit young person (HB Regs 2006 reg. 19), and their benefit lifts the cap (reg. 75F(1)). Both cases now say so, as do the exemption's docstring, its inline comment, the single-claimant rate's documentation and `has_sixteen_year_old_dependant`'s docstring, which all named #2287 as the gap; Universal Credit's exemption stays the same.
- **Changed expectation.** `housing_benefit_applicable_income_childcare_element.yaml` asserted that a disabled 16-year-old outside education "is not a relevant young person". Reg. 4 makes them a young person until 31 August, so HB reg. 28(6) counts them (1 relevant child, £9,000). This PR splits the case in two: the default, and the same person after the reg. 4 period ends, which keeps the original point that the disability age extension does not itself create the status.

### CTC

- **Properties**, new `test_child_tax_credit_school_leaver_routes_properties.py`:
  - invariant 1 for every date of birth from 2004 to 2009 (2,192 people, ages 14 to 20 on 6 October 2024), with the 31 August, 1 September and 6/7 October edges asserted, and again on Hypothesis-drawn dates of birth and years from 2015 (before 2015, `child_tax_credit` and `income_support` already raise a missing-parameter error for Pension Credit's mixed-age couple date, and the status itself now raises there, since it reads `is_on_income_support`);
  - invariants 2 to 4 over 12,288 combinations (the age the course began is drawn too: 17, or a course begun at 19);
  - invariant 5 on every combination from that grid that qualifies without Income Support, with each route present, in 2018, 2024 and 2026 (`test_income_support_ends_every_route`), and under a reform that neutralises `income_support` (`test_income_support_does_not_end_the_status_once_neutralised`);
  - invariants 2, 4 and 5 on generated benefit units, where a claimant can be a QYP by status and other members report Income Support, in 2018, 2024 and 2026.
- **YAML**, new `child_tax_credit_school_leaver_routes.yaml`, 20 cases hand-computed. Cases 1 to 17 run in 2024-25 (child element £3,455) and give the parent a reported CTC award and the family no Universal Credit claim, which `main`'s gate on the child element (`is_CTC_eligible`) needs. Cases 18 to 20 run in 2018-19, and Cases 18 and 19 set the cost-of-living payment, which DWP first paid in 2022, to zero:

| Case | Facts | Expected |
|---|---|---|
| 1 | parent 40, 16-year-old who has left school | QYP; one child element, **£3,455** |
| 2 | as 1, working 24 hours | not a QYP (reg. 5(4)(a)); Child Benefit still counts them |
| 3 | as 1, working 23 hours | QYP |
| 4 | 16 at school, working 24 hours | QYP: the bar is for a person "having ceased" |
| 5 | 16-year-old apprentice still on a college course, 30 hours | QYP: not ceased education (reg. 5(4)(a), 5(5)); **£3,455** |
| 6 | born 15 September 2007 (17 on 6 October) | the period runs to 31 August 2024: QYP |
| 7 | born 31 August 2007 | no period to the next 31 August: not a QYP |
| 8 | born 31 August 2008 | a child until 30 August 2024: the year counts |
| 9 | 16, receiving a benefit in own right | not a QYP (reg. 5(4)(c)) |
| 10 | 17, left school | not a QYP by default |
| 11 | 17, in the 20 weeks | QYP (reg. 5(3)(b)) |
| 12 | as 11, working 24 hours | not a QYP |
| 13 | 18, extension input set | not a QYP |
| 14 | lone 16-year-old who has left school | a QYP by status, not their own |
| 15 | 16, full-time advanced education, 24 hours | QYP: reg. 4(2) lifts the first condition, and they have not ceased education (reg. 5(5)); **£3,455** |
| 16 | 17, the same student, outside the reg. 4 period | not a QYP; **£0** |
| 17 | 16, apprentice no longer on any course, 30 hours | not a QYP: ceased and in remunerative work; **£0** |
| 18 | 2018: parent 40 on PIP daily living with a CTC award; 16-year-old born 15 January 2002, outside education and work, caring 35 hours, reporting £3,000 of their own Income Support | in the reg. 4 period, on Income Support, not a CTC QYP (reg. 4(2), 5(4)(c)) nor a Child Benefit QYP (Child Benefit reg. 8(2)(a)); CTC **£0**; Child Benefit **£0**; household net income **£6,188.30** (PIP £57.30 × 52 = £2,979.60 + Carer's Allowance £64.60 × 52 = £3,359.20 − TV licence £150.50). The model calculates Income Support only for the claimant's family, so the 16-year-old's own £3,000 is in no income here |
| 19 | as 18 without Income Support | QYP until 31 August 2018 (reg. 4(1)); CTC £2,780 + £545 = **£3,325**; Child Benefit **£1,076.40**; household net income £6,188.30 + £1,076.40 + £3,325 = **£10,589.70** |
| 20 | 2018: 17 at school, reporting their own Income Support | meets the first condition; on Income Support, not a QYP (reg. 5(3)(a), 5(4)(c)); CTC **£0** |

## Merges into this head

Each is its own commit.
- adc6e7822 merges #2279's head c5b9850a3 (cde22d73c) into the Child Benefit line, and then `main` 52b30809f. The first had two conflicts, both in tests:
  - `test_benefit_cap_specified_benefits_properties.py`, `reference_specified_benefit`: the Child Benefit line counted the school-leaver role as a young person for both schemes; #2279 counted it for Universal Credit only and filtered out an older UC young person who reports their own award. The merged oracle does both.
  - `test_child_and_adult_definitions_properties.py`, the invariant list: invariant 5 keeps the Child Benefit reg. 4 sentence, and #2279's supplied-roles invariant stays 6.
- 9986aec09 merges adc6e7822 into the CTC line. One conflict, in the same invariant list: invariant 5 keeps the CTC reg. 4 sentence, and #2279's invariant stays 6. The merge brought `main`'s eligibility gate on `CTC_child_element` (#2631), which made CTC Cases 1, 3, 4, 5, 6, 8, 11 and 15 compute £0; e35595651 gives those cases an existing claim.
- 1eb9e075d merges a447fe760, the Child Benefit reg. 8 reports, without conflicts.
- ad5dc0bf0 merges 3eb64dfc4, the Child Benefit Income Support gate, its cases and property, and the corrected benefit cap oracle, without conflicts.
- f5fd07931 merges `main` 95dfc62f8, and 78d4d5070 merges `main` 47a2817e2, both without conflicts. `main` now holds #2279 and #2855, which stops the model copying the 2022 cost-of-living payment amounts back to 2015.

## Mutation checks

Each applied alone in a copy of the branch (YAML cases and property tests that fail).

**Child Benefit routes**, at an earlier head, before the merges:

| Mutation | YAML cases | Property tests |
|---|---|---|
| Reg. 4 dropped | 1, 2, 3, 15, 16 | both calendar tests; the union grid |
| Reg. 5 dropped | 7 | the union grid |
| Reg. 7 dropped | 10, 11 | the union grid |
| Reg. 5 without the remunerative work bar | 8 | the union grid |
| Reg. 5 without the under-18 limit | 9 | the union grid |
| Reg. 7(3) ignored (always payable) | 11, 13 | the union grid |
| Reg. 7(3) bars every working young person | 2, 12 | the union grid; the entitlement count |
| Remunerative work from 25 hours | 8, 11 | the union grid |

**Child Benefit reg. 8**:

| Mutation | YAML cases that fail | Property tests that fail |
|---|---|---|
| At a447fe760: the reports not read (the code at adc6e7822) | 17, 18 | `test_sixteen_year_old_own_award_leaves_young_single_readers_unchanged`, both parameters |
| At f5fd07931: the Income Support gate dropped | Child Benefit Cases 19, 21, 22, 23 (4 of 28); CTC Case 18 (1 of 20) | `test_own_income_support_ends_the_status_on_every_route` (1 of 5 in its file; `test_legacy_award_readers_properties.py` passes, 20 of 20) |
| At f5fd07931, M3: the reports exclude only off the education route | Child Benefit Case 27 (1 of 28) | none |
| At f5fd07931, M4: `esa_contrib_reported` dropped from the reports | Child Benefit Case 27 (1 of 28) | none |

For the last three rows the YAML set was `child_benefit_school_leaver_routes.yaml`, `qualifying_young_person_reported_own_awards.yaml`, `qualifying_young_person_no_education_route.yaml` and `child_tax_credit_school_leaver_routes.yaml`, and the property set `test_child_benefit_school_leaver_routes_properties.py` and `test_legacy_award_readers_properties.py`. f5fd07931's code matches 78d4d5070's in every file these mutations touch.

**CTC routes**, at earlier heads, before the formula took its current form:

| Mutation | YAML cases | Property tests |
|---|---|---|
| Reg. 4 dropped | 1, 3, 6, 8, 14 | all four |
| Reg. 5(3)(b) dropped | 11 | the grid; the generated units |
| No remunerative work bar for a leaver | 2, 5, 12 | the grid; the generated units |
| Remunerative work bars a student too | 4 | the grid; the generated units |
| The 20 weeks without the under-18 limit | 13 | the grid |
| Leaver routes open to a student whose course began at 19 | none | the grid |

At 33fa9eea6:

| Mutation | YAML cases | Property tests |
|---|---|---|
| Failing the non-advanced test counts as having ceased | 15 | the grid; the generated units |
| The 20 weeks open to a person who has not ceased | none | the grid |
| Remunerative work bars the reg. 4 period for everyone | 15 | the grid; the generated units |

**The CTC Income Support bar** at e5f239b3a: dropping it fails YAML Cases 18 and 20 (2 of 20) and three of six property tests (`test_income_support_ends_every_route`, `test_income_support_does_not_end_the_status_once_neutralised`, `test_family_members_in_any_benefit_unit`).

## Run locally

- At 78d4d5070: `policyengine-core test` on 55 YAML files (every test YAML file the branch changes against `main` 47a2817e2, and every baseline file that names `qualifying_young_person`, `income_support_reported` or `receives_income_support_in_own_right`), 981 passed; `pytest -n 4`, one file at a time: `test_benefit_cap_specified_benefits_properties.py` 46 passed, `test_child_and_adult_definitions_properties.py` 21, `test_child_benefit_school_leaver_routes_properties.py` 5, `test_child_tax_credit_school_leaver_routes_properties.py` 6, `test_income_support_eligibility_properties.py` 4, `test_legacy_award_readers_properties.py` 20, `test_means_test_income_properties.py` 2, `test_pension_credit_young_person_income_support.py` 10, `policyengine_uk/tests/code_health` 3,174. No skip, xfail or failure.
- At 78d4d5070: `uvx ruff@0.17.2 format --check .`, `ruff check .` and `ruff check --select F811,F821 policyengine_uk`: clean; `git diff --check` against `main` 47a2817e2: clean; `git merge-tree --write-tree` with `main` 47a2817e2: no conflict.
- Earlier runs, at the commits named: at 1eb9e075d, `policyengine-core test` on 29 YAML files, 477 passed, and the same property files, all passing, with `code_health` 3,103; at a447fe760, 28 YAML files, 457 passed; the whole stack at 62e93179a, `policyengine-core test` on the 248 YAML files that mention these programmes, 2,685 passed, and `pytest` on the 135 Python test files that mention them, 1,930 passed, 1 xfailed and 6 failed (four the benefit cap property oracle, fixed at 8a5980c99, and two a test generator on #2279, fixed there at 560005cfa).

The full suites run in CI.

## Microsimulation impact

The CI report ran at 1eb9e075d, before the Child Benefit Income Support gate (3eb64dfc4) and the `main` merges. The local runs used e626d3f6f (Child Benefit) and c6e041ebe (CTC) and do not cover adc6e7822 or later. The reports remove any young person aged 16 to 19 who reports their own Universal Credit, Employment and Support Allowance or income-based Jobseeker's Allowance, on every route, including the education route `main` already modelled (reg. 8(1)). The Income Support gate removes a young person who reports their own Income Support; the CTC local runs on uk-data 1.58.0 found no one aged 16 to 19 who reports any Income Support.

### Aggregate impact report (CI)

[The report](https://github.com/PolicyEngine/policyengine-uk/pull/2301#issuecomment-6331646298) runs `main` 52b30809f against this PR at 1eb9e075d merged into it (02217d6c14) on the certified Enhanced FRS 2024-25 (policyengine-uk-data 1.58.1), one full run per side for each year 2025-26 to 2031-32. Result: **pass**, 0 checks broken, 1 failing on both sides, 0 moves flagged. The CTC change moves nothing from 2025-26, the first year the report runs, as tax credits ended in April 2025, so the Child Benefit routes make every change here.

| £ million a year | 2025-26 | 2026-27 | 2027-28 | 2028-29 | 2029-30 | 2030-31 | 2031-32 |
|---|---:|---:|---:|---:|---:|---:|---:|
| Child Benefit | +5.2 | +5.4 | +5.6 | +5.8 | +5.9 | +6.1 | +6.2 |
| Income tax | +1.8 | +1.8 | +1.9 | +2.0 | +2.0 | +2.1 | +2.2 |
| Household net income | +3.4 | +3.6 | +3.7 | +3.8 | +3.9 | +4.0 | +4.0 |
| Government balance | −3.4 | −3.6 | −3.7 | −3.8 | −3.9 | −4.0 | −4.0 |

The Child Benefit caseload rises by 1.0k each year. People in absolute poverty before housing costs fall by 4.7k in 2025-26 and 2026-27 and by 4.5k a year from 2027-28; people in relative poverty before housing costs fall by 4.5k each year; people in relative poverty after housing costs do not change in 2025-26 or 2026-27 and fall by 4.5k a year from 2027-28.

The standard reform costing "Reduce Universal Credit taper rate to 20%" moves by −£1.6m to −£2.2m a year. #2279's own report shows the same line, so the move comes from #2279, which the report's base `main` 52b30809f did not yet hold.

### Local runs, Child Benefit

Real runs on Enhanced FRS 2024-25 (uk-data 1.58.0, sha256 `5ba399ea5ab9a6c7`), one run each for 2024-25 to 2030-31, exact comparison of every record. Base: `main` e01f2f5ec with #2279 (9d2fadbf5) and #2387 merged (a local merge, eeabc65e3), so the figures measure the Child Benefit routes alone. Branch: e626d3f6f, before the reg. 8 reports and the Income Support gate.

**Who changes.** In 2026-27, 102 records (203,321 weighted people) become Child Benefit qualifying young persons by status. Only 22 records (4,929 weighted) belong to a family whose claimant now counts them: in the microdata most 16-year-olds outside education sit alone in a benefit unit of their own, apart from their parents' (PolicyEngine/policyengine-uk-data#562), and since #2285 nobody is their own QYP.

**Aggregates** (£ million a year):

| Variable | Records that differ | 2024-25 | 2026-27 | 2030-31 |
|---|---|---:|---:|---:|
| `child_benefit` | 22 | +4.6 | +4.9 | +5.5 |
| `household_tax` | 4 (8 in 2030-31) | +1.5 | +1.6 | +1.8 |
| `household_net_income` | 22 | +3.1 | +3.3 | +3.6 |
| `housing_benefit_applicable_income` | 22 | +3.1 | +3.3 | +3.6 |
| `council_tax_reduction_applicable_income` | 22 | +3.1 | +3.3 | +3.6 |

The extra tax comes from the High Income Child Benefit Charge: in 2026-27 the change in `household_tax` equals the change in `CB_HITC`.

Per benefit unit in 2026-27: in 16 units both applicable incomes take the new Child Benefit (£930.80, the additional rate, or £1,406.60) in full. In 4 units the charge takes back all or nearly all of it, so the applicable incomes move by under £1 either way. In the 2 units whose parent becomes a lone parent, Council Tax Reduction income rises by the £1,406.60, and Housing Benefit income by £1,040 less, because Housing Benefit disregards £25 a week of a lone parent's earnings and £5 of a single claimant's. No saved Housing Benefit, Income Support, Council Tax Reduction, Universal Credit, tax credit or Pension Credit award output changes in any year. That does not mean none of these families receives those benefits: three affected households keep a positive reported Council Tax Reduction (£156.54 for one, £417.43 for two others in 2026-27), which `council_tax_benefit` keeps as reported where it does not simulate the claim (for example where the model does not support the scheme), so the comparison does not compute how those awards would respond. `is_lone_parent` changes for those 2 records (35 weighted).

**Winners and losers** (`household_net_income`, changes over 50p):

| Year | Gaining (records / weighted households) | Mean gain | Losing |
|---|---:|---:|---:|
| 2024-25 | 18 / 3,530 | £886 | 0 |
| 2026-27 | 20 / 4,274 | £781 | 0 |
| 2030-31 | 18 / 3,628 | £1,001 | 0 |

In 2026-27 the gaining households by decile of base household income: 1 (529 weighted), 2 (863), 3 (1,419), 4 (283), 6 (475) and 9 (706, each gaining £0.59 after the charge). People in poverty after housing costs fall by 3,899 and before housing costs by 196.

### Local run, CTC, 2024-25

The one year with tax credits in payment, on the certified file (uk-data 1.58.1, sha256 `056e80fd8e91`), exact comparison of every record. Base: the Child Benefit line at 4772258fb (with `main` a8b2848c1, #2279 and #2387), so the figures measure the CTC change alone. Branch: c6e041ebe, before the Income Support bar and `main`'s gate on the child element.

No household's income changes, and no tax credit award changes.

- 81 records (74,446 weighted people) become CTC qualifying young persons by status. 22 records (5,275 weighted), all aged 16 or 17, belong to a family whose claimant now counts them. As with Child Benefit, most sit alone in their own benefit unit (PolicyEngine/policyengine-uk-data#562).
- `CTC_child_element` rises for those 22 benefit units: +£18.2m. None of them claims tax credits (`is_CTC_eligible` false and `child_tax_credit` £0 before and after), so `child_tax_credit`, `tax_credits`, net income and poverty match in every record. `main`'s gate on the child element (#2631), which this PR now carries, sets the element to £0 for a family with no claim, so at the current head these units' child element does not rise either.
- `tax_credits_applicable_income` matches too: #2387 stops a young person's own income counting.

Earlier local runs on uk-data 1.58.0 (at 33fa9eea6, before the apprentice change) found the same pattern for 2024-25 to 2030-31: a higher child element and no award.

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

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

AXIOM_356.md

## Addendum: Child Benefit (General) Regulations 2006 reg. 8(2)(a), Income Support in the young person's own right

PolicyEngine/policyengine-uk#2301 now lands the Child Benefit school-leaver routes (regs 4, 5 and 7) from the closed #2299, with the Income Support limb of reg. 8 on every route. The text it follows, as #2301's body quotes it:

> [Reg. 8(2)](https://www.legislation.gov.uk/uksi/2006/223/regulation/8): "The condition is that the person is not in receipt, in a week, of— (a) income support, (b) income-based jobseeker's allowance [...], (c) incapacity benefit [...], (d) tax credit under the Tax Credits Act 2002, [...] (e) employment and support allowance payable under Part 1 of the Welfare Reform Act 2007 [...] (f) universal credit under Part 1 of the Welfare Reform Act 2012".

Reg. 8 applies to every route to qualifying young person status, including education (reg. 3), not only the leaver routes (regs 4, 5 and 7).

**How PolicyEngine reads it.** `receives_income_support_in_own_right` holds when the person reports Income Support of their own (`income_support_reported` above zero) while DWP pays Income Support (`gov.dwp.income_support.active`, true to 31 March 2026; PolicyEngine reads a fiscal year's parameters on 30 April, so the parameter reads false from 2026-27) and no reform neutralises `income_support`. A positive report for the year ends the status for the whole year, where reg. 8(2) tests receipt "in a week". The test reads the report, not the calculated award, because the family's Income Support depends on which young people count for Child Benefit.

### Parity cases

Each case comes from `policyengine_uk/tests/policy/baseline/gov/hmrc/child_benefit/child_benefit_school_leaver_routes.yaml` at 78d4d5070. Unless the row says otherwise, the year is 2018-19, a parent aged 40 on PIP daily living (standard rate, £57.30 a week) claims for a benefit unit with no Universal Credit or tax credit claim, the young person (born 15 January 2002 when 16) has left education and cares for the parent 35 hours a week, and the cost-of-living payment, which DWP first paid in 2022, is set to zero. Child Benefit's eldest rate in 2018-19 is £20.70 a week (£1,076.40 a year), Carer's Allowance £64.60 a week and the colour TV licence £150.50.

| Case | Facts | Young person a QYP | Child Benefit | Household net income |
|---|---|---|---:|---:|
| 19 | 16-year-old in the reg. 4 period, reports £3,000 of their own Income Support | no (reg. 8(2)(a)) | **£0** | **£6,188.30** = PIP £2,979.60 + Carer's Allowance £3,359.20 − TV licence £150.50 |
| 20 | as 19, no report | yes (reg. 4) | **£1,076.40** | **£7,264.70**, £1,076.40 more than Case 19 |
| 21 | 17-year-old in an extension period, born 15 January 2001, reports Income Support | no (reg. 5 with reg. 8(2)(a)) | **£0** | **£6,188.30** |
| 22 | 18-year-old before the terminal date, born 15 January 2000, reports Income Support | no (reg. 7 with reg. 8(2)(a)) | **£0** | **£6,188.30** |
| 23 | 17-year-old at school, born 15 January 2001, not caring, reports Income Support | no (reg. 3 with reg. 8(2)(a)) | **£0** | **£2,829.10** = £2,979.60 − £150.50 |
| 24 | Case 19 under a reform that neutralises `income_support` | yes | **£1,076.40** | **£7,264.70** |
| 25 | 2026-27: Case 19's family, the young person born 15 January 2010; DWP made the last Income Support payments on 31 March 2026 | yes: the report does not count | **£1,406.60** (£27.05 × 52) | **£9,810.40** = PIP £76.70 × 52 = £3,988.40 + Carer's Allowance £86.45 × 52 = £4,495.40 + £1,406.60 + Household Energy Payment £100 − TV licence £180 |
| 26 | a lone parent aged 40 with no PIP, claiming Income Support for the family, with a child of 4 and a 16-year-old school leaver who has none of their own | yes: the parent's claim is not the young person's | **£1,788.80** = (£20.70 + £13.70) × 52 | **£5,439.50** = Income Support £73.10 × 52 = £3,801.20 + £1,788.80 − £150.50 |

Case 19 is the case that showed the gap: before the Income Support limb, the reg. 4 route paid that family £1,076.40 of Child Benefit, and household net income came to £1,076.40 more than the law gives.

The same file pins the other reg. 8 limbs PolicyEngine reads, in 2026-27 (eldest rate £27.05 × 52 = £1,406.60):

| Case | Facts | Young person a QYP | Child Benefit |
|---|---|---|---:|
| 17 | 16, left school, reports £5,000 of their own income-related ESA | no (reg. 8(2)(e)) | **£0** |
| 18 | as 17 with income-based JSA | no (reg. 8(2)(b)) | **£0** |
| 27 | 17 at school, reports £5,000 of their own contributory ESA | no (reg. 3 with reg. 8(2)(e), which names ESA under Part 1 of the 2007 Act without restriction) | **£0** |
| 28 | 16, left school, reports £5,000 of contribution-based JSA | yes: reg. 8(2)(b) names income-based JSA only | **£1,406.60** |

PolicyEngine does not model the incapacity benefit and tax credit limbs (reg. 8(2)(c) and (d)).

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

PR_2300_TITLE.txt

Place the UC and Pension Credit QYP terminal date from the birth date

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

PR_2300_BODY.md

Fixes #2291

Reproducer: policyengine_uk/tests/test_qualifying_young_person_terminal_date_properties.py::test_universal_credit_and_pension_credit_agree_on_every_combination

axiom: TheAxiomFoundation/rulespec-uk#531 queued (UC reg 5(1)(b), `uk:regulations/uksi/2013/376/5#continued_education_or_training_qualifying_condition`; no companion case on `main` sets the 19th-birthday input false, the issue's cases need a signed re-encode of reg 5, and a billed encoder run needs approval); TheAxiomFoundation/rulespec-uk#360 queued (SPC reg 4A(1)(b); [addendum](https://github.com/TheAxiomFoundation/rulespec-uk/issues/360#issuecomment-6103743869); rulespec-uk's corpus pin holds reg 4A's row, and a billed encoder run needs approval)

**Stack.** The merge order is #2300, #2301, then #2302. #2302 carries this PR and #2301, so landing #2302 lands both. This PR builds on #2279's Pension Credit qualifying young person, which has merged into `main`; 25cb0070e merges `main` 47a2817e2 into this branch. This PR's own commits:

```bash
git log -p --no-merges origin/qyp-terminal-date-19 ^origin/main
```

## Summary

A young person in non-advanced education or approved training is a qualifying young person (QYP) "up to, but not including, the 1st September following their 19th birthday". The model treated a 19-year-old three ways:

| Where | 19-year-old at school, course begun before 19 |
|---|---|
| Universal Credit, household calculation | not a QYP: the terminal-date input defaulted to false |
| Universal Credit, microdata | a QYP: policyengine-uk-data sets that input true for every 19-year-old in qualifying education or training |
| Pension Credit, everywhere | a QYP: no terminal-date condition at all |

This PR places the date from the date of birth for both programmes. Against its base (`main` 4d80b2941 with #2279 merged), 2026-27, a lone parent aged 45 with a 19-year-old at school (the same on `main` 9475c95ab before #2285):

| Variable | Base | This PR |
|---|---:|---:|
| `is_qualifying_young_person_for_universal_credit` (the 19-year-old) | false | true |
| `uc_individual_child_element` | £0 | £4,222.56 (£351.88 × 12) |
| `is_uc_work_allowance_eligible` | false | true |
| `is_qualifying_young_person_for_pension_credit` | true | true |

It follows up two of #2279's items: "Pension Credit reg. 4A(1)(b)'s 1 September" and the Universal Credit default.

## The law

- [UC Regs 2013 reg. 5(1)](https://www.legislation.gov.uk/uksi/2013/376/regulation/5): "A person who has reached the age of 16 but not the age of 20 is a qualifying young person for the purposes of Part 1 of the Act and these Regulations— (a) up to, but not including, the 1st September following their 16th birthday; and (b) up to, but not including, the 1st September following their 19th birthday, if they are enrolled on, or accepted for, approved training or a course of education [...]".
- [SPC Regs 2002 reg. 4A(1)(b)](https://www.legislation.gov.uk/uksi/2002/1792/regulation/4A): "up to, but not including, the 1st September following the person's 19th birthday, if the person is enrolled on, or accepted for, approved training or a course of education [...]".
- UC 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." SPC reg. 4A(5) lists those and "income support" too.

## Change

- **New date fact** `is_before_first_september_following_19th_birthday` (Person, year): true when the fiscal year starts (6 April) before the 1 September following the 19th birthday. It reads `date_of_birth`, or `age` and `months_since_last_birthday`, through `utils.dates.birth_day`, and mirrors #2279's 16th-birthday fact.
- **Universal Credit.** `is_before_universal_credit_qualifying_young_person_terminal_date` was an input with `default_value = False`. It now has a formula that returns the date fact. A dataset or situation can still set it.
- **Defaults.** The date fact, the Universal Credit variable and the Pension Credit condition all default to true (see "Setting the date for one person" below).
- **Pension Credit.** New `meets_qualifying_young_person_terminal_date_condition_for_pension_credit` (under 19, or the date fact), which the education route in `is_qualifying_young_person_for_pension_credit` requires, with a new parameter `terminal_date_age_limit` (19).
- **Own awards, from #2279.** Both programmes' formulas exclude a young person whose own-right flag or own reported award shows Universal Credit, ESA or JSA, on either route. Pension Credit's formula also reads `is_on_income_support`, for reg. 4A(5)'s Income Support. That exclusion applies before and after the date, so the terminal date never brings back a young person on their own award.

## The annual approximation

The period ends on a fixed 1 September that an annual period cannot split. As in #2279, a year counts if any of it falls in the period, so a year counts when it starts before that 1 September.

- **Every 19-year-old (age on 6 October) counts.** Their 19th birthday falls between 7 October of the year before and 6 October, so the following 1 September falls in that year or the next, always after 6 April.
- **A 20-year-old born from 1 September to 6 October** stays before the date until 31 August, but at 20 on 6 October, when the model reads age, they already fail the QYP age limit. The fact is true for them and changes nothing (YAML Case 7).
- **No one newly loses the status.** The fact is false only for people aged 20 or over on 6 October.

So the default matches what the microdata and Pension Credit did already. Set either variable to false for a 19-year-old whose 1 September has passed.

## Setting the date for one person

An input set for some people fills the others with the variable's default and skips its formula for them. With a false default, setting the date fact for one person gave everyone else in the situation `false`, and a second 19-year-old student silently lost both statuses.

The fact's formula gives true for everyone under 20 on 6 October, and only they can pass the QYP age limit. So each of the three variables now defaults to true. Setting one for some people leaves everyone else under 20 where the formula puts them. For someone older the default is true where the formula says false, but every reader of these variables applies the QYP age limit first, and they fail it. A property test checks that the formula is true for every age 0 to 19 and every month since the last birthday, in three years, and that each variable defaults to true. It also checks a household of two 19-year-old students with each variable set false for the first only: the second stays a QYP for both programmes. YAML Case 12 pins the same household.

**Setting both layers.** A situation can cross the two: the date fact set false for one student and a programme variable (Universal Credit's terminal date, or Pension Credit's condition) set false for another. The programme variable then gives the first student its default, true, not their date fact, so the first becomes eligible again for that programme. No default can keep a formula for people left unset, so both programme variables now say so: when setting both, set the programme variable for every person whose status matters. A crossed test shows the hazard and the documented use for each programme, and invariant 5 covers one variable at a time.

Two YAML cases changed with it. Case 3's parent, not set, now shows the default (true) for the Universal Credit variable; nothing reads it for a 45-year-old. And `child_qualifying_young_person.yaml`'s "aged 19 requires being before terminal date" case left its after-the-date student unset and relied on the old false default; it now sets that input to false explicitly. #2279 documents the same partial-input behaviour for its 16th-birthday fact, whose formula is not true for everyone it could default for.

## Invariants

1. The date fact is true exactly when the year starts before the 1 September following the 19th birthday, for every date of birth, against calendar arithmetic that shares no code with the model.
2. Universal Credit's terminal-date variable follows the fact unless set, and setting it changes Universal Credit only.
3. Over every combination of age, education, training, apprenticeship, own-right Universal Credit, ESA or JSA, entry age and the two date facts, for people not on Income Support, Universal Credit and Pension Credit agree, and both equal the regulation's conditions. They part only for a young person on their own Income Support. SPC reg. 4A(5) lists Income Support and UC reg. 5(5) doesn't, so that young person, where they otherwise qualify, keeps the Universal Credit status and loses the Pension Credit one, on either route.
4. With the 19th-birthday fact on, both statuses equal the earlier definition without a terminal date; switching it on never removes the status; it matters only for a 19-year-old in education or training.
5. The formula is true for everyone under 20 on 6 October, and each terminal-date variable defaults to true, so setting one of them for some people leaves everyone else's status as it would be with nothing set. That holds one variable at a time; a situation that also sets the date fact must set the programme variable for everyone whose status matters (the crossed test).

## Tests

- **Properties**, new `test_qualifying_young_person_terminal_date_properties.py`:
  - invariants 1 and 2 for every date of birth from 2004 to 2009 (2,192 people, ages 16 to 22 on 6 October 2026), with the 31 August and 1 September edges asserted, and again on Hypothesis-drawn dates of birth and years;
  - invariants 3 and 4 over 6,144 combinations in 2026-27, when the model pays no Income Support; the test asserts that nobody in the grid receives it;
  - invariant 3's Income Support difference over 128 households in 2025-26 (ages 16 to 19, in or outside education, entry at 17 or 19, both date facts, with and without the young person's own reported Income Support), against the same oracle;
  - invariant 2 for each programme's input set against the fact;
  - invariant 5: every age 0 to 25 and month since the last birthday, in 2022, 2026 and 2030, and the mixed household.
- **YAML**, new `qualifying_young_person_terminal_date.yaml`, fifteen cases hand-computed (2026-27, and 2025-26 for Case 15, when the model still pays Income Support):

| Case | Facts | Expected |
|---|---|---|
| 1 | lone parent 45, 19-year-old at school | UC and PC QYP; child element £351.88 × 12 = **£4,222.56**; work allowance |
| 2 | as 1, UC input false | UC not a QYP, element £0; PC still a QYP |
| 3 | as 1, date fact false | neither |
| 4 | pensioner 70, 19-year-old at school | PC first-child addition £81.07 × 52 = **£4,215.64** |
| 5 | as 4, date fact false | addition £0 |
| 6 | born 31 August 2007 | the period ends on the 19th birthday, in the year: QYP |
| 7 | born 15 September 2006, aged 20 on 6 October | fact true; not a QYP (age) |
| 8 | born 31 August 2006 | the date fell in 2025: fact false |
| 9 | 19, course begun at 19 | not a QYP (reg. 5(2); reg. 4A(2)) |
| 10 | 19, outside education | not a QYP |
| 11 | born 1 September 2006: the 1 September following the 19th birthday falls in 2026 | fact true, but aged 20 on 6 October: not a QYP |
| 12 | pensioner 70, two 19-year-olds at school, the fact set false for the first only | the second takes the default (true): a QYP for both programmes; the first for neither |
| 13 | as 1, the 19-year-old reports their own Universal Credit | fact and UC terminal date true; a QYP for neither (reg. 5(5); reg. 4A(5)); element £0; no work allowance |
| 14 | as 4, the 19-year-old reports their own income-related ESA | PC terminal condition true; a QYP for neither; addition £0 |
| 15 | as 4, in 2025-26, the 19-year-old reports their own Income Support | a UC QYP but not a PC one (reg. 4A(5); reg. 5(5) doesn't list it); addition £0 |

**Mutation checks**, each applied alone in a copy of the branch (YAML cases and property tests that fail):

| Mutation | YAML cases | Property tests |
|---|---|---|
| The year compared with `>` and not `>=` | 1, 2, 4, 6, 7, 10, 11 | both calendar tests |
| A 1 September birthday read as its own "1 September following" | 11 | the every-date-of-birth test |
| The 18th birthday in place of the 19th | 1, 2, 4, 6, 7, 10, 11 | both calendar tests |
| Pension Credit's terminal condition dropped | 3, 5 | the agreement grid; the input-override test |
| The Universal Credit variable back to a false default | 1, 6 | both calendar tests; the agreement grid |

Case 11 pins the 1 September mutation in YAML as well as in the property test.

**Mutations at b5cbd2e36**: each default put back to false fails the two new property tests; the date fact's also fails YAML Cases 3 and 12.

**Mutations at d5b8d12d8**, each one change in a copy, run against `qualifying_young_person_terminal_date.yaml` and `test_qualifying_young_person_terminal_date_properties.py`:

| Mutation | YAML cases that fail | Property tests that fail | Result |
|---|---|---|---|
| Pension Credit's terminal condition dropped from its formula | 3, 5, 12 | the agreement grid, the input-override test, the one-person test, the crossed test, the Income Support test | killed |
| Universal Credit's terminal condition dropped from its formula | 2, 3, 12 | the agreement grid, the input-override test, the one-person test, the crossed test, the Income Support test | killed |
| `is_on_income_support` dropped from Pension Credit's reg. 4A(5) exclusion | none of the 14; Case 15, added at ee0482e75, fails | the Income Support test | killed |

**Merges into this head.** 596bf4f18 merges `main` 95dfc62f8, and 25cb0070e merges `main` 47a2817e2, each as its own commit, without conflicts.

**Run locally at 25cb0070e:**

- `policyengine-core test` on 53 YAML files (every test YAML file the branch changes against `main` 47a2817e2, and every baseline file that names `qualifying_young_person`, `income_support_reported` or `receives_income_support_in_own_right`), 897 passed. Of those, `qualifying_young_person_terminal_date.yaml` 15 passed and `child_qualifying_young_person.yaml` 8.
- `pytest -n 4`, one file at a time: `test_qualifying_young_person_terminal_date_properties.py` 8 passed; `test_child_and_adult_definitions_properties.py` 21; `test_benefit_cap_specified_benefits_properties.py` 46; `test_income_support_eligibility_properties.py` 4; `test_legacy_award_readers_properties.py` 20; `test_means_test_income_properties.py` 2; `test_pension_credit_young_person_income_support.py` 10; `policyengine_uk/tests/code_health` 3,165. No skip, xfail or failure.
- `.github/check_fix_pr.py` on this title and body: PASS, 4 changed executable tests.
- `uvx ruff@0.17.2 format --check .`, `check .` and `check --select F811,F821 policyengine_uk`: clean. `git diff --check` against `main` 47a2817e2: clean. `git merge-tree --write-tree` with `main` 47a2817e2: no conflict.

**Local runs at 93d59f052** (this branch with #2279's head c5b9850a3 and `main` f4a317019 merged):

- `policyengine-core test` on the six test YAML files the branch changes against `main` f4a317019 (#2279's included): `qualifying_young_person_terminal_date.yaml` 14 passed; `child_qualifying_young_person.yaml` 8; `qualifying_young_person_reported_own_awards.yaml` 23; `qualifying_young_person_no_education_route.yaml` 14; `is_uc_work_allowance_eligible.yaml` 13; `means_test_income_members.yaml` 7. 79 passed, none failed.
- `pytest -n 4`: `test_qualifying_young_person_terminal_date_properties.py` 8 passed; `test_child_and_adult_definitions_properties.py` 21; `test_means_test_income_properties.py` 2; `test_legacy_award_readers_properties.py` 20; `policyengine_uk/tests/code_health` 3,093.

**Local runs at b5cbd2e36**:
- `policyengine-core test` on the 5 YAML files the branch changes against `main` (#2279's included): 49 passed. At 6f6e7debd, the five YAML files that set these variables: 53 passed.
- `pytest` on the 4 Python test files it changes against `main`: 34 passed.
- The whole stack at 62e93179a (#2387, #2279 and the Child Benefit, CTC, terminal-date and Income Support follow-ups): `policyengine-core test` on the 248 YAML files that mention these programmes, 2,685 passed; `pytest` on the 135 Python test files that mention them, 1,930 passed, 1 xfailed and 6 failed. Four came from the benefit cap property oracle, which #2299's commits fix (8a5980c99), and two from a test generator on #2279, which #2279 fixed (560005cfa); both files pass at #2302's head 98a473909 (64 passed).

The full suites run in CI.

## Microsimulation impact

### Aggregate impact report (CI)

In [the report](https://github.com/PolicyEngine/policyengine-uk/pull/2300#issuecomment-6331537021), CI runs `main` against this PR's head merged into it, on the certified Enhanced FRS 2024-25 (policyengine-uk-data 1.58.1, sha256 `056e80fd8e91`), one full run per side for each year 2025-26 to 2031-32. CI last ran it at ee0482e75: with ee0482e75 merged into `main` 52b30809ff (526188ec3e), the report passes: no check breaks because of this change, one check fails on both sides ("VAT excluding private school fees is never negative"), and the report flags no moves. No household's net income changes in any year, and no receipt, government balance, poverty or inequality figure moves. Only the standard reform costing "Reduce Universal Credit taper rate to 20%" moves: −£1.6m, −£1.6m, −£1.7m, −£1.8m, −£2.1m, −£2.1m and −£2.2m from 2025-26 to 2031-32. #2279's own report at its head c5b9850a3 (against `main` 648b112443) shows the same figures in every year, so this PR's own change moves nothing; `main` 52b30809ff did not yet hold #2279.

### Local runs

These cover b5cbd2e36 only. We ran Enhanced FRS 2024-25 (uk-data 1.58.0, sha256 `5ba399ea5ab9a6c7`) once for each year 2025-26 to 2030-31 and compared every record with no tolerance. Base: `main` e01f2f5ec with #2279 (9d2fadbf5) merged (local merge 006c0e813). Branch: b5cbd2e36.

No record changes in any year, in any of the Universal Credit, Pension Credit, Housing Benefit, benefit cap, net income or poverty outputs. policyengine-uk-data supplies `is_before_universal_credit_qualifying_young_person_terminal_date`, true for the 102 records aged 19 in qualifying education in 2026-27, and a supplied input takes precedence over the new formula (on the earlier base). The new date fact is true for all 780 records aged 19 that year, so no one fails the Pension Credit condition either. The PR changes household calculations (the table at the top) and any dataset that does not supply the input.

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

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

PR_2302_TITLE.txt

Test PC, CB and CTC QYP exclusion of young Income Support recipients

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

PR_2302_BODY.md

Fixes #2292

Reproducer: policyengine_uk/tests/test_qualifying_young_person_income_support_properties.py::test_a_reform_paying_no_income_support_restores_the_severe_disability_addition

axiom: SPC Regs 2002 reg 4A(5) TheAxiomFoundation/rulespec-uk#360 queued (its Case F and [addendum cases A3 and A4](https://github.com/TheAxiomFoundation/rulespec-uk/issues/360#issuecomment-6102734361) cover income support); Child Benefit (General) Regulations 2006 reg 8 TheAxiomFoundation/rulespec-uk#356 queued ([addendum](https://github.com/TheAxiomFoundation/rulespec-uk/issues/356#issuecomment-6103738864)); CTC Regs 2002 reg 5(4)(c) TheAxiomFoundation/rulespec-uk#361 queued ([addendum](https://github.com/TheAxiomFoundation/rulespec-uk/issues/361#issuecomment-6103743610), Case J). Each states its blocker: rulespec-uk's corpus pin holds SPC reg 4A's and CTC reg 5's rows and no row for Child Benefit reg 8, and a billed encoder run needs approval.

**Stack.** The merge order is #2300, #2301, then #2302. This branch carries both: 919cc37e8 merges #2301's head 78d4d5070, which lands the Child Benefit commits from the closed #2299, and 0d934a836 merges #2300's head 25cb0070e. So landing this PR lands both. #2279 and #2387 have merged into `main`, which both merged heads carry at 47a2817e2. Until #2300 and #2301 merge, the diff here also shows their commits. This PR's own commits:

```bash
git log -p --no-merges origin/qyp-income-support-exclusion ^origin/main ^origin/ctc-qyp-school-leaver-routes ^origin/qyp-terminal-date-19
```

## Summary

Pension Credit, Child Benefit and Child Tax Credit law each excludes a young person who receives Income Support in their own right from qualifying young person (QYP) status. Universal Credit's does not. The model's own-right input, `receives_benefits_in_own_right`, covers Universal Credit, Employment and Support Allowance and Jobseeker's Allowance, the benefits Universal Credit's own rule lists.

The exclusions themselves now reach `main` from the PRs below this one: #2279 gave Pension Credit `is_on_income_support`, #2301 gives CTC the same reader, and #2301 also brings the Child Benefit gate and `receives_income_support_in_own_right` from #2299's commits. This PR's own change narrows to tests that the three programmes exclude and Universal Credit does not, in the baseline and under reforms that remove Income Support, a comment in the Pension Credit test, and its changelog fragment.

**How each programme reads receipt.**

| Programme | Reader | A claimant or partner | Another member of the benefit unit |
|---|---|---|---|
| Pension Credit (reg. 4A(5)) | `is_on_income_support` (#2279) | the benefit unit's calculated award, for whichever of the claimant and partner receives it | their own reported award, while Income Support runs and no reform neutralises it |
| Child Tax Credit (reg. 5(4)(c)) | `is_on_income_support` (#2301) | the same | the same |
| Child Benefit (reg. 8(2)(a)) | `receives_income_support_in_own_right` (#2301, from #2299's commits) | their own reported award, on the same terms | the same |
| Universal Credit (reg. 5(5)) | none: the regulation does not list Income Support | | |

**What a reform must do.** A reform that sets `gov.dwp.income_support.active` to false, or neutralises `income_support`, ends every programme's exclusion for everyone. A reform that only replaces `income_support`'s formula with one returning zero ends the Pension Credit and Child Tax Credit exclusion for a claimant or partner, because both read the calculated award. For any other member, and for Child Benefit, that reform leaves a reported award standing.

**Why Child Benefit reads the report.** The benefit unit's Income Support depends on which young people count for the family's Child Benefit. In fresh simulations in 2016, 2019 and 2024 of a parent claiming Income Support with a 17-year-old at school reporting their own, reading `is_on_income_support` from the Child Benefit test raised a `CycleError` on calculating `child_benefit` first:

```
child_benefit → child_benefit_entitlement → child_benefit_respective_amount
  → is_child_benefit_payable_for_person → is_qualifying_young_person_for_child_benefit
  → is_on_income_support → income_support → income_support_entitlement
  → income_support_applicable_amount
  → is_responsible_for_child_or_young_person_for_legacy_benefits
  → is_child_or_young_person_for_legacy_benefits
  → is_child_or_qualifying_young_person_for_child_benefit
  → is_qualifying_young_person_for_child_benefit
```

**The case the reproducer pins.** In 2019, a pensioner aged 70 on PIP daily living with £5,000 of State Pension shares a household with an unrelated 18-year-old in their own benefit unit: an orphan with no parental substitute, in qualifying full-time non-advanced education, with an existing Income Support claim (IS Regs 1987 reg. 13(2)(c)), caring for the pensioner 35 hours a week. Neither claims Carer's Allowance or Universal Credit. The household net income column leaves out cost-of-living payments, which DWP first paid in 2022:

| | Student's Income Support | Student a PC QYP | Pension Credit | Household net income |
|---|---:|---|---:|---:|
| Baseline, Income Support in payment | £3,010.80 (£57.90 × 52) | no (reg. 4A(5)) | **£3,697** | £14,805.70 |
| Reform replacing `income_support`'s formula with zero, at 98a473909 | £0 | no: the report still counted | **£3,697** | not rerun |
| The same reform, at this head | £0 | yes | **£7,121.20** | £15,219.10 |
| `income_support` neutralised, at this head | £0 | yes | **£7,121.20** | £15,219.10 |

Once the student receives no Income Support, they count as a qualifying young person (reg. 4A(5) no longer applies), DWP does not count them as residing with the pensioner (Schedule I para. 2(2)(f)), and the pensioner gets the severe disability addition, £65.85 × 52 = **£3,424.20**: £167.25 × 52 = £8,697 − £5,000 + £3,424.20 = £7,121.20. Household net income: State Pension £5,000 + PIP £58.70 × 52 = £3,052.40 + Pension Credit + Income Support + winter fuel payment £200 − TV licence £154.50.

## The law

- **Pension Credit.** [SPC Regs 2002 reg. 4A(5)](https://www.legislation.gov.uk/uksi/2002/1792/regulation/4A): "A person who is receiving universal credit, a contributory employment and support allowance, a contribution-based jobseeker's allowance, an income-related employment and support allowance, an income-based jobseeker's allowance or income support is not a qualifying young person." [Schedule I para. 2(2)(f)](https://www.legislation.gov.uk/uksi/2002/1792/schedule/I/paragraph/2) ignores "a person who is a qualifying young person within the meaning of regulation 4A or child" residing with the claimant.
- **Child Benefit**, and through SSCBA 1992 s.142 the Housing Benefit and legacy young person. [Child Benefit (General) Regulations 2006 reg. 8(2)](https://www.legislation.gov.uk/uksi/2006/223/regulation/8): "The condition is that the person is not in receipt, in a week, of— (a) income support, [...]".
- **Child Tax Credit.** [CTC Regs 2002 reg. 5(4)(c)](https://www.legislation.gov.uk/uksi/2002/2007/regulation/5): the period must not be "a period in respect of which that person receives income support, [...]".
- **Universal Credit** does not list it. [UC Regs 2013 reg. 5(5)](https://www.legislation.gov.uk/uksi/2013/376/regulation/5): "A person who is receiving universal credit, an employment and support allowance or a jobseeker's allowance is not a qualifying young person."
- [IS Regs 1987 reg. 13(2)](https://www.legislation.gov.uk/uksi/1987/1967/regulation/13) lets a qualifying young person in relevant education claim Income Support where they have "no parent nor any person acting in the place of his parents" ((c)), among other cases.

## Change

- New tests (below) of the four programmes against each other, in the baseline and under two reforms that remove Income Support.
- `is_qualifying_young_person_for_pension_credit`: a comment says when a claimant or partner receives Income Support for reg. 4A(5) (while the model pays their benefit unit an award). The formula stays as #2279 and #2300 leave it.
- The changelog fragment.

`is_qualifying_young_person_for_pension_credit` and `is_qualifying_young_person_for_child_tax_credit` read `is_on_income_support`; `is_qualifying_young_person_for_child_benefit` reads `receives_income_support_in_own_right`; `is_qualifying_young_person_for_universal_credit` reads neither. Their docstrings note two gaps: Child Benefit reg. 8 also lists incapacity benefit and tax credits, and CTC reg. 5(4)(c) lists only the income-related and income-based allowances, where the own-right input also catches the contributory ones.

## Invariants

1. Over every combination of age, education, training, the other own-right benefits and the 16th-birthday fact, for a member who is not the claimant or partner: reporting Income Support of their own while Income Support runs ends the Pension Credit, Child Benefit and Child Tax Credit statuses; without a report each status equals the status with the report left alone; Universal Credit status never depends on it; and Pension Credit and Universal Credit agree without it.
2. By default `receives_income_support_in_own_right` holds when the person reports Income Support of their own while Income Support runs (to 2025-26) and no reform neutralises it, and not otherwise.
3. Under a reform that neutralises `income_support`, no Pension Credit, Child Tax Credit or Child Benefit status depends on reported Income Support. Under a reform that replaces `income_support`'s formula with zero, no Pension Credit or Child Tax Credit status of a claimant or partner depends on it; another member's report and the Child Benefit reader still exclude, as their documentation says.
4. Universal Credit status never depends on reported Income Support, in the baseline or under either reform.
5. Setting a young person's reported Income Support to zero never lowers their household's Pension Credit.

## Tests

- **Properties**, `test_qualifying_young_person_income_support_properties.py`:
  - invariant 1 over 512 combinations (8 ages, every education value, training, own-right benefits, the 16th-birthday fact), each young person in a 70-year-old claimant's benefit unit, with and without a £3,000 report, and a check that there is a status to lose; the same grid with the report under a reform that neutralises `income_support` gives every programme's status without the report (invariant 3 for a member who is not the claimant or partner);
  - invariant 2 on Hypothesis-drawn ages, reported amounts and years from 2021 to 2028, and under neutralisation;
  - invariants 3 and 4 on Hypothesis-drawn households in 2016, 2019 and 2024, under each reform: a pensioner with a young person in their benefit unit or in the young person's own (where the young person claims, so the model calculates their Income Support), or a CTC claimant with a young person; the test compares each status with the same reformed simulation with every report set to zero;
  - invariants 4 and 5 on the same draws in the baseline;
  - the case above under both reforms (Pension Credit £7,121.20, the addition £3,424.20, household net income less the cost-of-living payment £15,219.10), and Child Benefit's status under each: still excluded under the zero-formula reform, a QYP once neutralised.
- **YAML**, `qualifying_young_person_income_support.yaml`, eight cases hand-computed:

| Case | Facts | Expected |
|---|---|---|
| 1 | 2025-26; pensioner 70, 17-year-old at school, no Income Support | every programme's QYP; Pension Credit first-child addition £78.10 × 52 = **£4,061.20**; Child Benefit £26.05 × 52 = **£1,354.60** |
| 2 | as 1, the 17-year-old reports £3,000 of Income Support | not a Pension Credit or Child Benefit QYP, nor a legacy young person; addition and Child Benefit **£0**; still a Universal Credit QYP |
| 3 | 2024-25; parent 40 with a CTC award, same 17-year-old | on Income Support; not a CTC QYP; child element **£0** |
| 4 | as 3 without Income Support | child element **£3,455** |
| 5 | 2025-26; a 16-year-old school leaver reporting Income Support | excluded from Pension Credit and Child Benefit; still a Universal Credit QYP |
| 6 | 2026-27; a report of Income Support after it stopped paying | does not count |
| 7 | 2025-26; `receives_income_support_in_own_right` set directly | not a Child Benefit QYP; still a Pension Credit and CTC QYP, which read `is_on_income_support` |
| 8 | 2019; the case above, Income Support in payment, the cost-of-living payment set to zero | student's Income Support **£3,010.80**; not a PC QYP; addition **£0**; Pension Credit **£3,697**; household net income **£14,805.70** (State Pension £5,000 + PIP £58.70 × 52 = £3,052.40 + £3,697 + £3,010.80 + winter fuel payment £200 − TV licence £154.50) |

## Merges into this head

Each is its own commit.
- 30fe1a88f's first parent merges 1eb9e075d (#2301's branch, which carried #2299's branch and its reg. 8 reports). Three conflicts, all in the qualifying young person tests:
  - CTC: #2301's documentation; the formula kept both Income Support gates for the next commit to settle.
  - Pension Credit: #2279's own-benefit block (reported awards and `is_on_income_support`) with #2300's terminal-date condition and documentation; the formula kept this branch's gate as well.
  - Child Benefit: #2299's own-award block (the flag and the reported Universal Credit, ESA and income-based JSA) and this branch's gate, with documentation naming both.
- 30fe1a88f merges 93d59f052, #2300's head at the time. One conflict, in the scope note of `test_child_and_adult_definitions_properties.py`: the merged text keeps #2299's and #2301's leaver inputs and #2300's terminal-date and Income Support sentences, and names Child Benefit reg. 8 and CTC reg. 5(4)(c) beside SPC reg. 4A(5).
- 4e1e0176a removes `receives_income_support_in_own_right` from the Pension Credit and CTC tests, and 6db2b6d47 reformats the property file.
- ee8ca3024 merges #2301's head 77f6cdc5a, which brought the Child Benefit gate and `receives_income_support_in_own_right` from #2299's commits. Three conflicts:
  - `receives_income_support_in_own_right.py`, which both sides added: one copy, with the same formula and comment, and documentation that names Child Benefit, Pension Credit and CTC as readers, the active parameter among the ways a reform ends the exclusion, and the claimant-or-partner reading of `is_on_income_support`.
  - `is_qualifying_young_person_for_child_benefit.py`: the same gate on both sides; the documentation keeps the incoming sentence that a positive report for the year ends the status for the whole year.
  - `child_tax_credit_school_leaver_routes.yaml` Cases 18 and 19: the incoming cases, which set the cost-of-living payment to zero.
- f7e5fdedc merges #2300's ee0482e75. One conflict, in the same scope note: this branch's text, with ee0482e75's change that the tests set UC's terminal-date input "false unless a test varies it".
- c21e35b26 sets the cost-of-living payment to zero in YAML Case 8, and 05095a161 subtracts it in the reproducer and adds the neutralised grid check.
- 87e2d1cdf and 66a200e28 merge #2301's and #2300's heads after each took `main` 95dfc62f8; 919cc37e8 and 0d934a836 merge them again after each took `main` 47a2817e2. None of the four had a conflict.

## Differential against 98a473909

A generated population of 400 households a year (pensioners with a young person in their benefit unit or in the young person's own, CTC claimants with a young person and sometimes a child, and young people alone; reported Income Support, income-related ESA and income-based JSA drawn for the young person), run on 98a473909 and 6db2b6d47. Households whose Pension Credit, CTC or Child Benefit differ:

| Year | Differ | Young person reports their own income-related ESA or income-based JSA | Young claimant reports Income Support the model does not calculate |
|---|---:|---:|---:|
| 2016 | 12 | 11 | 1 |
| 2019 | 15 | 14 | 1 |
| 2024 | 9 | 9 | 0 |

- **ESA or JSA reported** (34 households): at 6db2b6d47 the young person is not a Pension Credit QYP (#2279's report block) or a Child Benefit QYP (#2299's reg. 8 reports). Child Benefit falls by the young person's rate: £1,076.40 (eldest) or £712.40 (additional) in 2016 and 2019, £1,331.20 or £881.40 in 2024. In 10 households the young person belongs to a pensioner's benefit unit, and Pension Credit falls by the child addition: £3,319.68 in 2019 (5 households) and £3,993.08 in 2024 (5). In two of the 2024 ones (231 and 317) the pensioner receives PIP and the young person is 18, so the young person, no longer a QYP, counts as a resident under Schedule I para. 2 and the pensioner also loses the severe disability addition, £81.50 × 52 = £4,238: £8,231.08 in all. The model counts only residents aged 18 or over, so where the young person is 16 or 17 (2019 household 134, 2024 household 103) the addition stays.
- **Income Support reported, none calculated** (2 households): an 18-year-old in their own benefit unit, at school, reporting Income Support. The model calculates no award for them (in 2016 their family claims Universal Credit), so at 6db2b6d47 they are a Pension Credit QYP, and the pensioner's Pension Credit rises by the severe disability addition: £8,091.20 to £11,307.40 in 2016 (£61.85 × 52 = £3,216.20) and £8,697 to £12,121.20 in 2019 (£3,424.20).
- **A third reason** appears where the population includes 15-year-old carers who claim Carer's Allowance: 98a473909 pays them Carer's Allowance, which withholds the pensioner's severe disability addition, and 6db2b6d47 does not, so the pensioner gains it. That change comes from the commits merged in at 30fe1a88f's first parent, not from this PR's reader. The table above does not count such households.
- No household's CTC differs.

## Mutation checks

At 4e1e0176a (6db2b6d47 changes only the property file's formatting), each applied alone in a copy of the branch (`qualifying_young_person_income_support.yaml`, `child_tax_credit_school_leaver_routes.yaml`, `qualifying_young_person_terminal_date.yaml`, `qualifying_young_person_reported_own_awards.yaml` and three property files):

| Mutation | YAML cases that fail (of 65) | Property tests that fail | Result |
|---|---|---|---|
| The Income Support bar dropped from the CTC test | this file's Case 3; CTC Cases 18 and 20 | `test_income_support_ends_three_statuses_and_not_universal_credit`, `test_reforms_that_remove_income_support_end_the_exclusions`; in the CTC file `test_income_support_ends_every_route`, `test_income_support_does_not_end_the_status_once_neutralised`, `test_family_members_in_any_benefit_unit` | killed |
| Pension Credit reads the report again (adds `receives_income_support_in_own_right` to its own-benefit block, so a claimant's report counts) | this file's Case 7; #2279's "A claimant's report without a calculated award leaves the Pension Credit status available" | `test_a_reform_paying_no_income_support_restores_the_severe_disability_addition`, `test_reforms_that_remove_income_support_end_the_exclusions` | killed |
| The claimant branch dropped from the reader (`is_on_income_support` reads the report for everyone) | #2279's "A claimant's report without a calculated award leaves the Pension Credit status available" | `test_a_reform_paying_no_income_support_restores_the_severe_disability_addition`, `test_reforms_that_remove_income_support_end_the_exclusions` | killed |

`test_qualifying_young_person_terminal_date_properties.py` (8 tests) passes under all three.

At 0d934a836, `is_on_income_support` keeping another member's report under a reform that neutralises `income_support` (`(not removed)` dropped from `own_award`) fails `test_income_support_ends_three_statuses_and_not_universal_credit` (120 of 512 statuses differ from the unreported grid under neutralisation): killed. 05095a161 added that check.

Earlier mutations, at the commits named:

| Mutation | YAML cases | Property tests |
|---|---|---|
| At c6c46e60e: Pension Credit ignores Income Support | 2, 5, 7 | the grid |
| At c6c46e60e: Child Benefit ignores Income Support | 2, 5, 7 | the grid |
| At c6c46e60e: Child Tax Credit ignores Income Support | 3 | the grid |
| At c6c46e60e: Universal Credit excludes for Income Support too | 2, 5, 7 | the grid |
| At c6c46e60e: a report counts after Income Support stopped paying | 6 | the default test |
| At c6c46e60e: receipt that ignores a reform removing Income Support | | `test_no_receipt_once_a_reform_removes_income_support` (removed at 4e1e0176a) |

## Run locally

- At 0d934a836: `policyengine-core test` on 57 YAML files (every test YAML file the branch changes against `main` 47a2817e2, and every baseline file that names `qualifying_young_person`, `income_support_reported` or `receives_income_support_in_own_right`), 1,004 passed, `qualifying_young_person_income_support.yaml` 8 of them; `pytest -n 4`, one file at a time: `test_benefit_cap_specified_benefits_properties.py` 46 passed, `test_child_and_adult_definitions_properties.py` 21, `test_child_benefit_school_leaver_routes_properties.py` 5, `test_child_tax_credit_school_leaver_routes_properties.py` 6, `test_income_support_eligibility_properties.py` 4, `test_legacy_award_readers_properties.py` 20, `test_means_test_income_properties.py` 2, `test_pension_credit_young_person_income_support.py` 10, `test_qualifying_young_person_income_support_properties.py` 5, `test_qualifying_young_person_terminal_date_properties.py` 8, `policyengine_uk/tests/code_health` 3,177. No skip, xfail or failure.
- At 0d934a836: `uvx ruff@0.17.2 format --check .`, `ruff check .` and `ruff check --select F811,F821 policyengine_uk`: clean; `git diff --check` against `main` 47a2817e2: clean; `git merge-tree --write-tree` with `main` 47a2817e2: no conflict.
- Earlier, at 6db2b6d47: `policyengine-core test` on 31 YAML files, 499 passed, and the same property files, all passing, with `code_health` 3,107. At 62e93179a (#2387, #2279 and the Child Benefit, CTC, terminal-date and Income Support follow-ups): `policyengine-core test` on the 248 YAML files that mention these programmes, 2,685 passed; `pytest` on the 135 Python test files that mention them, 1,930 passed, 1 xfailed and 6 failed (four the benefit cap property oracle, fixed at 8a5980c99, and two a test generator on #2279, fixed there at 560005cfa).

The full suites run in CI.

## Microsimulation impact

### Aggregate impact report (CI)

[The report](https://github.com/PolicyEngine/policyengine-uk/pull/2302#issuecomment-6331636085) runs `main` 52b30809f against this PR merged into it (122551f1d, head 6db2b6d47) on the certified Enhanced FRS 2024-25 (policyengine-uk-data 1.58.1), one full run per side for each year 2025-26 to 2031-32. Result: **pass**, 0 checks broken, 1 failing on both sides, 0 moves flagged. Child Benefit rises by £5.2m to £6.2m a year (about 1,000 more awards) and income tax by £1.8m to £2.2m. The report matches #2301's at 1eb9e075d line for line, so the Child Benefit routes that #2301 lands make these moves, and the income tax comes from the High Income Child Benefit Charge on that Child Benefit, as #2301's local runs show; this PR's own change and #2300's move nothing. The standard reform costing "Reduce Universal Credit taper rate to 20%" moves by −£1.6m to −£2.2m a year, which #2279's own report also shows.

### Local runs

These cover c6c46e60e only. Real runs on Enhanced FRS 2024-25 (uk-data 1.58.0, sha256 `5ba399ea5ab9a6c7`), one run each for 2024-25 to 2030-31, exact comparison of every record. Base: local merge 565b29c04 (on `main` e01f2f5ec with #2279 and #2387). Branch: c6c46e60e.

**No record changes in any year.** In 2024-25 and 2025-26, 60 records (about 36,000 weighted people) receive Income Support in their own right, and none of them is 16 to 19; no one aged 16 to 19 reports any Income Support (on the earlier base). From 2026-27 Income Support no longer runs. The change shows in household calculations (the tables above).

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

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Ready for hub review

Heads

All three sit on claude/child-benefit-routes-2301-jx4bqx, whose head is #2302's; its history holds the other two. I pushed to no PR branch and to neither earlier claude/ branch, and opened no PR.

PR Final head Built from
#2300 25cb0070ee3db1e69a0f86be1f8a39073cc120dd 596bf4f18 + merge of main 47a2817e2
#2301 78d4d507043a65eeb39548aaf903e3c3bba4ac38 f5fd07931 + merge of main 47a2817e2
#2302 0d934a836984a20447f1c3e0f02a9b30dc7b8f35 66a200e28 + 62f5fcad2 (changelog wording) + merge of 78d4d5070 (919cc37e8) + merge of 25cb0070e

No main move during the session: main stayed at 47a2817e2d57e6acbc08b76e09fde89fc27b81fa, which holds #2279 and #2855 (90e8418cb).

Before starting and before each push and post I checked: claude/child-benefit-routes-2301-jsci1r at 66a200e28, claude/child-benefit-income-support-gate-tphs7t at 812131cdb, #2300 ee0482e75, #2301 1eb9e075d, #2302 6db2b6d47, and no comment on #2300, #2301 or #2302 after 03:23:34Z on 5 Nov other than mine. The hub documents are in the comments above: PR_2300_TITLE.txt 6652095083, PR_2300_BODY.md 6652095385, PR_2301_TITLE.txt 6651727617, PR_2301_BODY.md 6651727949, PR_2302_TITLE.txt 6652626328, PR_2302_BODY.md 6652626662, AXIOM_356.md 6651728164. The work record (6650884267) holds the same counts and can go.

Review of 828's thirteen commits, dd8cd3e8e and the four merges

I read each diff and git show --remerge-diff for each merge. All hold; I found nothing to fix in code or tests. One wording fix on top (62f5fcad2).

Commit Steps and findings Holds?
892911c81 Step 1: the gate in is_qualifying_young_person_for_child_benefit.py (formula and comment identical to 6db2b6d47) and receives_income_support_in_own_right.py (formula identical). Its documentation names only readers at that head: the CB test (and through it HB and the legacy young person), and PC through is_on_income_support (git grep at 892911c81: PC, the HB and CTR non-dep deductions and _legacy.py read is_on_income_support; only the CB test reads the new variable). It also carries 816 f3 / A3 (the active parameter) and 815 P3-2 wording. Changelog adds the IS limb. yes
d15f64d32 Step 2: CB YAML Cases 19-26 and tests/income_support_reforms.py (a Reform the YAML reforms: key loads). Case 19 is juaristi22's 2018 case; Case 25 uses 2026-27, and I confirmed parameters(period) for a YEAR period reads active on 30 April (utils/dated_reforms.py, utils/parameters.py: 2025 → true, 2026 → false). The 2018 cases pin the cost-of-living payment (S1). yes
bec8d91bb Step 3 property test_own_income_support_ends_the_status_on_every_route, 2016/2019/2024; invariants 6 and 7 in the file docstring. yes
3d5ef701e 815 P2-2: the review's oracle diff, verbatim. yes
3eb64dfc4 815 P2-4: Cases 27 and 28 (numbered after Step 2's); 815 P3-6 docstring sentence. yes
ad5dc0bf0 Step 4: merges 3eb64dfc4 into #2301's 1eb9e075d; no conflicts. yes
817ee7d45 815 P3-2 and 816 f3 / A3 in the CTC docstring; CTC named as a reader in the new variable's documentation. yes
77f6cdc5a 815 P2-3 / 816 f2 / S1 and 815 P3-3: CTC Cases 18 and 19 pin the payment; with the CB gate Case 18 gives £6,188.30 and Case 19 £10,589.70, as the brief expected. yes
dd8cd3e8e changelog actor wording. yes
ee8ca3024 Step 4: merges 77f6cdc5a into #2302; three conflicts (the new variable, add/add: one copy with the incoming documentation, which names CB, PC and CTC as readers; CB docstring: incoming sentence kept; CTC Cases 18/19: incoming values). yes
f7e5fdedc Step 4: merges ee0482e75; the scope-note conflict resolved as 816 shows. yes
c21e35b26 S1: IS YAML Case 8 pins the payment, £14,805.70; header states 815 P3-2's reading. yes
05095a161 S1 reproducer: household net income less the payment, £15,219.10; 816 f5: neutralised grid check. yes
812131cdb 816 f7 changelog. yes, but see 62f5fcad2
596bf4f18, f5fd07931, 87e2d1cdf, 66a200e28 main 95dfc62f8 merges and #2302's re-merges; --remerge-diff lists no file for any. yes

62f5fcad2: #2302's fragment said PC and CTC read "the Income Support the model calculates for a claimant or partner"; it now says "the benefit unit's calculated Income Support for whichever of the claimant and partner receives it", per 815 P3-2.

Item 2: main merges

main sat at 47a2817e2 throughout (holds #2279 and #2855). 25cb0070e and 78d4d5070 each merge it, and 919cc37e8 then 0d934a836 merge those into #2302's line. No conflicts in any of the four. git merge-tree --write-tree --name-only HEAD origin/main: clean at all three heads.

Step 4 file check. I compared each old PR head merged with main 47a2817e2 (git merge-tree --write-tree) against my head:

Checks

  • Step 1 cycles at f5fd07931 (same Child Benefit, Income Support and CTC code as 78d4d5070): child_benefit, household_net_income, income_support and is_qualifying_young_person_for_child_benefit, each calculated first and alone in a fresh simulation, in 2016, 2018, 2019 and 2024, for (a) CB Case 19's household and (b) a lone parent claiming Income Support (income_support_reported, claims_all_entitled_benefits) with a child of 4 and a 16-year-old school leaver who reports their own Income Support: 0 CycleError in 32 calculations. In (b) the parent's Income Support is £3,801.20 (2016-2019) and £4,706 (2024), Child Benefit pays for the 4-year-old only (£1,076.40; £1,331.20 in 2024), and the 16-year-old is not a QYP.
  • Benefit cap oracle (815 P2-2): a probe built from the file's own blank, set_circumstance, situation and references (claimant 40 + one dependant + a 30-year-old non-dependant, 2025; dependant young_person 16 or 17 or sixteen; ESA income, ESA contrib, JSA income or JSA contrib, with or without DLA; both schemes): 828's oracle 0 mismatches of 48 at f5fd07931; 1eb9e075d's oracle on the same model 12 of 48. pytest -n 4 on the file: 46 passed at each final head.
  • Fix evidence (.github/check_fix_pr.py from main, as pr_fix_evidence.yaml runs it, no labels, changes from git diff --name-status --no-renames -z against the merge base with main): Place the UC and Pension Credit QYP terminal date from the birth date #2300 PASS, 4 changed executable tests; Add CB and CTC qualifying young person routes for school leavers #2301 PASS, 10; Test PC, CB and CTC QYP exclusion of young Income Support recipients #2302 PASS, 15.
  • uvx ruff@0.17.2 format --check ., ruff check ., ruff check --select F811,F821 policyengine_uk: clean at all three heads. git diff --check origin/main...HEAD: clean at all three. git merge-tree --write-tree --name-only HEAD origin/main: clean at all three.
  • The passive-voice grep from the brief over the four documents: hits only in quoted law.

Mutations

Each in a scratch worktree, applied alone, then reverted.

Where Mutation YAML that fails Properties that fail Result
f5fd07931 CB Income Support gate removed (Step 3) CB Cases 19, 21, 22, 23 (4 of 28); CTC Case 18 (1 of 20); qualifying_young_person_reported_own_awards.yaml 23/23 and qualifying_young_person_no_education_route.yaml 14/14 pass test_own_income_support_ends_the_status_on_every_route (CB file 1 of 5); legacy readers 20/20 pass killed
f5fd07931 815 M3: reports exclude only off the education route CB Case 27 (1 of 28) none killed
f5fd07931 815 M4: esa_contrib_reported dropped CB Case 27 (1 of 28) none killed
0d934a836 816 M4: (not removed) dropped from is_on_income_support's own_award not run test_income_support_ends_three_statuses_and_not_universal_credit (120 of 512 statuses differ) killed

juaristi22's case (CB Case 19) and the Step 3 property both fail without the gate. Cases 20, 24, 25 and 26 pass without it, as they should: none holds a report the gate would count.

Tests at the final heads

From clean runs, one file at a time; no skip, xfail, XPASS, failure or timeout anywhere. YAML: every test YAML the branch changes against main 47a2817e2 plus every baseline YAML naming qualifying_young_person, income_support_reported or receives_income_support_in_own_right, with policyengine-core test. Python: pytest -n 4, each capped at 40 minutes (the slowest took 5m44s).

#2300 25cb0070e #2301 78d4d5070 #2302 0d934a836
YAML 53 files, 897 passed 55 files, 981 passed 57 files, 1,004 passed
test_benefit_cap_specified_benefits_properties.py 46 46 46
test_child_and_adult_definitions_properties.py 21 21 21
test_child_benefit_school_leaver_routes_properties.py n/a 5 5
test_child_tax_credit_school_leaver_routes_properties.py n/a 6 6
test_income_support_eligibility_properties.py 4 4 4
test_legacy_award_readers_properties.py 20 20 20
test_means_test_income_properties.py 2 2 2
test_pension_credit_young_person_income_support.py 10 10 10
test_qualifying_young_person_income_support_properties.py n/a n/a 5
test_qualifying_young_person_terminal_date_properties.py 8 n/a 8
policyengine_uk/tests/code_health 3,165 3,174 3,177

At #2300's head, its own files on their own: qualifying_young_person_terminal_date.yaml 15 passed, child_qualifying_young_person.yaml 8; its Python files (test_qualifying_young_person_terminal_date_properties.py, test_child_and_adult_definitions_properties.py) are in the table. At #2302's head, qualifying_young_person_income_support.yaml 8 passed. #2855 sits in main 47a2817e2, which each head carries, so these are the runs on a merge with #2855: CTC Cases 18 and 19, IS Case 8 and the reproducer pass there.

Findings, and where each now stands

Sentences that carry CI figures (for the hub to refresh)

I expect the taper line to drop out of all three once CI reruns against a main that holds #2279; I have not run that.

Skipped

  • Survey-data runs: no private data access. Every body labels its earlier local figures by commit.
  • The full suite; CI covers it. No CI ran on my heads: I opened no PR and moved no PR branch.
  • A YAML run for the 816 M4 mutation (I ran the one property test that the brief's finding names).
  • rulespec-uk#356 and Simulate PIP #361: outside this session's GitHub scope, so I could not check their current text; AXIOM_356.md builds only on Add CB and CTC qualifying young person routes for school leavers #2301's body and the YAML at 78d4d5070.
  • shard_durations.toml: no test renamed or removed.

Judgment calls


Generated by Claude Code

@MaxGhenis MaxGhenis changed the title Exclude young Income Support claimants from PC, CB and CTC QYP status Test PC, CB and CTC QYP exclusion of young Income Support recipients Nov 5, 2026

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review status: COMPLETE at 0d934a836984a20447f1c3e0f02a9b30dc7b8f35. All prior findings are resolved; no new findings or material evidence gaps.

C1 and A1 remain resolved: the calculated claimant/payee Income Support reader for PC/CTC and Child Benefit's acyclic own-report reader retain their distinct documented scopes. The current final-PC regression computes the award rather than supplying it.

A2 resolved: the impact narrative now matches the dated primary CI report. A3 resolved: reform instructions include the programme's active = false option as well as neutralisation. S1 resolved: the affected income assertions isolate inherited cost-of-living payments.

13/13 focused checks passed at this exact head on lock-matched Core 3.34.1, with valid unchanged proof retained and the inherited fixes traced to the separately tested #2300/#2301 heads. Ancestry confirms this head contains both current parents, so the stated combined-delivery alternative is preserved. Across the three actual snapshots, the single bounded campaign passed 156 checks; those are not a hypothetical merge run or a full-suite claim.

The independent source review supports the programme-specific exclusion and the filed queued Axiom records. Annual/report approximations and historical aggregate qualifications remain explicit. No approval is issued while this PR is a draft; pending CI can be checked later without repeating this review.

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

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

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

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

MaxGhenis added a commit that referenced this pull request Nov 6, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Nov 6, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Income Support in own right does not end qualifying young person status for Pension Credit, Child Benefit or Child Tax Credit

3 participants