Repository navigation
Conversation
…ducation condition UC Regs 2013 reg. 5(1)(a) and SPC Regs 2002 reg. 4A(1)(a) make a person who has reached 16 a qualifying young person up to the 1 September following their 16th birthday, whatever their education. The model encoded only the education route, so a 16-year-old outside education was not a UC or PC qualifying young person. is_before_first_september_following_16th_birthday places that date from the date of birth and counts the route for a year that starts (6 April) before it: every annual age of 16, and a 17-year-old born from 1 September to 6 October. Both QYP variables OR it with the education route, under the same age band and own-right benefits exclusion. Tests: YAML cases hand-computed from the regulations (flags, date edges, UC child element and work allowance, PC child addition), the property-test oracle, a calendar differential over every birth date in 2008-2011 and across years, and a grid test of sufficiency, monotonicity, exclusion and UC/PC agreement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
Two property tests build a member who is outside every programme's family as a 16 to 19 year old not in education. At 16 that member is now a UC qualifying young person under reg 5(1)(a), so the family's UC gains a child element and the tests' premise no longer holds. Their generators now set is_before_first_september_following_16th_birthday false for that member. The test functions are unchanged, so Hypothesis draws the same examples. 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>
…date-of-birth tests - The route variable's docs now warn that setting it for some people gives everyone else false, and point to date_of_birth for placing one person. They also state that the rule departs from the 6 October reading that is_SP_age uses. - YAML: a 2025-26 case where the 16-year-old ranks first under the two-child limit and the youngest child loses the element (GBP 7,581.72), and a case mixing a person with date_of_birth and one without (both in the route). The second kills a mutant that reads the raw date_of_birth input instead of birth_day. - means_test_income_members.yaml: the 17-year-old case's name says why they are outside the route. 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 |
# Conflicts: # policyengine_uk/tests/test_legacy_award_readers_properties.py
#2089 replaced the shared is_benefit_cap_single_claimant_rate with is_uc_benefit_cap_single_claimant_rate and its Housing Benefit counterpart. The lone-parent case asserts the UC rate, which is still false: the 16-year-old outside education is a qualifying young person the claimant is responsible for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
Standard reform costings (change in gov_balance)
Standard reforms: change in each reform's target
Household net income by decileNo household's net income changes in any year. No change in any year: receipts by tax head; government balance, each component counted once; model totals; components the model counts twice; household income totals; population; poverty rates; people in poverty; inequality; mean household net income in each side's own deciles. Checks failing on both base and head (1)
Counted-once checkEach of 84 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
Components the model counts twice: 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
|
#2549 added other_members_with_awards ("As other_members") for the young singles and working parents properties. It draws a 16-60-year-old outside education, but unlike other_members it did not set is_before_first_september_following_16th_birthday false. A drawn 16-year-old was therefore a UC qualifying young person, so the family changed. In the shrunk case, a single 19-year-old in Wales with GBP 12,000 earnings, housing_benefit_applicable_income went from GBP 11,740 to 0, and the two properties failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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:
|
Fixes #2291
Stacked on #2279. Do not merge before it. This PR builds on #2279's Pension Credit qualifying young person. Until #2279 merges, the diff here also shows its commits. 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 (scratch/probe_D_base_vs_branch.txt; 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_creditThis is a follow-up from #2279 (its 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), required by the education route inis_qualifying_young_person_for_pension_credit, with a new parameterterminal_date_age_limit(19).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, which is the same as the year starting 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
The review of this PR found that setting the date fact for one person gave everyone else in the situation
false: an input set for some people fills the others with the variable's default and skips its formula for them. A second 19-year-old student then silently lost both statuses.The fact's formula gives true for everyone under 20 on 6 October, which is everyone the age limit lets be a QYP. 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 is behind the QYP age limit, which excludes them first. Property test: the formula is true for every age 0 to 19 and every month since the last birthday, in three years, and each variable's default is true; and in 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. The round 2 review found a crossed case: 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 is qualified to 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, twelve cases hand-computed (2026-27):Mutation checks, each applied alone in a copy of the branch (YAML cases and property tests that fail):
>and not>=The 1 September mutation was first caught by the property test alone; Case 11 was added to pin it in YAML.
Run locally 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 were the benefit cap property oracle (fixed on Add the Child Benefit qualifying young person routes for young people who have left education #2299, 8a5980c) and two 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 (fixed there, 560005c); both files pass at End the Pension Credit, Child Benefit and CTC qualifying young person status for Income Support in own right #2302's head 98a4739 (64 passed).ruff format --checkandruff checkonpolicyengine_uk: clean.Round 2 mutations at b5cbd2e (
mutation/prD_r2.txt): each default put back to false fails the two new property tests; the date fact's also fails YAML Cases 3 and 12.The full suites run in CI.
Microsimulation impact
Aggregate impact report (CI)
The report runs
maina8b2848 against this PR merged into it (bfb108d) 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, no moves flagged. No household's net income changes in any year, and no receipt, spending, poverty or inequality figure moves. The one moved figure, the standard reform costing "Reduce Universal Credit taper rate to 20%" (−£1.6m to −£2.2m a year), is #2279's: #2279's own report is identical to this one, line for line, so this PR's own change moves nothing. The commit since then changes tests only (#2279's test generator), so the report carries over to the current head.Local runs
Real runs on Enhanced FRS 2024-25 (uk-data 1.58.0, sha256
5ba399ea5ab9a6c7), one run each for 2025-26 to 2030-31, exact comparison of every record. Base:maine01f2f5 with #2279 (9d2fadb) merged (local merge 006c0e813). Branch: b5cbd2e. Outputs:impact/prD_r2/.No record changes in any year, in any of the Universal Credit, Pension Credit, Housing Benefit, benefit cap, net income or poverty outputs. The reason is the data: 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 (impact/prD_r1/context.txt, on the earlier base). The new date fact is true for all 780 records aged 19 that year, so the Pension Credit condition removes no one either. The PR changes household calculations (the table at the top) and any dataset that does not supply the input.Reproducer: policyengine_uk/tests/test_qualifying_young_person_terminal_date_properties.py::test_universal_credit_and_pension_credit_agree_on_every_combination
axiom: UC reg 5(1)(b) is encoded at
uk:regulations/uksi/2013/376/5#continued_education_or_training_qualifying_condition, but no companion case sets the 19th-birthday input false; the cases are queued on TheAxiomFoundation/rulespec-uk#355 (addendum; test-only, waiting on TheAxiomFoundation/rulespec-uk#446, which has the file open). SPC reg 4A(1)(b) TheAxiomFoundation/rulespec-uk#360 queued (addendum; reg 4A's row is in an unreleased ingest and a billed encoder run needs approval)🤖 Generated with Claude Code