Skip to content

Apply the 6 April 2017 child cutoffs by date of birth - #1904

Merged
MaxGhenis merged 12 commits into
mainfrom
date-of-birth-cutoffs
Oct 5, 2026
Merged

MaxGhenis merged 12 commits into
mainfrom
date-of-birth-cutoffs

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #1903

Builds on #1899 (merged 2026-10-03), which added months_since_last_birthday. This now targets main and has main merged in, including #1907's State Pension Credit qualifying age. Its variables state_pension_credit_qualifying_age and has_attained_state_pension_credit_qualifying_age, its helper months_since_attaining, and meets_savings_credit_age_requirement's "attained 65" test now take the same shared birth instant as State Pension age, so a date_of_birth input sets them consistently. Without one, every pension-age result is main's, which the impact run confirms record by record. #1907's age-only helper utils.state_pension_age.date_of_birth, left without callers, is removed.

Summary

Four rules turn on whether a child was born before 6 April 2017, and a fifth, the Pension Credit mixed-age couple saving, on whether the older partner was born on or before 5 February 1954:

Rule Law Variable
Universal Credit child limit: transitional protection UC Regs 2013 reg 24A(3), in force 6 April 2017 to 5 April 2026 uc_is_child_born_before_child_limit
Higher UC child element for the first child UC (Transitional Provisions) Regs 2014 reg 43 the same variable, via uc_individual_child_element
Child Tax Credit child limit Tax Credits Act 2002 s.9(3A) and (3B); CTC Regs 2002 reg 9 is_CTC_child_limit_exempt
Pension Credit first child addition for the eldest child SPC Regs 2002 Sch IIA para 10 child_minimum_guarantee_addition
Pension Credit mixed-age couple saving: the older member reached the qualifying age by 14 May 2019, so was born on or before 5 February 1954 SI 2019/37 art. 4(1); Pensions Act 1995 Sch 4 para 1 has_mixed_age_couple_pension_credit_saving

The first four compared birth_year (period less whole age) with 2017. So everyone aged 9 in 2026-27 counted as born in 2017, whether born in January (before the cutoff) or May (after it). In survey data, the whole cohort born from October 2016 to October 2017 was treated as born after the cutoff, when about half of it was born before.

This PR compares an exact date of birth with 6 April 2017:

  • date_of_birth (new, YYYYMMDD). Each person was born age years and months_since_last_birthday months before 6 October of the fiscal year. The day of birth is the one starting at or after that instant, so their legal age on 6 October is age. It uses Set State Pension age from date of birth, including the rise to 67 #1899's date grid. It can also be set directly, with age as the person's age on 6 October. Every date-of-birth rule then reads one instant of birth through a shared helper, utils.dates.birth_instant, so a date_of_birth input sets state_pension_age, months_since_state_pension_age, is_SP_age and each cutoff consistently. Age-based rules still read age. A situation that sets date_of_birth for some people gives the others 0, which the helper treats as not given; see the review section. Without an input, state_pension_age is exactly Set State Pension age from date of birth, including the rise to 67 #1899's.

  • born_before date parameters replace the year parameters:

    • gov.dwp.universal_credit.elements.child.limit.born_before
    • gov.dwp.tax_credits.child_tax_credit.limit.born_before
    • gov.dwp.pension_credit.guarantee_credit.child.first.born_before, which was a hard-coded 2017.

    Each is 20170406 and cites its provision.

  • Children are ordered by date of birth. UC Regs reg 24B(1) orders children "by reference to the date of birth ... taking the earliest date first", and Sch IIA para 10 names the "eldest". uc_child_index and the Pension Credit child rank now sort by date_of_birth rather than whole age. This only breaks ties between children of the same whole age, but once a cutoff can fall between two such siblings the order matters. With the old ranking, a family with children born 5 and 6 April 2017 could list the later-born child second, and it would get the child element under the limit.

  • birth_year is now the calendar year of date_of_birth. Its model readers on main were the three child cutoffs and the mixed-age saving; all four now compare the exact date instead.

For the child rules, household situations with whole ages are unchanged. A whole age in a situation sits in the middle of the year of age, which is 6 April of the year the person turned that age. So "born before 6 April 2017" gives exactly what period - age < 2017 gave, and birth_year is still period - age. This is tested for every year from 2015 to 2035 and every age up to 19. Only survey data, which spreads birthdays over the year (#1899), moves, along with fractional ages. A fractional age is now read as the exact age on 6 October, as #1899 already does for State Pension age: in 2025, an age of 8.25 places the birth on 6 July 2017, after the cutoff, where the old truncated birth year said 2016. A date of birth can now also be set directly (date_of_birth: 20170405) for a year, with age as the age on 6 October. A value that isn't a calendar date, or that gives a different age, raises an error rather than being silently normalised.

Invariants (stated and tested)

policyengine_uk/tests/test_date_of_birth.py:

  1. Households with whole ages match the birth-year rule. Exhaustively, for 2015-2035 and ages 0-19:

    • born before the cutoff ⇔ period - age < 2017;
    • is_CTC_child_limit_exempt agrees, and so does uc_is_child_born_before_child_limit for under-16s;
    • birth_year == period - age.

    This differential test pins "whole-age situations unchanged".

  2. The date of birth follows the legal age (Hypothesis). For any whole age 0-15, any position in the year (including instants within a day) and any year 2015-2040:

    • date_of_birth equals a datetime reference that shares no code with utils/dates.py, though it re-implements the same grid rule;
    • the legal-age check below is the fully independent one;
    • the person has legal age age on 6 October;
    • birth_year is its year;
    • the UC and CTC flags hold exactly when the date is before 6 April 2017.
  3. Ordering refines age order (Hypothesis). uc_child_index is a permutation of 1..n. A child born earlier is always numbered first, and so is an older whole age.

  4. Microdata shares match the statute. In a data-built simulation with weights, for 2022, 2024, 2025, 2026 and 2028 and ages 5-12:

    • the weighted share of each age born before 6 April 2017 equals the share of that year of age's days before it (about 181/365 for the straddling cohort, 0 or 1 for the others), within the largest weight plus one day;
    • the share with birth_year the year before period - age equals the share of birthdays from 7 October to 31 December;
    • every record's UC and CTC flags equal date_of_birth < 20170406.
  5. A date input matches the same birthday placed by age (Hypothesis, births_and_year). For any year from 2015 to 2040, and people of either sex born from 1935 to the year before, setting date_of_birth (with the legal age) gives the same birth_year, is_SP_age, has_attained_state_pension_credit_qualifying_age, meets_savings_credit_age_requirement, UC flag and CTC flag as setting the matching months_since_last_birthday. state_pension_age, state_pension_credit_qualifying_age and months_since_state_pension_age agree exactly (to 1e-9). A woman's SPC qualifying-age attainment equals her is_SP_age. A person left without the input in the same situation reads date_of_birth 0 but gets the results they would with no input at all.

    A uniform draw rarely reaches the transitional cohorts in their attainment year, where a wrong birth instant would change SPC attainment, so two are pinned as @examples: a woman born 10 December 1952 in 2015, and a man born 10 November 1960 in 2027. A mutant whose attainment ignores the date input fails on both. The Savings Credit "attained 65" half can't be told apart on this path, because the input check already forces the legal age to equal age.

Boundary YAML tests (born 5 against 6 April 2017):

File Cases
demographic/date_of_birth.yaml 9: the derivation, a December birthday, a fractional age, the UC and CTC cutoffs reached through age and months_since_last_birthday, a date set for one person only (the parent keeps State Pension age 68, not over it), and a date setting State Pension age exactly (5 January 1960: 66, attained 5 January 2026, 9.032 months before 6 October)
uc_is_child_born_before_child_limit.yaml 6 new: a third child either side of the limit (2025-26); same-age siblings ordered by date of birth; the higher first child element either side (2026-27); a sibling without a date input placed by age
tax_credits.yaml 4 new: the exemption flag and CTC_child_element (2024-25)
child_minimum_guarantee_addition.yaml 3 new: an only child either side, and the eldest of two same-age children (2026-27)

As a mutation check, reverting either ranking to -age fails exactly the two same-age ordering tests.

Microsimulation impact

Enhanced FRS 2024-25 (policyengine-uk-data-private, snapshot ace89433). Each column is a full microsimulation of current main (8090f1c19, which includes #1899 and #1907) and of this branch (297a210f8). The later commits 20e67683d and dc2ae4e22 change no model code: they remove an uncalled helper and touch only tests, docs and changelog. These figures therefore stand at the head. Children in poverty use HBAI's definition (is_hbai_dependent_child).

This branch minus main

2024-25 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31
Children in the straddling cohort (period - age = 2017), level k 751 817 921 753 825 959 774
of whom now counted as born before 6 April 2017 (main: 0) k +373 +404 +472 +382 +417 +478 +384
Children with a UC child element in benefit units receiving UC (main 5,255k, 5,202k) k +92 +40 +0 +0 +0 +0 +0
UC children not receiving one because of the child limit k -91 -38 +0 +0 +0 +0 +0
UC benefit units with a child held back by the limit (main 656k, 698k) k -54 -20 +0 +0 +0 +0 +0
Mixed-age couples losing the Pension Credit saving (survey benefit units) k -1.0 (3) -6.6 (5) -0.0 (1) -2.0 (6) -0.5 (3) -1.4 (6) 0
Universal Credit £bn +0.36 +0.22 +0.04 +0.05 +0.06 +0.07 +0.05
of which the child cutoffs £bn +0.35 +0.15 +0.04 +0.05 +0.06 +0.06 +0.05
of which the mixed-age saving £bn +0.00 +0.06 +0.00 +0.00 +0.00 +0.00 +0.00
Pension Credit (all from the mixed-age saving) £bn +0.00 -0.05 -0.00 -0.02 -0.00 +0.00 +0.00
Child Tax Credit £bn +0.00 +0.00 +0.00 +0.00 +0.00 +0.00 +0.00
Household net income £bn +0.34 +0.15 +0.04 +0.03 +0.06 +0.06 +0.05
Government balance £bn -0.34 -0.15 -0.04 -0.03 -0.06 -0.06 -0.05
People in absolute poverty, BHC k -94 -19 +0 -10 +0 +0 +0
People in absolute poverty, AHC k -59 -0 -48 +2 +0 -9 -1
Children in absolute poverty, BHC k -69 -11 +0 -6 +0 +0 +0
Children in absolute poverty, AHC k -47 -0 -24 +0 +0 -6 -1
Survey households leaving / entering absolute poverty, BHC; AHC households 6/2; 2/0 2/0; 2/0 0/0; 1/0 1/2; 0/2 0/0; 0/0 0/0; 2/0 0/0; 2/0

The split between the two causes is measured, not assumed. Every change in Pension Credit, and the UC "mixed-age saving" row, comes from benefit units whose has_mixed_age_couple_pension_credit_saving changed. Every one of those lost the saving; none gained it.

  • The child cutoffs: Universal Credit.
    • In 2024-25 and 2025-26 the effect is the child limit: about half the straddling cohort is now transitionally protected.
    • From 2026-27 the limit is gone (child_count is infinite from 6 April 2026). The remaining effect is the higher first child element (reg 43) for families whose eldest child is in that half.
    • Child Tax Credit doesn't change: tax credits end on 6 April 2025 in the model, and 2024-25 CTC in the data is £0.07bn.
    • The Pension Credit eldest-child amount moves no award: every Pension Credit change is in a benefit unit whose mixed-age saving changed. About 2.9-3.0k Pension Credit benefit units have a child or qualifying young person in 2024-2030, and 0.54-0.56k have someone under 18.
  • The mixed-age saving.
    • main compared birth_year (period less age) with 1954. In data that admits an older member aged period - 1954, born from 7 October 1953 to 6 October 1954.

    • Only births on or before 5 February 1954 reached the qualifying age by 14 May 2019, so the fix removes births from 6 February to 6 October 1954.

    • In 2025-26, 6.56k benefit units (5 survey records) lose the saving: 3.67k lose a Pension Credit payment, 5.77k gain a UC payment (3.25k do both), and one record's payments don't change. The net effect is -£0.05bn Pension Credit and +£0.06bn UC.

    • These couples also lose Housing Benefit, so their households lose net income overall. Measured over the benefit units whose saving changed (Pension Credit, UC and Housing Benefit), with Council Tax Reduction and household net income taken at household level for their households:

      2024-25 2025-26 2026-27 2027-28 2028-29 2029-30
      Pension Credit, £m +0.0 -51.6 -0.1 -17.6 -0.0 +0.0
      UC, £m +3.7 +62.0 +0.0 +4.0 +0.0 +2.3
      Housing Benefit, £m -11.6 -17.8 +0.0 -6.4 +0.0 -2.8
      Council Tax Reduction, £m +0.0 +0.0 +0.0 -1.3 +0.0 +0.0
      Household net income, £m -7.9 -7.4 -0.1 -21.3 -0.0 -0.5

      This is why household net income rises by less than UC in 2024-25 and 2027-28. These are also the households entering absolute poverty in the table above. Every household that enters is one of them: 2 before housing costs in 2024-25, and 2 before and 2 after housing costs in 2027-28. This was matched by household id in saving_hh_aggregate.py.

    • Without the date fix, deriving birth_year from the exact date would have pushed this rule the other way. Births from October to December 1954 entered it, which a pre-fix run showed as one 10.6k-weight benefit unit gaining £83m of Pension Credit in 2027-28.

  • The pension-age rules are untouched without a date input. Against main, for every record in every year 2024-2030, the following are identical: state_pension_age, months_since_state_pension_age, is_SP_age, state_pension_credit_qualifying_age, has_attained_state_pension_credit_qualifying_age, meets_savings_credit_age_requirement and state_pension. The data carries no date_of_birth, so routing them through the shared birth instant changes nothing there.
  • Ordering by date of birth changes Universal Credit by at most £0.003bn in any year, and no poverty count. This was measured against Set State Pension age from date of birth, including the rise to 67 #1899's head with the rankings reverted to whole age.
  • Poverty counts move in whole survey households, one to eleven a year, so the year-to-year pattern is lumpy.
    • Relative poverty shows the same. Its line, 60% of the person-weighted median, is identical before housing costs in every year. After housing costs it is identical except in 2026-27, where it rises £1.
    • Before housing costs: -173k and -101k people in 2024-25 and 2025-26, then between -79k and 0.
    • After housing costs: -192k and -114k, then between -77k and 0.

The scripts, CSVs and household-crossing counts are in ~/PolicyEngine/_pr_worktrees/dob-cutoffs-axiom/impact/: v3_main.csv, v3_head.csv, v3_summary.txt, v3_*_flips.npz, v3_saving_*.npz, v3_pa_*.npz and v3_lines_rel_*.txt, v3_saving_hh2.txt and, for the UC recipient-children row, v3_hh_*.npz and v3_saving_hh.txt (whose household columns are withdrawn), from run_one_v2.py, flips.py, saving_diag.py, saving_hh_diag2.py with saving_hh_aggregate.py, pension_age_diag.py and lines_rel.py. A PROVENANCE.md there records the commits and the dataset blob. Every figure is from a real run; none is scaled or interpolated. Earlier runs against #1899's head and against main at 325d585e5 gave the same child-cutoff figures to within £0.01bn.

Caveats

  • One record over several years. Ages are held fixed across years in the data, so a record's date of birth moves a year later each period. Transitional protection is therefore a property of each period's cohort, not of a child followed over time.
  • Siblings of the same age. Set State Pension age from date of birth, including the rise to 67 #1899 places each person in their year of age independently, by weight within age and sex. Twins therefore get different dates of birth and can fall either side of the cutoff. The multiple-birth exception (UC Regs 2013 reg 24A(1)(b)(ii) and Sch 12 para 2) isn't modelled either way.
  • CTC family element. Tax Credits Act 2002 s.9(2)(a) limits the CTC family element to claimants responsible for a child born before 6 April 2017, but CTC_family_element pays it to every CTC claimant. That is a separate, pre-existing gap for 2017-18 to 2024-25, and it's queued as a follow-up; the Axiom side is in Encode TCA 2002 s.9(2)(a), (3A), (3B) and CTC Regs 2002 regs 7(2)–(2A) and 9 (family element and child limit by date of birth, 2017-18 to 2024-25) TheAxiomFoundation/rulespec-uk#372.
  • Other exceptions. The other statutory exceptions to the limits (UC Regs 2013 Sch 12: adoption, non-parental caring arrangements, non-consensual conception; CTC Regs 2002 regs 10 to 14) aren't modelled.

Removed parameters

gov.dwp.universal_credit.elements.child.limit.start_year and gov.dwp.tax_credits.child_tax_credit.limit.start_year held the year 2017, and gov.dwp.pension_credit.mixed_age_couples.saving_latest_birth_year held 1954 (added by #1940). They are replaced by the dates born_before (20170406) and saving_latest_birth_date (19540205), and the removed fragment says how to write a reform now. In the local checkouts of policyengine.py, policyengine-api, policyengine-api-v2, policyengine-app-v2, microcosm and policyengine-uk-data, the only other mention is policyengine-app-v2's mock metadata (app/src/mocks/UK_Metadata.json). A saved policy that changes any of them will stop loading, rather than silently doing nothing. The fragment type is removed (a minor release), following #1899's removal of the State Pension age year parameters and this repo's github-prs.md; downstream packages pin exact versions.

Independent review

An Opus 5.5 reviewer (Subfleet, review/standard) requested changes. Each finding and what was done:

Finding Severity Resolution
A date_of_birth set for one person gave the others 0, so a 35-year-old parent got State Pension age −1925 and counted as over it; unset siblings ranked first and counted as born before the cutoff blocking Reproduced. utils.dates.birth_instant now resolves every rule's birth instant: the input where given, otherwise from age and months_since_last_birthday. There are YAML regressions and a Hypothesis property.
A date input inconsistent with the age grid gave a State Pension age measured from two births (5 January 1960 gave 65.75) should-fix Reproduced. state_pension_age and months_since_state_pension_age now share one instant, which gives 66, attained 5 January 2026.
"Household situations unchanged" does not hold for fractional ages should-fix Now scoped to whole ages in the body and changelog. The UC flag was added to the household differential test.
Poverty-crossing and poverty-line figures had no saved output should-fix Saved, with provenance.
removed vs breaking should-fix Kept removed, following #1899's precedent (see above).
Invariant 4's year list; reference independence; revoked regs 24A and 24B nit Corrected here. The code comment and rulespec-uk#371 now note that reg 24B was revoked from 6 April 2026.
The UC age_exemption reform never lifts the limit pre-existing Split into a separate follow-up.

A round-2 review approved, on the condition that the tests pass (they do; see "Tests run"). Its one should-fix: a date_of_birth input was never checked, so 20170229 became 1 March, and 2017 was read as a date in year 1, which put a child over State Pension age. It also wasn't checked against age. Such inputs now raise a ValueError (test_an_invalid_date_of_birth_is_rejected and test_a_date_of_birth_that_contradicts_age_is_rejected). Its wording nits, the confounded before/after comparison and the BHC/AHC mix-up in the poverty sentence, are corrected above.

A final review by GPT-6.1 Sol (Subfleet, review/hard) approved 86523cf3f after merging main and the mixed-age change. It re-ran all the suites, 280,000 no-input comparisons against main, and 73,049 calendar round trips, and confirmed the 5 February 1954 cutoff is inclusive for both sexes. Its should-fixes, all done in c6db31973:

  • reject negative date_of_birth inputs;
  • scope the "unchanged" claim to the child rules;
  • note that reg 24A(3) only took its date wording from 1 February 2019.

Its figure nits (rounding, poverty ranges, the Pension Credit population, the saving breakdown, and a relative-line check that had used the absolute line) are corrected above from new runs.

A Sol delta review of c6db31973 approved, with three documentation nits:

  • AHC rounding;
  • the provenance of the relative lines;
  • the UC reference title's description of reg 40.

All three are fixed, the last in 297a210f8. Main then moved 255 commits, so 297a210f8 merges it and routes #1907's new pension-age helpers through the shared birth instant (see the summary). That merge is what the next review covers.

The Subfleet daemon hung, so that review ran in-session: two independent Opus 5.5 reviewers, each with its own lens.

  • Engine and merge lens: APPROVE. It found:

    • 0 differences against main across 102 arrays, covering six pension-age variables for 4,228 people over 2010-2046, with whole and fractional ages, edge months and both sexes;
    • 0 mismatches against the statute through the date-input path, for 10,186 people over 13 years;
    • all targeted tests passing.

    Its nits are fixed in 20e67683d: the unused age-only helper is removed, the input-path test is extended, and the State Pension docs cover a direct date_of_birth.

  • Claims lens: REQUEST_CHANGES, on this description only. Its points are corrected above from my own re-runs:

    • Housing Benefit and net income for the mixed-age couples;
    • the UC child row counted only among recipients;
    • main's 1954 rule, which covered births from 7 October 1953 to 6 October 1954;
    • the birth_year readers and changelog wording.

A final in-session review of 20e67683d ran with the same two lenses.

  • Code lens: the property test barely exercised SPC attainment on the input path, and the invariant text was stale. Fixed in dc2ae4e22, as above, along with the Class 4 docs sentence.
  • Description lens: the 2025-26 household net income for the mixed-age households was wrong (-£12.6m). My diagnostic had mapped household values to benefit units with map_to, which splits a household's value across its members. Recomputed at household level, it is -£7.4m. The poverty entrants are now explained, and the provenance is completed.

Tests run

At head dc2ae4e22; the head commit dcf391214 only corrects a test comment (the 1960 example's qualifying age, 66 years and 8 months):

  • YAML: policyengine-core test policyengine_uk/tests/policy -c policyengine_uk, every policy YAML: 2,419 passed.
  • pytest: 85 passed across test_date_of_birth.py, test_state_pension_age.py, test_state_pension_credit_qualifying_age.py, test_mixed_age_couple_properties.py and test_pension_age_severe_disability_premium_properties.py.
  • Mutation check: making has_attained_state_pension_credit_qualifying_age ignore a date input fails both pinned examples.
  • Not run locally: the full pytest suite, which CI runs.

axiom: uk/regulation/uksi/2002/1792/schedule/IIA encoded-correct (takes the eldest child's born-before-6-April-2017 status as an input judgment; companion tests post_2017_child_has_no_eldest_child_increase and mixed_children_with_pre_2017_disabled_and_severely_disabled_amounts) | TheAxiomFoundation/rulespec-uk#371 queued (UC Regs reg 24A(3) and 24B, SI 2014/1230 reg 43, reg 36 first-child row) | TheAxiomFoundation/rulespec-uk#372 queued (TCA 2002 s.9(2)(a), (3A), (3B); CTC Regs regs 7(2A) and 9) | TheAxiomFoundation/rulespec-uk#370 queued (SI 2019/37 art. 4(1) saving, extended with the 5 February 1954 condition)

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits September 30, 2026 06:44
The Universal Credit child limit and higher first child element, the Child
Tax Credit child limit and the Pension Credit first child addition turn on
whether a child was born before 6 April 2017. They compared birth_year
(period less whole age) with 2017, which in survey data treats the whole
cohort born October 2016 to October 2017 as born after the cutoff.

Add date_of_birth (YYYYMMDD) from age and months_since_last_birthday, which
state_pension_age now reads too; compare it with new born_before date
parameters; order UC and Pension Credit children by date of birth; derive
birth_year from it. Household situations are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 3 commits September 30, 2026 06:58
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1904 found that a situation setting date_of_birth for some
people gives the others 0: state_pension_age then read a birth date of 0
(a 35-year-old came out over State Pension age), unset siblings ranked
first and counted as born before 6 April 2017. A date_of_birth input
that disagreed with the age grid also gave a State Pension age measured
from two different births.

utils.dates.birth_instant now resolves the instant of birth once: the
date_of_birth input where given (not 0, and not the day age and
months_since_last_birthday already place), otherwise the instant from
age and months_since_last_birthday. state_pension_age and
months_since_state_pension_age share it, and the cutoffs, child order
and birth_year read birth_day. Without an input nothing changes.

Adds YAML regressions (partial input, exact State Pension age from a
date, an unset sibling), a Hypothesis test that a date input matches the
same birthday placed by age, UC flags in the household differential
test, and changelog wording limited to whole ages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round-2 review of #1904: an input was accepted as any positive integer and
normalised silently (20170229 became 1 March, 2017 read as a date in year
1, making a child over State Pension age), and nothing checked it against
age (a child given only a date kept the default age). birth_instant now
raises a ValueError naming the input for a non-date, or for a date whose
legal age on 6 October is not floor(age). Inputs that agree with age are
unaffected, and data never carries the input.

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

Copy link
Copy Markdown
Collaborator Author

Coordination note from #2051 (Child Tax Credit family element, #2050), which adds a fourth reader of the 6 April 2017 cutoff on main.

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Follow-up from #2051's review: when CTC_family_element moves to birth_day, three of #2051's YAML cases (policyengine_uk/tests/policy/baseline/gov/dwp/tax_credits/ctc_family_element.yaml) need date_of_birth inputs instead of birth_year, because the helper ignores entered birth years:

  • "a child aged 7 with an entered birth year of 2016";
  • the two "birth year entered for one child only" cases.

#2051's birth_year > 0 fallback in the formula also goes away. Its property test generates entered birth years too (birth_year_offset in test_ctc_family_element_properties.py), so that generator would switch to date_of_birth.

Base automatically changed from state-pension-age-67-phase-in to main October 3, 2026 19:00
MaxGhenis and others added 2 commits October 3, 2026 16:15
#1899 (State Pension age by date of birth) merged. Conflicts resolved:
- utils/dates.py: main's exact_age_in_months (a fractional age is the
  exact age) with this branch's birth_instant helpers after it.
- months_since_state_pension_age: this branch's shared birth instant,
  documenting main's fractional-age rule.
- child_minimum_guarantee_addition: rank by date of birth over main's
  is_child_or_qualifying_young_person condition (#1896).
- its YAML: main's QYP test and this branch's 5/6 April 2017 cases, with
  the inert birth_year inputs dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1940 (merged after this branch last synced) added the SI 2019/37 art. 4
saving with birth_year <= 1954, a year stand-in for reaching the State
Pension Credit qualifying age by 14 May 2019, i.e. born on or before 5
February 1954. With birth_year now the calendar year of the exact date,
people born October-December 1954 entered that rule; in the Enhanced FRS
one benefit unit (weight 10,598) gained £83m of Pension Credit in
2027-28. Replace the year with saving_latest_birth_date (19540205) and
compare birth_day with it; add 5 vs 6 February 1954 YAML boundaries and
tighten the property test's P8 to the date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 5 commits October 3, 2026 22:13
GPT-6.1 Sol review of 86523cf (APPROVE, should-fixes):
- birth_instant treated any negative date_of_birth as not given; only 0
  means that now, and a negative value raises.
- The changelog said whole-age situations are unchanged; that holds for
  the child rules, not the mixed-age saving (an older member aged period
  less 1954 is placed on 6 April 1954, after the 5 February cutoff).
- The UC born_before reference now says reg 24A(3) took its date wording
  from 1 February 2019; before then it pointed to SI 2014/1230 reg 40,
  whose continuous-entitlement condition is not modelled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main (255 commits) moved State Pension age into utils/state_pension_age
and added the State Pension Credit qualifying age (#1907), each deriving
the birth from age and months_since_last_birthday. Resolved so every
date-of-birth rule shares utils.dates.birth_instant (date_of_birth where
given, 0 = not given, invalid input rejected):
- state_pension_age and state_pension_credit_qualifying_age take the
  instant from birth_instant;
- months_since_attaining now takes the birth instant and year, and
  months_since_state_pension_age and
  has_attained_state_pension_credit_qualifying_age pass it;
- meets_savings_credit_age_requirement's "attained 65" uses it too;
- utils.state_pension_age.date_of_birth reuses birth_instant_from_age;
- the mixed-age saving keeps main's SPC qualifying age condition with
  this branch's 5 February 1954 date cutoff.
Without a date_of_birth input every result is main's. Also narrows the
UC born_before reference title (Sol review nit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In-session review of 297a210 (engine reviewer APPROVE, nits):
- utils.state_pension_age.date_of_birth had no callers after the merge
  and built the birth from age alone, so a future caller would ignore a
  date_of_birth input; delete it (birth_instant is the one path).
- test_a_date_of_birth_input_matches_the_same_birthday_by_age now also
  checks state_pension_credit_qualifying_age, its attainment,
  months_since_state_pension_age and the Savings Credit age test, for
  both sexes and years from 2015 (when SPC qualifying age still differs
  for men born before 6 December 1953).
- Document a direct date_of_birth input in the State Pension docs; fix
  changelog wording and list saving_latest_birth_date among the added
  parameters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Final review of 20e6768 (code lens): the input-path property test
rarely exercised has_attained_state_pension_credit_qualifying_age, since
a wrong birth instant only changes it for transitional cohorts in their
attainment year; a mutant ignoring the date input survived 9 of 10 seeds.
- @example a woman born 10 December 1952 in 2015 and a man born 10
  November 1960 in 2027; the mutant now fails both.
- Assert a woman's SPC qualifying-age attainment equals is_SP_age
  (SPCA 2002 s.1(6)(a)) on the date path.
- Compare the ages and months since State Pension age exactly (atol
  1e-9, rtol 0): the date and months inputs agree to the bit.
- Docs: a date_of_birth input also moves the Class 4 birthday.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round-diff review nit: a man born 10 November 1960 reaches State Pension
age (and the SPC qualifying age) at 66 years and 8 months (Pensions Act
2014 table 3, 6 November to 5 December 1960), on 10 July 2027, not 66
years and 7 months. Comment only.

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

Copy link
Copy Markdown
Collaborator Author

Merging on the gates at head dcf391214.

CI and mergeability. gh pr checks exits 0 with all 7 checks passing (the Test job took 42m17s). The PR is MERGEABLE, its merge state is CLEAN, and it is up to date with main (8090f1c19).

Independent reviews. These are not GitHub reviews; their outputs are in ~/PolicyEngine/_pr_worktrees/dob-cutoffs-axiom/ and the session's workflow journals.

Round Reviewer Head Verdict
r1 Opus 5.5 (Subfleet) 42b358e2 request changes (partial-input bug), fixed
r2 Opus 5.5 ef35551d approve
r3 GPT-6.1 Sol 86523cf3f approve
r4 GPT-6.1 Sol c6db31973 approve
r5 two-lens in-session Opus 297a210f8, the main merge with #1907 engine approved; description findings fixed
r6 two-lens in-session Opus 20e67683d findings fixed
r7 description lens dc2ae4e22 approve
final in-session Opus dcf391214 approve

What the final check executed

  • Women's qualifying-age assertion: no counterexample over 878,455 person-years.
  • Date path against months path: a maximum difference of 0.0 over 1.76 million combinations.
  • Mutants: both caught by the pinned examples.
  • Tests: passed.

Why this needs no decision from Max. It corrects the model to the cited law. Per Max (2026-10-02), bug fixes that move aggregates merge on gates; the impact is stated in the PR body.

Axiom.

@MaxGhenis
MaxGhenis marked this pull request as ready for review October 5, 2026 01:37
@MaxGhenis
MaxGhenis merged commit e24c866 into main Oct 5, 2026
7 checks passed
@MaxGhenis
MaxGhenis deleted the date-of-birth-cutoffs branch October 5, 2026 01:37
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.

Apply the 6 April 2017 child cutoffs by date of birth, not birth year

1 participant