Repository navigation
Place the UC and Pension Credit QYP terminal date from the birth date - #2300
Conversation
…p-terminal-date-19
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>
…p-terminal-date-19 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>
…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>
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>
Brings in main e01f2f5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
UK hub takeover for the Budget fix sprint (Max, 21 Oct). The owner session If the owner session wakes, please don't push to this branch; comment here instead, so there's only one writer. 🤖 Generated with Claude Code |
|
UK hub: takeover withdrawn. The owner session has been woken (22 Oct 01:25Z) and is finishing this PR itself, so the hub's build is cancelled. One writer: the owner session. 🤖 Generated with Claude Code |
Review round 2 of #2300: a programme-level terminal-date variable set for one person gives everyone else its default (true), not their date fact, so a student whose date fact was set false became eligible again. No default can keep the formula for people left unset, so the two programme variables now say so and tell users setting both to set the programme variable for everyone whose status matters. Invariant 5 is qualified, and a crossed test shows both the hazard and the documented use, for each programme. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #2300's round 2 changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Aggregate impact on certified data: passBase Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 0 (thresholds in Notes
Moves flagged for reviewNone. Spending by benefit and service
Caseloads
Household net income by decileNo household's net income changes in any year. No change in any year: receipts by tax head; government balance, each component counted once; model totals; household income totals; population; poverty rates; people in poverty; inequality; mean household net income in each side's own deciles; standard reform costings (change in gov_balance); standard reforms: change in each reform's target. Checks failing on both base and head (1)
Counted-once checkEach of 92 components was raised by £1 per unit in 2026-27, and every head of its totals was read household by household: its own head must rise by exactly £1, no other tax head may move, no other benefit may rise, and the model's own total must move by what its heads moved. A head that breaks a rule is measured again twice: with every other head held at its baseline value, and with every other head held at its value after the first £1 while the component rises again. It fails where either still shows the response. 0 failed; 0 were skipped.
Provenance
Dataset: Receipts by tax head: head levels
Spending by benefit and service: head levels
Caseloads: head levels
Government balance, each component counted once: head levels
Model totals: head levels
Household income totals: head levels
Population: head levels
Poverty rates: head levels
People in poverty: head levels
Inequality: head levels
Mean household net income in each side's own deciles: head levels
Standard reform costings (change in gov_balance): head levels
Standard reforms: change in each reform's target: head levels
|
…p-terminal-date-19
|
Parked until after the 28 October Budget under the UK hub's Budget triage (04:10Z 22 Oct: the QYP set is not Budget-material). Status at c710718:
|
juaristi22
left a comment
There was a problem hiding this comment.
Reviewed c710718dccbfb6e0b8024062e503e61639f7729c. Review COMPLETE: no confirmed code blocker; one nonblocking finding remains.
A1 — Refresh the resolved Axiom fixture blocker. The PR and #355 addendum say the nineteen-year-old negative companion cases are waiting because #446 has the same file open. #446 merged on 11 October. Please update the description/addendum to the current fixture handoff and actual blocker, if any. The encoded UC terminal-date provision itself is already correct; this is a stale completion record, not a request for a billed encoder run.
Six native final UC/PC scenarios passed. They confirm the documented requirement to supply complete programme-level vectors when composing terminal-date overrides. The local combined tests timed out after 120 seconds with 63 passing progress markers; the 17 successful checks at this exact head, including the frozen all-node Result audit, separately cover the unfinished repository tests. I have not treated the timeout as a full local pass or independently certified the private population-impact claims.
I am leaving a comment rather than approving: A1 remains, the PR is a draft, and the declared #2279 merge-order/integration condition still needs resolving. The resulting merged head will need its own current checks.
|
Picking this up for the UK hub. Reply here if you're mid-change and I'll stop. Generated by Claude Code |
Keep #2279's own-right documentation for Pension Credit's young person and this branch's 1 September end; drop the sentence that said the model did not apply that end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6Jra6ok6eCQvZUMMLg
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6Jra6ok6eCQvZUMMLg
juaristi22
left a comment
There was a problem hiding this comment.
Followed up at unchanged head c710718dccbfb6e0b8024062e503e61639f7729c. Review COMPLETE, with no open findings or material evidence gaps.
A1 RESOLVED: the description and actual #355 addendum now correctly state that #446 merged and identify the open test-only rulespec-uk#529 for the three companion cases. This closes the stale handoff finding; it does not imply that #529 has merged.
The prior code and policy review still applies at this head. No repeated model runs were needed. Current-head CI passes; approval remains deferred while this PR is a draft.
|
Generated by Claude Code |
|
Generated by Claude Code |
|
Ready for hub review Head Commits
Findings
MutationsEach mutant is one change in a scratch worktree at d5b8d12, run against
Tests at 93d59f0
Skipped
Judgment calls
Generated by Claude Code |
|
Ready for hub review Verdict: APPROVE WITH NITS at The delta (merge FindingsP2P2-1. The "Aggregate impact report (CI)" section names the wrong commits and says the report doesn't cover this head; it does now. The linked comment (updated 2026-11-03T04:04:10Z, when the Replace the section's paragraph with:
P3P3-1. Invariant 3 says the Income Support split holds "on either side of the date", but after the date nobody has a status to keep. In the body's invariant 3, replace the last sentence with: "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." P3-2. A YAML case at 19 would also pin the Income Support exclusion. The body's "none" is right only for the terminal-date file. Dropping - name: Case 15, a pensioner gets no Pension Credit child addition for a 19-year-old at school before the date who reports their own Income Support.
# SPC reg. 4A(5) lists income support; UC reg. 5(5) does not.
period: 2025
absolute_error_margin: 0.01
input:
people:
pensioner:
age: 70
is_claimant_or_partner: true
student:
age: 19
current_education: UPPER_SECONDARY
age_started_or_accepted_current_education_or_training: 17
income_support_reported: 5_000
is_claimant_or_partner: false
benunits:
benunit:
members: [pensioner, student]
output:
is_before_first_september_following_19th_birthday: [false, true]
is_on_income_support: [false, true]
is_qualifying_young_person_for_universal_credit: [false, true]
is_qualifying_young_person_for_pension_credit: [false, false]
child_minimum_guarantee_addition: 0
P3-3. The scope note says these tests set Universal Credit's terminal-date input false, but two tests set both values. P3-4. In the changelog fragment, the programme acts. P3-5. The Checked and sound
The first three match the body's table. The grid's property tests don't set reported awards, so only YAML kills the last three.
What I couldn't check
Generated by Claude Code |
…otes - Add Case 15: in 2025-26, a 19-year-old at school before the date who reports their own Income Support is a UC qualifying young person but not a PC one (SPC reg. 4A(5) lists Income Support; UC reg. 5(5) doesn't). - Invariant 3's docstring: the Income Support split applies to a young person who otherwise qualifies, on either route. - The scope note: the tests set UC's terminal-date input false unless a test varies it. - The changelog fragment: the model applies the date to Pension Credit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
juaristi22
left a comment
There was a problem hiding this comment.
Re-reviewed exact head ee0482e75e0230e01ae55598df8d24e0089c2566. Review COMPLETE, with no open findings or material evidence gaps. A1 remains resolved: the actual fixture handoff correctly identifies the open rulespec-uk #529 work.
The newly merged own-award/QYP handling was checked through final Universal Credit, Pension Credit and household net income. All 66 selected regressions passed in the qualified local environment (core3.33.12 versus lock3.34.1); the focused final-benefit checks also passed in an existing environment matching all checked head-lock versions. The unchanged calendar evidence was retained.
The PR remains a draft, and current-head hosted CI is still running. Approval is withheld until its stated stack/readiness conditions and required checks are satisfied.
juaristi22
left a comment
There was a problem hiding this comment.
Re-reviewed exact head ee0482e75e0230e01ae55598df8d24e0089c2566 after the source-handoff update. Review COMPLETE, no remaining findings or material evidence gaps; A1 remains resolved.
The actual description now accurately cites rulespec-uk #531 as queued. #529 was closed without merging after its test-only change failed the signed-manifest guard. #531 carries the three source-backed companion cases, pinned module/corpus target and explicit signed-run approval blocker; these cases are queued, rather than merged encoding proof. The 355/360 records and unchanged primary-law expectations were checked.
The model head is unchanged. Valid prior evidence—66 passing selected regressions and the matching-runtime final UC/Pension Credit/income checks—was retained without rerunning it. Current-head CI passes. Approval remains deferred while this is a draft; its declared #2279-first stack condition must also be satisfied.
ee0482e adds Case 15 to qualifying_young_person_terminal_date.yaml (a 19-year-old at school before the date who reports their own Income Support is a UC qualifying young person but not a PC one), tightens invariant 3's docstring and the terminal-date changelog fragment, and changes the scope note of test_child_and_adult_definitions_properties.py. One conflict, in that scope note: this branch's text, which names the Child Benefit and CTC leaver inputs and their tests, with ee0482e's change that the tests set UC's terminal-date input false unless a test varies it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QmsZJYMi6pBmPsiyFLGCQK
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. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6HKeDniUizUaZcFw4Q
juaristi22
left a comment
There was a problem hiding this comment.
Review status: COMPLETE at 25cb0070ee3db1e69a0f86be1f8a39073cc120dd. No open findings or material evidence gaps.
The 11 feature files match the previously reviewed head byte-for-byte. I reviewed their integration with current main, the updated description and applicable source records, retaining valid earlier evidence. A1 remains resolved.
23/23 current-head checks passed on lock-matched Core 3.34.1. This count is separate from the retained historical validation. The independent source review supports the UC/Pension Credit date distinction and the documented annual-date, partial-input and crossed-input limitations. Queued Axiom work remains queued; historical aggregate results are attributed to their stated heads.
No approval is issued while this PR remains a draft. The last CI capture included pending checks; those can be checked later without repeating this completed review. The described delivery order, or the combined #2302 delivery that carries both parents, remains explicit.
juaristi22
left a comment
There was a problem hiding this comment.
Review status: COMPLETE — approved at 25cb0070ee3db1e69a0f86be1f8a39073cc120dd. 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.
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 onmainsets 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; 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; 25cb007 mergesmain47a2817 into this branch. This PR's own commits: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:
This PR places the date from the date of birth for both programmes. Against its base (
main4d80b29 with #2279 merged), 2026-27, a lone parent aged 45 with a 19-year-old at school (the same onmain9475c95 before #2285):is_qualifying_young_person_for_universal_credit(the 19-year-old)uc_individual_child_elementis_uc_work_allowance_eligibleis_qualifying_young_person_for_pension_creditIt follows up two of #2279's items: "Pension Credit reg. 4A(1)(b)'s 1 September" and the Universal Credit default.
The law
Change
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 readsdate_of_birth, orageandmonths_since_last_birthday, throughutils.dates.birth_day, and mirrors Count 16-year-olds outside education as UC and Pension Credit qualifying young persons until the 1 September after their 16th birthday #2279's 16th-birthday fact.is_before_universal_credit_qualifying_young_person_terminal_datewas an input withdefault_value = False. It now has a formula that returns the date fact. A dataset or situation can still set it.meets_qualifying_young_person_terminal_date_condition_for_pension_credit(under 19, or the date fact), which the education route inis_qualifying_young_person_for_pension_creditrequires, with a new parameterterminal_date_age_limit(19).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.
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
Tests
test_qualifying_young_person_terminal_date_properties.py: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):Mutation checks, each applied alone in a copy of the branch (YAML cases and property tests that fail):
>and not>=Case 11 pins the 1 September mutation in YAML as well as in the property test.
Mutations at b5cbd2e: 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 d5b8d12, each one change in a copy, run against
qualifying_young_person_terminal_date.yamlandtest_qualifying_young_person_terminal_date_properties.py:is_on_income_supportdropped from Pension Credit's reg. 4A(5) exclusionMerges into this head. 596bf4f merges
main95dfc62, and 25cb007 mergesmain47a2817, each as its own commit, without conflicts.Run locally at 25cb007:
policyengine-core teston 53 YAML files (every test YAML file the branch changes againstmain47a2817, and every baseline file that namesqualifying_young_person,income_support_reportedorreceives_income_support_in_own_right), 897 passed. Of those,qualifying_young_person_terminal_date.yaml15 passed andchild_qualifying_young_person.yaml8.pytest -n 4, one file at a time:test_qualifying_young_person_terminal_date_properties.py8 passed;test_child_and_adult_definitions_properties.py21;test_benefit_cap_specified_benefits_properties.py46;test_income_support_eligibility_properties.py4;test_legacy_award_readers_properties.py20;test_means_test_income_properties.py2;test_pension_credit_young_person_income_support.py10;policyengine_uk/tests/code_health3,165. No skip, xfail or failure..github/check_fix_pr.pyon this title and body: PASS, 4 changed executable tests.uvx ruff@0.17.2 format --check .,check .andcheck --select F811,F821 policyengine_uk: clean.git diff --checkagainstmain47a2817: clean.git merge-tree --write-treewithmain47a2817: no conflict.Local runs at 93d59f0 (this branch with #2279's head c5b9850 and
mainf4a3170 merged):policyengine-core teston the six test YAML files the branch changes againstmainf4a3170 (Count 16-year-olds outside education as UC and Pension Credit qualifying young persons until the 1 September after their 16th birthday #2279's included):qualifying_young_person_terminal_date.yaml14 passed;child_qualifying_young_person.yaml8;qualifying_young_person_reported_own_awards.yaml23;qualifying_young_person_no_education_route.yaml14;is_uc_work_allowance_eligible.yaml13;means_test_income_members.yaml7. 79 passed, none failed.pytest -n 4:test_qualifying_young_person_terminal_date_properties.py8 passed;test_child_and_adult_definitions_properties.py21;test_means_test_income_properties.py2;test_legacy_award_readers_properties.py20;policyengine_uk/tests/code_health3,093.Local runs at b5cbd2e:
policyengine-core teston the 5 YAML files the branch changes againstmain(Count 16-year-olds outside education as UC and Pension Credit qualifying young persons until the 1 September after their 16th birthday #2279's included): 49 passed. At 6f6e7de, the five YAML files that set these variables: 53 passed.pyteston the 4 Python test files it changes againstmain: 34 passed.policyengine-core teston the 248 YAML files that mention these programmes, 2,685 passed;pyteston 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 Add Child Benefit qualifying young person routes for school leavers #2299's commits fix (8a5980c), and two from a test generator on Count 16-year-olds outside education as UC and Pension Credit qualifying young persons until the 1 September after their 16th birthday #2279, which Count 16-year-olds outside education as UC and Pension Credit qualifying young persons until the 1 September after their 16th birthday #2279 fixed (560005c); both files pass at Test PC, CB and CTC QYP exclusion of young Income Support recipients #2302's head 98a4739 (64 passed).The full suites run in CI.
Microsimulation impact
Aggregate impact report (CI)
In the report, CI runs
mainagainst this PR's head merged into it, on the certified Enhanced FRS 2024-25 (policyengine-uk-data 1.58.1, sha256056e80fd8e91), one full run per side for each year 2025-26 to 2031-32. With 25cb007 merged intomain040e9a5 (bbac69f), 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, inequality or standard reform figure moves.Local runs
These cover b5cbd2e 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:maine01f2f5 with #2279 (9d2fadb) merged (local merge 006c0e813). Branch: b5cbd2e.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