Skip to content

Speed up the slowest property tests - #2819

Merged
juaristi22 merged 5 commits into
mainfrom
claude/speed-up-slow-tests-3il0gj
Nov 5, 2026
Merged

juaristi22 merged 5 commits into
mainfrom
claude/speed-up-slow-tests-3il0gj

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Addresses CI shard time
Reproducer: n/a: this changes tests only
axiom: n/a: tests only

This PR changes two of the slowest test files. It touches no formula, parameter, input, fixture that a production path reads, or CI workflow. Every strategy, max_examples setting and assertion stays.

What the tests do now

test_statutory_pension_edges.py

  • test_nics_cap_preserves_aa_inputs_and_charge_with_fixed_ordinary_tax used to build 16 simulations for each drawn example. It now builds 5. The drawn DC and DB populations still get their own simulation for each cap. One charge simulation now holds every group under both caps. Each person in it has their own benefit unit and household and supplies the same inputs as everyone else, so no charge reads another person's. _charges_with_fixed_ordinary_tax_inputs returns each set's charges and checks each set against excess × 0.45.
  • The two supplied-ANI populations depend on no drawn value. The property used to rebuild them for every example. They now run once a year in a new test, test_nics_cap_preserves_aa_inputs_and_charge_with_supplied_ani[2029|2030], with the same expected arrays. The test asserts equality for each array under both caps; main asserted equality only with no cap, and checked the capped results to within 0.05. The DC witness [130_000, 304_600] gets the same check under both caps. This adds two node ids and renames or removes none, so shard_durations.toml needs no change.

test_childcare_entitlement_floor_properties.py

  • test_more_hours_used_never_lowers_childcare_support used to build three simulations for each example. It now builds one, holding the unbumped, cap-bumped and hours-bumped copies of the families. simulate takes a variants list. Each copy has its own benefit units and households and supplies the same inputs.
  • The edit changed the test's source, so the derandomized draws changed too (Hypothesis seeds them from the source). The new draws missed the class that the mutation below needs. So the test now has an explicit @example with two 3-year-olds in England: one whose family's maximum hours used (40) exceed the working-parent total, and one whose own hours used (20) bind below the family maximum.
  • test_raising_the_income_limit_never_lowers_support_or_income now reads the current-law side from reform.baseline. Simulation.__init__ builds that baseline as Simulation(scenario=None, situation=situation, ...), which matches the separate simulation the test used to build. That is one simulation fewer per example.

What else this PR adds

  • test_income_limit_admits_exactly_the_couples_within_it[100000-True|100001-False] gives a working couple in England a 3-year-old, 30 hours used and a second parent on £20,000, and checks that the child gets extended hours when the first parent's adjusted net income comes to £100,000 and none when it comes to £100,001. SI 2022/1134 reg. 14(3)(c)(i) and reg. 15(3)(b)(i) require that the parent and the partner do not "expect their adjusted net income to exceed £100,000", and gov.dfe.extended_childcare_entitlement.income.limit sets £100,000 for 2026. The gap this test closes predates this PR: the property on main also misses the reversed comparison (see "Mutation checks").

Timings

On CI (.github/test_durations.py over the shard logs), the statutory file takes 2,083 → 1,022 node seconds (−51%) and the childcare file 824 → 476 (−42%). The "after" figures come from the six Python shards of c7f54e405; the "before" figures come from the seven Python shards of run 41579222743, a PR run on main whose branch leaves both files as main has them.

Locally, the machine had 4 cores and ran pytest <file> -n 4 --durations=0. Each file ran three times before and three times after. The figures are medians of node seconds (setup, call and teardown summed). The "before" runs used origin/main at 2da212ab in a separate worktree. This machine runs slower than CI: the property node takes about 1,100 s here and 760 s on CI.

File CI listed s Local before (median node s) Local after (median node s) Change
test_statutory_pension_edges.py 1663 3151 1627 −48%
test_childcare_entitlement_floor_properties.py 685 787 520 −34%
Both 2348 3938 2147 −45%

By node (local medians, before → after):

  • test_nics_cap_preserves_aa_inputs_and_charge_with_fixed_ordinary_tax[2029]: 1109 → 380
  • test_nics_cap_preserves_aa_inputs_and_charge_with_fixed_ordinary_tax[2030]: 1164 → 389
  • test_nics_cap_preserves_aa_inputs_and_charge_with_supplied_ani[2029] / [2030] (new): 45 / 43
  • test_more_hours_used_never_lowers_childcare_support: 359 → 140
  • test_raising_the_income_limit_never_lowers_support_or_income: 286 → 224
  • test_becoming_eligible_never_lowers_childcare_support (unchanged): 143 → 157

Mutation checks

Each check mutated a copy of origin/main, then ran the old test and the new test against it. Shrinking was off (phases=[explicit, reuse, generate]) so failures report quickly. For M4, the old test file is main's and the new one is this head's, each with test_income_limit_admits_exactly_the_couples_within_it appended, run in a copy of this head with only the formula mutated.

Mutation Old test New test
M1: pension_annual_allowance_threshold_income reads pension_contributions_via_salary_sacrifice_adjusted (capped) in place of ..._from_pay both fixed_ordinary_tax nodes fail both fixed_ordinary_tax nodes and both supplied_ani nodes fail
M2: childcare_term_value limits working hours by 60 - family_hours fails without the explicit example: passes (the new draws missed the class); with it: fails in 13 s
M3c: both income-limit conditions (extended_childcare_entitlement_meets_income_requirements, extended_childcare_entitlement_work_condition) also require p.income.limit < np.inf fails fails
M4: extended_childcare_entitlement_meets_income_requirements tests ani > p.income.limit in place of ani <= p.income.limit the income-limit property passes the income-limit property passes; test_income_limit_admits_exactly_the_couples_within_it[100000-True] fails

The income-limit property misses M4, and M4 with the limited-capability route reversed too, in the old and the new test alike, so neither miss is a regression. M4 makes both the baseline and the reform ineligible: the lower earner then fails the reversed test under either limit, and both parents must meet it. Reversing the limited-capability route as well changes nothing more in 2026, because the Department for Education stopped applying the limit on that route from 1 December 2022 (exemption/income_limit_applies.yaml).

Differentials

Two uncommitted scripts compared the old per-case path with the new one.

  • Statutory charge batching: 245 drawn people in 32 result sets over 2029 and 2030. Per-set simulations and one batched simulation gave identical charges (np.array_equal).
  • Childcare hours batching: 269 drawn families. The first 165 came from 70 derandomized examples, the other 104 from 30 examples at seed 20261102. Three separate simulations and one simulation with three copies gave identical values for every output.
  • Income-limit baseline: 200 drawn couples. A fresh current-law simulation and reform.baseline gave identical arrays for the three entitlements and household_net_income.

Checks run

  • Both changed files: all nodes pass in each of the three after runs (43 and 20 nodes).
  • Both changed files at this head, -n 4 --durations=0: 43 passed in 672.26 s (statutory) and 22 passed in 311.98 s (childcare, now 22 nodes with the two new ones). The new nodes' calls take 5.66 s ([100000-True]) and 6.07 s ([100001-False]).
  • pytest policyengine_uk/tests/code_health -n 4: 3087 passed.
  • ruff@0.17.2 format --check ., ruff check . and ruff check --select F811,F821 policyengine_uk pass, as does git diff --check.

Not changed

Open PRs change these files from the brief, so this PR leaves them alone: test_tax_credits_pension_credit_passport_properties.py, test_uc_single_claim_properties.py and test_pension_credit_child_amount_tax_credit_properties.py (#2077), and test_council_tax_reduction_young_person_properties.py (#2301, #2302). No PR has started the rest of the list yet.

🤖 Generated with Claude Code

The NICs cap property builds one charge simulation for all its groups and
both caps, and tests the fixed supplied-ANI populations once a year in their
own test. The childcare hours property simulates its three variants in one
simulation, and the income limit property reads the reform's own baseline.

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

Copy link
Copy Markdown
Collaborator Author

Ready for hub review

Head: c7f54e405a1332492d0ec2038227dd6784b8c9d5 on claude/speed-up-slow-tests-3il0gj (the harness requires this branch name). Base: origin/main at 2da212ab.

Items

  1. test_statutory_pension_edges.py: done. Local median node seconds went from 3151 to 1627 (−48%).
    • _charges_with_fixed_ordinary_tax_inputs (line 266) builds one charge simulation for a list of result sets and checks each set against excess × 0.45.
    • _aa_results_under_both_caps (line 309) and _assert_caps_agree (line 325) hold the code the property and the new test share.
    • The property (line 363) builds 5 simulations per example, down from 16.
    • The new test_nics_cap_preserves_aa_inputs_and_charge_with_supplied_ani (line 403) runs the fixed supplied-ANI populations once a year. It keeps all the original expected arrays and checks them under both caps.
  2. test_childcare_entitlement_floor_properties.py: done. Local median node seconds went from 787 to 520 (−34%).
    • simulate takes variants (line 172).
    • The more-hours property simulates its three variants in one simulation and has a new explicit @example with two families (line 679).
    • The income-limit property reads reform.baseline (line 775).
  3. Changelog: changelog.d/slow-python-tests-2026-11.changed.md.
  4. shard_durations.toml: unchanged. This PR renames or removes no node; it adds two.

Tests run

  • Each changed file ran three times before and three times after with -n 4 --durations=0. Every run passed: 41 → 43 nodes for the statutory file and 20 → 20 for the childcare file. Per-node medians are in the PR body.
  • pytest policyengine_uk/tests/code_health -n 2: 3087 passed.
  • ruff@0.17.2 format --check ., ruff check . and ruff check --select F811,F821 policyengine_uk pass, as does git diff --check.

Mutation and revert checks

Each ran against a scratch copy of main, with shrinking off.

  • M1 (AA threshold income reads the capped sacrifice): the old test fails 2/2; the new tests fail 4/4.
  • M2 (60 - family_hours): the old test fails. The new test passed until I added the explicit example. The edit moved the derandomized draws, and the new draws missed the class. With the example, it fails in 13 s.
  • M3c (both income-limit conditions also require a finite limit): old and new both fail.
  • Three other income-limit mutations survived the old and the new tests alike, so neither version catches them: one reversed comparison on one route (M3, M3b), or on both routes. Both parents must meet the limit, so the mutation leaves the baseline and the reform equally ineligible.

Differentials

None of these scripts is committed.

  • Statutory charge batching: 245 people identical.
  • Childcare batching: 269 families identical (165 derandomized, 104 at seed 20261102).
  • reform.baseline against a fresh simulation: 200 couples identical.

Judgement calls

  • I left out four of the listed files because open PRs change them. Charge in-unit non-dependants in the local Council Tax Reduction schemes, and exclude a lodger personally only outside the head's family #2077 changes test_tax_credits_pension_credit_passport_properties.py, test_uc_single_claim_properties.py and test_pension_credit_child_amount_tax_credit_properties.py. Add CB and CTC qualifying young person routes for school leavers #2301 and Test PC, CB and CTC QYP exclusion of young Income Support recipients #2302 change test_council_tax_reduction_young_person_properties.py.
  • I did not batch test_uc_claimant_elements_properties.py. add_universal_credit_reform draws its 2026 rebalancing seed by benefit-unit position (np.random.default_rng(43)), and some cleared inputs (uc_limited_capability_for_WRA) have formulas, so batching would let variants interact. I stopped at two files rather than take that on loosely. Not started: everything else on the list.
  • Timings are local and noisy. Scratch jobs shared the 4 cores during some of the before and after runs. The absolute seconds exceed CI's, so the hub's CI-log regeneration is the better before/after measure.
  • Hypothesis seeds derandomized draws from the test source, so both edited properties now draw different (same-strategy, same-count) examples than before. M2 showed this matters, which is why the explicit example went in.
  • The brief asked for a "picking this up" comment before pushing. There was no target issue or PR at that point, so I posted none.

CI

I checked once, just after opening the PR. Lint, Changelog fragments, Changes, Fix evidence and Aggregate impact were queued, and no Test shard had reported yet.


Generated by Claude Code

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Aggregate impact on certified data: pass

Base 52b30809ff against head 2d20ec389e, both on enhanced_frs_2024_25.h5 (policyengine-uk-data 1.58.1, sha256 056e80fd8e91), fiscal years 2025-26 to 2031-32. Each year is a full microsimulation run on each side.

Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 0 (thresholds in report_spec.yaml; a flag does not fail the job).

Workflow run and full report

Notes

  • In the years a figure from fewer than 10 records is suppressed (spending.disabled_students_allowance), every total containing it, and the decile incomes, are rebuilt from their other components, so no published total includes it. Those totals are marked †. A change is withheld where the two sides leave out different figures and can't be lined up.

Moves flagged for review

None.

Spending by benefit and service

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed

Caseloads

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed

Standard reform costings (change in gov_balance)

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise Class 1 main employee NICs rate to 10% 0 0 0 0 0 suppressed suppressed
Raise additional rate by 3pp 0 0 0 0 0 suppressed suppressed

The report suppresses a reform's costing and target change in these years, for the reasons below (privacy.py):

  • Raise Class 1 main employee NICs rate to 10%, 2030-31 to 2031-32 (base and head): the reform changes income_tax in fewer than 10 records.
  • Raise additional rate by 3pp, 2030-31 to 2031-32 (base and head): the reform changes universal_credit in fewer than 10 records.

Standard reforms: change in each reform's target

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise Class 1 main employee NICs rate to 10%: change in national_insurance 0 0 0 0 0 suppressed suppressed
Raise additional rate by 3pp: change in income_tax 0 0 0 0 0 suppressed suppressed

Household net income by decile

No 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.

Checks failing on both base and head (1)
  • VAT excluding private school fees is never negative (sign:vat): 2025-26: 76 units below zero, a weighted £-53,888,684 in total; 2026-27: 76 units below zero, a weighted £-55,777,260 in total; 2027-28: 76 units below zero, a weighted £-57,727,917 in total
Counted-once check

Each 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.

  • widowed_parents_allowance: pension_credit responds only through other heads in fewer than 10 households (other heads that moved there: carers_allowance; gov_spending; not a failure)
  • widowed_parents_allowance: pension_credit responds only through other heads in fewer than 10 households (other heads that moved there: carers_allowance; household_benefits; not a failure)
Provenance
Side Commit Runtime Environment hash Partition hash Time
base main 52b30809ff uk 2.172.0 (files f8400aea019e), core 3.34.1, py 3.13.18 aarch64 18199314be15 7cb61324a46b 2058.46s
head #2819 merged into main 2d20ec389e uk 2.172.0 (files f8400aea019e), core 3.34.1, py 3.13.18 aarch64 18199314be15 7cb61324a46b 2073.74s

Dataset: hf://policyengine/policyengine-uk-data-private/enhanced_frs_2024_25.h5@2526edbbb687a3b54dbb11272e5389e4b62d2ac2, sha256 056e80fd8e9151d78f432a25cde863340baee81304c6216a37fb575ce8563784 (verified on both sides), policyengine-uk-data 1.58.1.

Receipts by tax head: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Final charge on cash interest in non-cash ISAs £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Lifetime ISA withdrawal charge £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Income tax £294.46bn £315.23bn £335.82bn £351.28bn £366.96bn £385.36bn £400.00bn
National Insurance (employee and self-employed) £51.70bn £54.14bn £56.13bn £57.73bn £59.76bn £61.64bn £63.51bn
National Insurance (employer) £148.71bn £155.37bn £160.88bn £165.09bn £172.06bn £177.36bn £182.57bn
Employer NI change borne by capital £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Employer NI change borne by consumers £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Capital gains tax £12.60bn £13.15bn £13.60bn £14.11bn £14.67bn £15.24bn £15.80bn
Student loan repayments £7.82bn £8.13bn £8.39bn £8.58bn £8.79bn £9.04bn £9.29bn
VAT excluding private school fees £249.37bn £256.87bn £266.33bn £272.70bn £278.97bn £285.24bn £291.59bn
Change in VAT from its baseline £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
VAT on private school fees £1.95bn £2.01bn £2.09bn £2.14bn £2.20bn £2.26bn £2.31bn
Change in private school VAT from its baseline £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Fuel duties £24.71bn £24.13bn £26.00bn £26.83bn £26.51bn £25.78bn £24.63bn
Alcohol duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Tobacco duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Vehicle excise duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Betting and gaming duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
TV licence fee £4.26bn £4.40bn £4.41bn £4.42bn £4.43bn £4.44bn £4.45bn
Council tax £49.21bn £52.16bn £54.89bn £57.95bn £61.16bn £64.47bn £67.91bn
High value council tax surcharge £0.00bn £0.00bn £0.00bn £0.48bn £0.49bn £0.50bn £0.51bn
Northern Ireland domestic rates £0.55bn £0.58bn £0.60bn £0.63bn £0.66bn £0.69bn £0.72bn
Business rates £34.68bn £36.20bn £38.00bn £39.46bn £42.27bn £43.25bn £44.29bn
Stamp duty land tax on the household's own purchases £6.88bn £7.32bn £7.66bn £8.06bn £8.47bn £8.91bn £9.35bn
Stamp duty land tax paid by companies £4.72bn £4.57bn £5.29bn £6.03bn £6.37bn £6.92bn £7.45bn
Land transaction tax (Wales) £0.07bn £0.08bn £0.08bn £0.09bn £0.10bn £0.10bn £0.11bn
Land and buildings transaction tax (Scotland) £0.31bn £0.32bn £0.33bn £0.35bn £0.36bn £0.38bn £0.40bn
Unspecified tax change borne by capital £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Unspecified tax change borne by consumers £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Wealth tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Wealth tax excluding the main residence £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Land value tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Carbon tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Spending by benefit and service: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Lifetime ISA bonus £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
State Pension £121.68bn £125.92bn £126.72bn £129.49bn £132.99bn £136.66bn £140.42bn
Pension Credit £6.66bn £7.03bn £7.29bn £7.48bn £7.60bn £7.84bn £8.03bn
Winter Fuel Payment £1.86bn £1.85bn £1.79bn £1.77bn £1.78bn £1.78bn £1.78bn
Pension Age Winter Heating Payment (Scotland) £0.19bn £0.20bn £0.20bn £0.21bn £0.21bn £0.22bn £0.22bn
Universal Credit £75.48bn £80.52bn £82.95bn £84.96bn £86.11bn £87.61bn £88.21bn
Housing Benefit £8.56bn £8.89bn £9.23bn £9.46bn £9.69bn £9.99bn £10.27bn
Council Tax Reduction £2.07bn £2.20bn £2.31bn £2.45bn £2.58bn £2.72bn £2.86bn
Employment and Support Allowance (income-related) £0.79bn £0.82bn £0.84bn £0.86bn £0.88bn £0.90bn £0.92bn
Employment and Support Allowance (contribution-based) £5.20bn £5.37bn £5.56bn £5.69bn £5.82bn £5.95bn £6.09bn
Jobseeker's Allowance (income-based) £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Jobseeker's Allowance (contribution-based) £0.29bn £0.30bn £0.31bn £0.31bn £0.32bn £0.33bn £0.34bn
Income Support £0.11bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Working Tax Credit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Child Tax Credit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Incapacity Benefit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Personal Independence Payment £26.95bn £28.03bn £29.05bn £30.09bn £30.75bn £31.51bn £32.21bn
Disability Living Allowance £7.49bn £7.79bn £8.07bn £8.36bn £8.55bn £8.76bn £8.95bn
Attendance Allowance £8.68bn £9.03bn £9.36bn £9.69bn £9.90bn £10.15bn £10.38bn
Carer's Allowance £4.48bn £4.66bn £4.83bn £5.00bn £5.11bn £5.27bn £5.42bn
Carer Support Payment (Scotland) £0.31bn £0.32bn £0.33bn £0.34bn £0.35bn £0.36bn £0.37bn
Carer's Allowance Supplement (Scotland) £0.00bn £0.04bn £0.04bn £0.05bn £0.05bn £0.05bn £0.05bn
Severe Disablement Allowance £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn
Industrial Injuries Disablement Benefit £0.50bn £0.52bn £0.54bn £0.55bn £0.56bn £0.58bn £0.59bn
Armed Forces Compensation Scheme £0.61bn £0.63bn £0.65bn £0.66bn £0.68bn £0.69bn £0.71bn
War Widow's or Widower's Pension £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Child Benefit £16.19bn £16.82bn £17.39bn £18.00bn £18.39bn £18.84bn £19.25bn
Scottish Child Payment £0.49bn £0.52bn £0.53bn £0.55bn £0.56bn £0.57bn £0.56bn
Two Child Limit Payment (Scotland, counterfactual) £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Healthy Start vouchers £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn
Maternity Allowance £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Statutory Maternity Pay £3.30bn £3.41bn £3.54bn £3.62bn £3.70bn £3.78bn £3.87bn
Statutory Paternity Pay £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Statutory Sick Pay £1.12bn £1.16bn £1.20bn £1.23bn £1.26bn £1.29bn £1.32bn
Sure Start Maternity Grant £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn
Bereavement Support Payment £0.67bn £0.69bn £0.71bn £0.73bn £0.75bn £0.76bn £0.78bn
Widowed Parent's Allowance £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Tax-Free Childcare £0.56bn £0.57bn £0.57bn £0.58bn £0.60bn £0.61bn £0.59bn
Extended childcare entitlement £2.99bn £3.50bn £3.59bn £3.71bn £3.78bn £3.86bn £3.93bn
Universal childcare entitlement £2.06bn £2.22bn £2.29bn £2.37bn £2.43bn £2.48bn £2.53bn
Targeted childcare entitlement £0.50bn £0.51bn £0.52bn £0.54bn £0.55bn £0.57bn £0.56bn
Childcare Offer for Wales £0.26bn £0.28bn £0.28bn £0.29bn £0.30bn £0.31bn £0.32bn
Care to Learn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Childcare Grant £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Parents' Learning Allowance £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn
Adult Dependants' Grant £0.06bn £0.06bn £0.04bn £0.04bn £0.04bn £0.04bn £0.04bn
Travel Grant £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed
16 to 19 Bursary Fund £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
NHS spending £203.89bn £204.30bn £204.56bn £204.91bn £205.34bn £205.85bn £206.35bn
State education spending £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn
Transport subsidy spending £25.05bn £25.10bn £25.14bn £25.18bn £25.23bn £25.30bn £25.36bn
Cost-of-living support payment £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Energy Bills Rebate £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Household Energy Payment £0.00bn £2.11bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Energy Price Guarantee subsidy £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Basic income £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Disability basic income £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Other public spending budget change £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Caseloads: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Lifetime ISA bonus 0k 0k 0k 0k 0k 0k 0k
State Pension 11,264k 11,145k 10,819k 10,700k 10,723k 10,750k 10,776k
Pension Credit 1,486k 1,480k 1,472k 1,452k 1,450k 1,455k 1,464k
Winter Fuel Payment 7,844k 7,783k 7,473k 7,375k 7,391k 7,409k 7,427k
Pension Age Winter Heating Payment (Scotland) 803k 802k 800k 797k 799k 801k 802k
Universal Credit 6,342k 6,347k 6,336k 6,297k 6,292k 6,271k 6,223k
Housing Benefit 1,249k 1,245k 1,244k 1,227k 1,227k 1,229k 1,232k
Council Tax Reduction 3,176k 3,195k 3,200k 3,214k 3,224k 3,235k 3,242k
Employment and Support Allowance (income-related) 71k 71k 71k 71k 71k 71k 71k
Employment and Support Allowance (contribution-based) 719k 720k 721k 722k 724k 726k 728k
Jobseeker's Allowance (income-based) 0k 0k 0k 0k 0k 0k 0k
Jobseeker's Allowance (contribution-based) 67k 67k 67k 67k 67k 68k 68k
Income Support 25k 0k 0k 0k 0k 0k 0k
Working Tax Credit 0k 0k 0k 0k 0k 0k 0k
Child Tax Credit 0k 0k 0k 0k 0k 0k 0k
Incapacity Benefit 0k 0k 0k 0k 0k 0k 0k
Personal Independence Payment 3,567k 3,575k 3,579k 3,585k 3,593k 3,602k 3,611k
Disability Living Allowance 1,291k 1,294k 1,296k 1,298k 1,300k 1,304k 1,307k
Attendance Allowance 1,695k 1,698k 1,700k 1,703k 1,707k 1,711k 1,715k
Carer's Allowance 1,034k 1,037k 1,038k 1,040k 1,042k 1,060k 1,062k
Carer Support Payment (Scotland) 71k 71k 71k 72k 72k 72k 72k
Carer's Allowance Supplement (Scotland) 0k 71k 71k 72k 72k 72k 72k
Severe Disablement Allowance 4k 4k 4k 4k 4k 4k 4k
Industrial Injuries Disablement Benefit 137k 137k 137k 138k 138k 138k 139k
Armed Forces Compensation Scheme 93k 93k 94k 94k 94k 94k 94k
War Widow's or Widower's Pension 0k 0k 0k 0k 0k 0k 0k
Child Benefit 8,123k 8,120k 8,120k 8,130k 8,145k 8,161k 8,178k
Scottish Child Payment 351k 352k 352k 353k 350k 348k 337k
Two Child Limit Payment (Scotland, counterfactual) 0k 0k 0k 0k 0k 0k 0k
Healthy Start vouchers 200k 200k 200k 201k 201k 201k 202k
Maternity Allowance 0k 0k 0k 0k 0k 0k 0k
Statutory Maternity Pay 333k 333k 334k 334k 335k 336k 337k
Statutory Paternity Pay 0k 0k 0k 0k 0k 0k 0k
Statutory Sick Pay 181k 182k 182k 182k 182k 183k 183k
Sure Start Maternity Grant 26k 26k 26k 26k 26k 26k 26k
Bereavement Support Payment 98k 98k 98k 98k 98k 99k 99k
Widowed Parent's Allowance 0k 0k 0k 0k 0k 0k 0k
Tax-Free Childcare 1,043k 1,027k 1,014k 1,009k 1,011k 1,009k 993k
Extended childcare entitlement 795k 800k 796k 797k 798k 798k 792k
Universal childcare entitlement 1,071k 1,072k 1,072k 1,073k 1,076k 1,079k 1,074k
Targeted childcare entitlement 165k 156k 154k 154k 155k 155k 150k
Childcare Offer for Wales 39k 39k 39k 39k 39k 39k 39k
Care to Learn 0k 0k 0k 0k 0k 0k 0k
Childcare Grant 0k 0k 0k 0k 0k 0k 0k
Parents' Learning Allowance 98k 98k 98k 99k 99k 99k 99k
Adult Dependants' Grant 18k 18k 12k 12k 12k 12k 12k
Travel Grant 0k 0k 0k 0k 0k 0k 0k
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed
16 to 19 Bursary Fund 0k 0k 0k 0k 0k 0k 0k
Cost-of-living support payment 0k 0k 0k 0k 0k 0k 0k
Energy Bills Rebate 0k 0k 0k 0k 0k 0k 0k
Household Energy Payment 0k 21,065k 0k 0k 0k 0k 0k
Energy Price Guarantee subsidy 0k 0k 0k 0k 0k 0k 0k
Basic income 0k 0k 0k 0k 0k 0k 0k
Disability basic income 0k 0k 0k 0k 0k 0k 0k
Government balance, each component counted once: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Government receipts, each component once £892.00bn £934.66bn £980.50bn £1,015.93bn £1,054.24bn £1,091.56bn £1,124.89bn
Government spending £529.38bn † £545.66bn † £550.77bn † £559.51bn † £567.20bn † £575.95bn † £583.60bn †
Government balance, each receipt once £362.62bn † £389.00bn † £429.74bn † £456.42bn † £487.04bn † £515.61bn † £541.29bn †
Benefits in household_benefits but not in gov_spending £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Model totals: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
gov_tax £892.00bn £934.66bn £980.50bn £1,015.93bn £1,054.24bn £1,091.56bn £1,124.89bn
household_tax £491.97bn £520.41bn £551.21bn £576.01bn £601.00bn £626.70bn £648.42bn
pre_budget_change_household_tax £484.15bn £512.28bn £542.82bn £566.95bn £591.72bn £617.16bn £638.61bn
gov_spending £529.38bn † £545.66bn † £550.77bn † £559.51bn † £567.20bn † £575.95bn † £583.60bn †
household_benefits £529.38bn † £545.66bn † £550.77bn † £559.51bn † £567.20bn † £575.95bn † £583.60bn †
gov_balance £362.62bn † £389.00bn † £429.74bn † £456.42bn † £487.04bn † £515.61bn † £541.29bn †
Household income totals: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Household market income £1,637.96bn £1,702.77bn £1,757.35bn £1,806.41bn £1,855.97bn £1,912.43bn £1,970.44bn
Household net income £1,636.69bn † £1,687.82bn † £1,715.45bn † £1,747.49bn † £1,778.69bn † £1,817.02bn † £1,859.69bn †
Household net income (HBAI definition) £1,449.57bn £1,499.83bn £1,530.86bn £1,565.17bn £1,597.89bn £1,635.60bn £1,676.61bn
Population: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
People 69,245k 69,383k 69,473k 69,592k 69,738k 69,912k 70,080k
Benefit units 35,110k 35,181k 35,226k 35,286k 35,360k 35,449k 35,534k
Households 31,046k 31,108k 31,149k 31,201k 31,267k 31,345k 31,420k
Poverty rates: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Absolute poverty, before housing costs: everyone 16.3% 15.5% 16.4% 16.3% 16.5% 16.5% 16.6%
Absolute poverty, before housing costs: children 24.7% 22.1% 23.3% 22.9% 23.1% 23.2% 23.5%
Absolute poverty, before housing costs: working-age adults 14.1% 13.8% 14.7% 14.7% 14.8% 14.8% 15.0%
Absolute poverty, before housing costs: pensioners 13.7% 13.4% 14.0% 13.8% 13.9% 13.9% 13.9%
Absolute poverty, after housing costs: everyone 21.6% 21.0% 22.0% 22.6% 22.9% 23.1% 23.4%
Absolute poverty, after housing costs: children 33.3% 31.3% 32.3% 32.5% 32.9% 33.4% 34.2%
Absolute poverty, after housing costs: working-age adults 19.9% 19.8% 20.8% 21.6% 21.9% 22.0% 22.3%
Absolute poverty, after housing costs: pensioners 12.7% 12.4% 13.2% 13.2% 13.3% 13.4% 13.6%
Relative poverty, before housing costs: everyone 17.0% 16.0% 16.4% 16.3% 16.4% 16.3% 16.6%
Relative poverty, before housing costs: children 25.9% 22.9% 23.3% 22.9% 23.0% 22.9% 23.5%
Relative poverty, before housing costs: working-age adults 14.5% 14.1% 14.6% 14.7% 14.7% 14.7% 15.0%
Relative poverty, before housing costs: pensioners 14.5% 14.0% 13.9% 13.8% 13.8% 13.9% 13.9%
Relative poverty, after housing costs: everyone 21.4% 21.0% 21.3% 21.3% 21.3% 21.5% 21.7%
Relative poverty, after housing costs: children 33.1% 31.1% 31.2% 31.0% 31.1% 31.2% 31.8%
Relative poverty, after housing costs: working-age adults 19.9% 19.8% 20.2% 20.4% 20.5% 20.6% 20.8%
Relative poverty, after housing costs: pensioners 12.5% 12.4% 12.2% 11.7% 11.7% 11.8% 12.0%
People in poverty: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Absolute poverty, before housing costs: everyone 11,278k 10,779k 11,402k 11,365k 11,476k 11,513k 11,651k
Absolute poverty, before housing costs: children 3,688k 3,314k 3,496k 3,445k 3,480k 3,502k 3,549k
Absolute poverty, before housing costs: working-age adults 5,965k 5,893k 6,308k 6,362k 6,420k 6,430k 6,521k
Absolute poverty, before housing costs: pensioners 1,624k 1,572k 1,598k 1,558k 1,576k 1,580k 1,581k
Absolute poverty, after housing costs: everyone 14,944k 14,600k 15,310k 15,721k 15,937k 16,141k 16,426k
Absolute poverty, after housing costs: children 4,975k 4,682k 4,837k 4,888k 4,945k 5,040k 5,179k
Absolute poverty, after housing costs: working-age adults 8,463k 8,461k 8,971k 9,344k 9,491k 9,584k 9,706k
Absolute poverty, after housing costs: pensioners 1,506k 1,457k 1,502k 1,490k 1,501k 1,517k 1,542k
Relative poverty, before housing costs: everyone 11,750k 11,084k 11,388k 11,364k 11,416k 11,415k 11,651k
Relative poverty, before housing costs: children 3,872k 3,423k 3,496k 3,445k 3,466k 3,454k 3,549k
Relative poverty, before housing costs: working-age adults 6,159k 6,013k 6,301k 6,362k 6,389k 6,389k 6,521k
Relative poverty, before housing costs: pensioners 1,720k 1,648k 1,590k 1,557k 1,562k 1,572k 1,581k
Relative poverty, after housing costs: everyone 14,849k 14,552k 14,776k 14,809k 14,874k 15,014k 15,234k
Relative poverty, after housing costs: children 4,942k 4,660k 4,673k 4,663k 4,679k 4,707k 4,806k
Relative poverty, after housing costs: working-age adults 8,426k 8,440k 8,711k 8,823k 8,873k 8,967k 9,062k
Relative poverty, after housing costs: pensioners 1,482k 1,452k 1,392k 1,323k 1,322k 1,339k 1,365k
Inequality: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Gini coefficient, equivalised HBAI income before housing costs 0.3080 0.3052 0.3058 0.3058 0.3063 0.3069 0.3085
Mean household net income in each side's own deciles: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Decile 1: mean household_net_income per household £17,681 † £17,997 † £17,915 † £18,152 † £18,307 † £18,531 † £18,717 †
Decile 2: mean household_net_income per household £27,939 † £29,023 † £28,711 † £29,013 † £29,276 † £29,691 † £30,150 †
Decile 3: mean household_net_income per household £34,320 † £35,485 † £36,073 † £36,611 † £37,197 † £37,737 † £38,187 †
Decile 4: mean household_net_income per household £38,933 † £40,687 † £41,723 † £42,727 † £43,334 † £44,132 † £44,432 †
Decile 5: mean household_net_income per household £43,239 † £45,567 † £46,014 † £46,959 † £47,712 † £48,584 † £50,282 †
Decile 6: mean household_net_income per household £47,927 † £49,210 † £50,892 † £51,939 † £52,945 † £53,942 † £54,680 †
Decile 7: mean household_net_income per household £55,662 † £56,571 † £57,442 † £57,670 † £58,729 † £59,657 † £61,082 †
Decile 8: mean household_net_income per household £68,789 † £70,803 † £71,858 † £73,466 † £73,880 † £75,320 † £77,178 †
Decile 9: mean household_net_income per household £78,890 † £81,013 † £80,861 † £82,646 † £85,464 † £86,946 † £89,509 †
Decile 10: mean household_net_income per household £117,005 † £120,339 † £124,742 † £126,632 † £125,971 † £128,600 † £130,938 †
Standard reform costings (change in gov_balance): head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise basic rate by 1pp £6.41bn £6.66bn £6.63bn £6.85bn £7.03bn £7.25bn £7.49bn
Raise higher rate by 1pp £3.61bn £3.87bn £4.00bn £4.22bn £4.44bn £4.71bn £4.83bn
Raise personal allowance by ~800GBP/year £-3.80bn £-3.92bn £-3.98bn £-4.04bn £-4.10bn £-4.23bn £-1.95bn
Raise child benefit by 25GBP/week per additional child £-1.99bn £-1.75bn £-1.55bn £-1.38bn £-1.30bn £-1.18bn £-1.07bn
Reduce Universal Credit taper rate to 20% £-20.77bn £-21.32bn £-21.75bn £-21.99bn £-22.34bn £-22.75bn £-22.94bn
Raise Class 1 main employee NICs rate to 10% £10.89bn £11.30bn £11.71bn £12.01bn £12.31bn suppressed suppressed
Raise VAT standard rate by 2pp £24.89bn £25.71bn £26.63bn £27.24bn £27.87bn £28.50bn £29.14bn
Raise additional rate by 3pp £2.90bn £3.15bn £3.22bn £3.41bn £3.61bn suppressed suppressed
Raise the Universal Credit standard allowance by 10% £-3.06bn £-3.18bn £-3.26bn £-3.35bn £-3.44bn £-3.49bn £-3.50bn
Raise the State Pension by 10% £-7.78bn £-8.01bn £-8.24bn £-8.47bn £-8.82bn £-8.55bn £-8.89bn
Standard reforms: change in each reform's target: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise basic rate by 1pp: change in income_tax £6.55bn £6.86bn £6.79bn £7.00bn £7.20bn £7.44bn £7.66bn
Raise higher rate by 1pp: change in income_tax £3.61bn £3.92bn £4.01bn £4.23bn £4.45bn £4.72bn £4.84bn
Raise personal allowance by ~800GBP/year: change in income_tax £-3.93bn £-4.07bn £-4.13bn £-4.18bn £-4.22bn £-4.40bn £-1.75bn
Raise child benefit by 25GBP/week per additional child: change in child_benefit £2.33bn £2.14bn £1.94bn £1.76bn £1.65bn £1.52bn £1.40bn
Reduce Universal Credit taper rate to 20%: change in universal_credit £20.74bn £21.31bn £21.78bn £22.04bn £22.37bn £22.78bn £22.99bn
Raise Class 1 main employee NICs rate to 10%: change in national_insurance £11.15bn £11.63bn £12.00bn £12.29bn £12.61bn suppressed suppressed
Raise VAT standard rate by 2pp: change in vat £24.69bn £25.51bn £26.42bn £27.03bn £27.65bn £28.28bn £28.91bn
Raise additional rate by 3pp: change in income_tax £2.90bn £3.15bn £3.22bn £3.42bn £3.62bn suppressed suppressed
Raise the Universal Credit standard allowance by 10%: change in universal_credit £3.06bn £3.19bn £3.28bn £3.37bn £3.43bn £3.49bn £3.51bn
Raise the State Pension by 10%: change in state_pension £10.68bn £11.17bn £11.28bn £11.60bn £12.03bn £12.47bn £12.97bn

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Ready for hub review

Verdict: APPROVE WITH NITS at c7f54e405a1332492d0ec2038227dd6784b8c9d5

Neither file loses a check. CI confirms the speed-up: the statutory file drops from 2,083 to 1,022 node seconds (−51%) and the childcare file from 824 to 476 (−42%). Every differential and mutation I reran agrees with the body. The findings below are text fixes, plus one follow-up test for an income-limit mutation that main also misses.

Findings

P1

None.

P2

None.

P3

P3-1. The body understates the supplied-ANI change: the head checks the expected arrays exactly under both caps, and main didn't. (policyengine_uk/tests/test_statutory_pension_edges.py:448; the body's "What the tests do now", second bullet)

main asserted the three supplied-ANI expected arrays, and the DC [:2] == [130_000, 304_600] witness, only on results[-1], the uncapped run, and checked the capped run only against it to within 0.05:

$ git show 2da212ab:policyengine_uk/tests/test_statutory_pension_edges.py | grep -n 'results\[-1\]'
389:                    results[-1]["pension_annual_allowance_threshold_income"],
393:                    results[-1]["pension_annual_allowance_adjusted_income"],
397:                    results[-1]["pension_annual_allowance"],
402:                    results[-1]["pension_contributions_for_annual_allowance"][:2],

The head runs these checks in for results in both_caps: (line 448) and for results in groups["DC"]: (line 394), so it checks both caps exactly. That makes the test stronger, but the body says "checked under both caps as before". Replace:

They now run once a year in a new test, test_nics_cap_preserves_aa_inputs_and_charge_with_supplied_ani[2029|2030], with the same expected arrays, checked under both caps as before.

with:

They now run once a year in a new test, test_nics_cap_preserves_aa_inputs_and_charge_with_supplied_ani[2029|2030], with the same expected arrays. The test checks each array exactly under both caps; main checked them exactly only with no cap, and checked the capped results to within 0.05. The DC witness [130_000, 304_600] gets the same check under both caps.

P3-2. The body counts the surviving income-limit mutations as two; the hand-off counts three. (the body's "Mutation checks", last paragraph)

The hand-off lists M3, M3b and a both-routes mutation; the body says "Two more". I confirmed one of them (M3, below). On the second route, extended_childcare_entitlement_work_condition.py:62 applies the limit only when exemption.income_limit_applies is true, and income_limit_applies.yaml sets it to false from 2022-12-01. So in 2026, the property's only year, reversing that comparison alone can't change any result: that mutant is equivalent there, not a gap in the test. Replace the paragraph with:

Other income-limit mutations survived the old and the new test alike, so neither is a regression. Reversing the comparison in extended_childcare_entitlement_meets_income_requirements makes both the baseline and the reform ineligible: the lower earner then fails the reversed test under either limit, and both parents must meet it. Reversing it on the limited-capability route as well changes nothing more in 2026, because the Department for Education stopped applying the limit on that route from 1 December 2022 (exemption/income_limit_applies.yaml).

P3-3. The changelog fragment leaves out the income-limit property. (changelog.d/slow-python-tests-2026-11.changed.md:1)

The income-limit property in the childcare file also builds one simulation fewer, since it now reads reform.baseline. Replace:

The NICs salary sacrifice cap and childcare hours property tests build fewer simulations, with the same strategies, example counts and assertions.

with:

The NICs salary sacrifice cap, childcare hours and childcare income limit property tests build fewer simulations, with the same strategies, example counts and assertions.

P3-4. Two docstrings use the passive voice, and one describes the change rather than the code. (policyengine_uk/tests/test_statutory_pension_edges.py:370-371 and :404-407)

Replace:

The fixed populations with a supplied ANI are tested once, in
test_nics_cap_preserves_aa_inputs_and_charge_with_supplied_ani.

with:

test_nics_cap_preserves_aa_inputs_and_charge_with_supplied_ani tests
the fixed populations with a supplied ANI.

and replace:

These populations do not depend on any drawn value, so they are tested
once for each year rather than with every generated population.

with:

These populations depend on no drawn value, so this test runs them once
for each year.

P3-5. The body could report the CI timings, which now exist. (the body's "Timings")

The body gives local medians only and calls them noisy. .github/test_durations.py over the head's six shard logs and over the seven shard logs of a recent PR run on main (run 41579222743; main has not changed either file since 2da212ab) gives these sums of node seconds:

head: test_statutory_pension_edges.py 1022 (43 nodes), test_childcare_entitlement_floor_properties.py 476 (20 nodes)
main: test_statutory_pension_edges.py 2083 (41 nodes), test_childcare_entitlement_floor_properties.py 824 (20 nodes)

You could add a row or a sentence: "On CI (.github/test_durations.py over the shard logs), the statutory file takes 2,083 → 1,022 node seconds (−51%) and the childcare file 824 → 476 (−42%)." The "Timings" paragraph also describes how the runs were made ("Other scratch jobs … shared the machine"). With CI figures in the body, that sentence can go.

P3-6 (follow-up, not caused by this PR). An explicit test kills the reversed income-limit comparison that both property versions miss.

I confirmed the survivor in a scratch copy. I changed extended_childcare_entitlement_meets_income_requirements.py:36 from (ani <= p.income.limit) to (ani > p.income.limit) and ran main's file (as kill_main_childcare.py) and the head's (as kill_head_childcare.py), each with the test below appended:

$ uv run pytest kill_head_childcare.py kill_main_childcare.py -k "admits_exactly or raising_the_income_limit"
FAILED kill_head_childcare.py::test_income_limit_admits_exactly_the_couples_within_it[100000-True]
FAILED kill_main_childcare.py::test_income_limit_admits_exactly_the_couples_within_it[100000-True]
2 failed, 4 passed

The four passes are the two test_raising_the_income_limit_never_lowers_support_or_income nodes, which survive on main and on the head, and the two [100001-False] nodes. Without the mutation, all four new nodes pass: two nodes add 3.2 to 4.4 s each (--durations, with three other jobs running on the 4-core machine). Proposed addition to test_childcare_entitlement_floor_properties.py:

@pytest.mark.parametrize("high, funded", [(100_000, True), (100_001, False)])
def test_income_limit_admits_exactly_the_couples_within_it(high, funded):
    """A working couple's 3-year-old gets working parent hours exactly when
    both parents' adjusted net income is at most £100,000 (SI 2022/1134 regs
    14(3)(c)(i) and 15(3)(b)(i))."""
    year = 2026
    people = {
        "parent1": {
            "age": {year: 35},
            "employment_income": {year: high},
            "is_parent": {year: True},
        },
        "parent2": {
            "age": {year: 35},
            "employment_income": {year: 20_000},
            "is_parent": {year: True},
        },
        "child0": {"age": {year: 3}},
    }
    members = list(people)
    situation = {
        "people": people,
        "benunits": {
            "bu": {
                "members": members,
                "maximum_extended_childcare_hours_usage": {year: 30},
            }
        },
        "households": {"hh": {"members": members, "country": {year: "ENGLAND"}}},
    }
    sim = new_simulation(situation=situation)
    extended = float(sim.calculate("extended_childcare_entitlement", year).sum())
    assert (extended > 0) == funded, (high, extended)

The regulation numbers in the docstring come from the model's own documentation string on extended_childcare_entitlement_meets_income_requirements. I could not read legislation.gov.uk from this session, so the follow-up should check them against the regulations.

Checked and sound

  • Same checks, same strength. I read git diff 2da212ab..c7f54e405 for both files.
    • Statutory property: max_examples=10, derandomize=True, the strategies (_pension_person, list sizes 4 to 10) and every assertion are unchanged. The DC witness and the supplied-ANI expected arrays are now checked under both caps, not only with no cap (P3-1). The AA-variable comparison (atol=0.05) and the per-set excess × 0.45 charge check (atol=0.05) are the same; the charge comparison gained an err_msg. The supplied-ANI populations depend only on year, so running them once per year, where main reran them in all ten examples, drops no class.
    • Childcare: PROPERTY_SETTINGS (max_examples=30), the strategies, the assertions and the 0.02 tolerances are unchanged. The more-hours property gains an @example. The income-limit property differs only in base = reform.baseline.
    • Nothing adds a skip, an xfail or a tolerance.
  • The explicit example. For a 3-year-old, the working parent total is 30 hours a week (hours.calc). In the first family, min(30, 45, 40) = 30 and the family maximum, 40, exceeds it. In the second, min(30, 20, 30) = 20, so the child's own hours bind below the family maximum of 30. The code comments match.
  • Batching.
    • Pension charge simulation: every person gets their own b{i}/h{i} (_population_simulation) and supplies the same seven keys, so no input is partial and no formula reads another person's values.
    • Childcare simulate: every copy gets its own b{copy}_{i}/h{copy}_{i}. Every copy carries the same keys. The one partial input, early_years_school_start_deferred, is present in every copy whenever any family defers, which matches each of main's three separate simulations.
    • The only position-seeded randomness the simulation constructor applies is the UC rebalancing seed (uc_reform.py:311, by benefit unit). It affects only uc_LCWRA_element, and none of the variables these tests read depends on it. pip_reform.py is a scenario these tests don't apply.
    • No reform is shared: each _population_simulation call applies its own parameter changes to a cloned system (the cloned_uk_tax_benefit_system fixture, or _PROPERTY_SYSTEM.clone in the childcare file).
  • Code-health rules. The PR adds no functools cache: _PROPERTY_SYSTEM was there before, and neither file has lru_cache or @cache. The new helpers write into no array that calculate returns: np.split returns views, and nothing writes to them. policyengine_uk/tests/code_health passes on the merged tree (below).
  • reform.baseline. The baseline is built by policyengine_uk/simulation.py:446-453, not by policyengine-core: Simulation(scenario=None, situation=situation, dataset=dataset, trace=trace, capital_gains_projection=...). It runs inside new_simulation's patch, so it also gets a clone of _PROPERTY_SYSTEM. Lines 466-470 then change only warning and reform-state flags on it. The property sets no input after construction.
  • Differentials (scratch scripts in head, loading main's module from 2da212ab and the head's, Hypothesis @seed(20261103…), generate phase only):
    • Pension charge: pension charge: 248 people in 40 result sets, mismatching sets 0. The test compares main's per-set _charge_with_fixed_ordinary_tax_inputs with the head's _charges_with_fixed_ordinary_tax_inputs, using np.array_equal, over 10 drawn DC/DB examples in 2029 and 2030.
    • Childcare hours: childcare hours: 100 examples, 263 families, mismatching examples 0. The test compares main's three simulate(..., cap_bump, hours_bump) calls with the head's single variants call, on every output simulate returns.
    • Income-limit baseline: baseline: 200 couples (200 distinct), mismatches 0. The test compares a fresh new_simulation(situation) with reform.baseline, using np.array_equal, on the three entitlements, household_net_income, extended_childcare_entitlement_eligible and extended_childcare_entitlement_meets_income_requirements.
  • Mutations (scratch tree at the head; main's files copied in as old_*.py; shrinking off through a scratch Hypothesis profile):
    • M1, pension_annual_allowance_threshold_income.py:26 reading pension_contributions_via_salary_sacrifice_adjusted: killed. The old test fails 2 of 2 nodes, and the new tests fail 4 of 4 (fixed_ordinary_tax[2029|2030], supplied_ani[2029|2030]); 6 failed in 116.05s.
    • M2, utils/childcare.py:123 60 - family_hours: the head's test fails (killed); the head's test without the @example passes (survives); main's test fails (killed); 2 failed, 1 passed in 129.92s. This matches the body.
    • M3: survives both versions (P3-6).
  • main merged into the head (scratch tree, merge commit ebe3c090 over 52b30809, no conflicts):
    • Both changed files with -n 4: 63 passed in 569.48s (43 + 20 nodes).
    • policyengine_uk/tests/code_health with -n 4: 3091 passed, 3 warnings in 232.27s.
  • CI at the head. All 19 check runs completed success: six Python shards, YAML, Result, Lint, Changelog fragments, Changes, Fix evidence, Aggregate impact, the four smoke imports, Documentation build and Release build. Nothing failed or was cancelled. Shard 1/6 took 70.5 min against the 75-min limit; this PR makes it no slower.
  • Shards at the head (from the PASSED lines; the file's nodes spread over every shard):
    • Shard 1: income-limit property, 194 s.
    • Shard 2: fixed_ordinary_tax[2029] (240 s) and supplied_ani[2030] (26 s).
    • Shard 3: fixed_ordinary_tax[2030], 257 s.
    • Shard 4: supplied_ani[2029], 25 s.
    • Shard 5: becoming_eligible, 150 s.
    • Shard 6: more-hours property, 132 s.
    • The shorter statutory nodes run in all six shards. On main, by comparison, the property nodes took 827 / 798 s (statutory) and 400 / 268 / 156 s (childcare).
  • Fix evidence. I ran main's .github/check_fix_pr.py on the live title, the live body, no labels, and git diff --name-status --no-renames -z from the merge base to the head, exactly as pr_fix_evidence.yaml runs it: Fix evidence: PASS ('Speed up the slowest property tests'). Not a fix PR: no added/modified/type-changed fixed fragment or bug/bug-fix label., exit 0.
  • Text. The title and first three lines match the brief. The node counts (41 → 43, 20 → 20), the "16 → 5 simulations" count (4 groups × 2 caps + 8 charge simulations, against 2 × 2 + 1), the body's description of Simulation.__init__, and the @example description all match the code. ruff@0.17.2 format --check and ruff check pass on both files. shard_durations.toml needs no change, since the PR renames and removes no node.

What I couldn't check

  • The body's local timings (3151 → 1627, 787 → 520 median node seconds over three runs each): they come from the author's machine. I report CI figures (P3-5) and one merged-tree run instead.
  • The body's "fails in 13 s" for M2 with the example: my run printed only the summary line, not that node's time.
  • The author's own differential counts (245 people, 269 families, 200 couples): I ran fresh seeded differentials (above), not their scripts.
  • The regulation citations in P3-6: legislation.gov.uk does not resolve from this session.

Generated by Claude Code

@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

… in the changelog

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

Copy link
Copy Markdown
Collaborator Author

PR_TITLE.txt

Speed up the slowest property tests

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

PR_BODY.md

Addresses CI shard time
Reproducer: n/a: this changes tests only
axiom: n/a: tests only

This PR changes two of the slowest test files. It touches no formula, parameter, input, fixture that a production path reads, or CI workflow. Every strategy, `max_examples` setting and assertion stays.

## What the tests do now

**`test_statutory_pension_edges.py`**

- `test_nics_cap_preserves_aa_inputs_and_charge_with_fixed_ordinary_tax` used to build 16 simulations for each drawn example. It now builds 5. The drawn DC and DB populations still get their own simulation for each cap. One charge simulation now holds every group under both caps. Each person in it has their own benefit unit and household and supplies the same inputs as everyone else, so no charge reads another person's. `_charges_with_fixed_ordinary_tax_inputs` returns each set's charges and checks each set against `excess × 0.45`.
- The two supplied-ANI populations depend on no drawn value. The property used to rebuild them for every example. They now run once a year in a new test, `test_nics_cap_preserves_aa_inputs_and_charge_with_supplied_ani[2029|2030]`, with the same expected arrays. The test checks each array exactly under both caps; `main` checked them exactly only with no cap, and checked the capped results to within 0.05. The DC witness `[130_000, 304_600]` gets the same check under both caps. This adds two node ids and renames or removes none, so `shard_durations.toml` needs no change.

**`test_childcare_entitlement_floor_properties.py`**

- `test_more_hours_used_never_lowers_childcare_support` used to build three simulations for each example. It now builds one, holding the unbumped, cap-bumped and hours-bumped copies of the families. `simulate` takes a `variants` list. Each copy has its own benefit units and households and supplies the same inputs.
- The edit changed the test's source, so the derandomized draws changed too (Hypothesis seeds them from the source). The new draws missed the class that the mutation below needs. So the test now has an explicit `@example` with two 3-year-olds in England: one whose family's maximum hours used (40) exceed the working-parent total, and one whose own hours used (20) bind below the family maximum.
- `test_raising_the_income_limit_never_lowers_support_or_income` now reads the current-law side from `reform.baseline`. `Simulation.__init__` builds that baseline as `Simulation(scenario=None, situation=situation, ...)`, which matches the separate simulation the test used to build. That is one simulation fewer per example.

## What else this PR adds

- `test_income_limit_admits_exactly_the_couples_within_it[100000-True|100001-False]` gives a working couple in England a 3-year-old, 30 hours used and a second parent on £20,000, and checks that the child gets extended hours when the first parent's adjusted net income comes to £100,000 and none when it comes to £100,001. SI 2022/1134 [reg. 14(3)(c)(i)](https://www.legislation.gov.uk/uksi/2022/1134/regulation/14) and [reg. 15(3)(b)(i)](https://www.legislation.gov.uk/uksi/2022/1134/regulation/15) require that the parent and the partner do not "expect their adjusted net income to exceed £100,000", and `gov.dfe.extended_childcare_entitlement.income.limit` sets £100,000 for 2026. The gap this test closes predates this PR: the property on `main` also misses the reversed comparison (see "Mutation checks").

## Timings

On CI (`.github/test_durations.py` over the shard logs), the statutory file takes 2,083 → 1,022 node seconds (−51%) and the childcare file 824 → 476 (−42%). The "after" figures come from the six Python shards of `c7f54e405`; the "before" figures come from the seven Python shards of run 41579222743, a PR run on `main` whose branch leaves both files as `main` has them.

Locally, the machine had 4 cores and ran `pytest <file> -n 4 --durations=0`. Each file ran three times before and three times after. The figures are medians of node seconds (setup, call and teardown summed). The "before" runs used `origin/main` at `2da212ab` in a separate worktree. This machine runs slower than CI: the property node takes about 1,100 s here and 760 s on CI.

| File | CI listed s | Local before (median node s) | Local after (median node s) | Change |
|---|---:|---:|---:|---:|
| `test_statutory_pension_edges.py` | 1663 | 3151 | 1627 | −48% |
| `test_childcare_entitlement_floor_properties.py` | 685 | 787 | 520 | −34% |
| **Both** | 2348 | 3938 | 2147 | −45% |

By node (local medians, before → after):

- `test_nics_cap_preserves_aa_inputs_and_charge_with_fixed_ordinary_tax[2029]`: 1109 → 380
- `test_nics_cap_preserves_aa_inputs_and_charge_with_fixed_ordinary_tax[2030]`: 1164 → 389
- `test_nics_cap_preserves_aa_inputs_and_charge_with_supplied_ani[2029]` / `[2030]` (new): 45 / 43
- `test_more_hours_used_never_lowers_childcare_support`: 359 → 140
- `test_raising_the_income_limit_never_lowers_support_or_income`: 286 → 224
- `test_becoming_eligible_never_lowers_childcare_support` (unchanged): 143 → 157

## Mutation checks

Each check mutated a scratch copy of `origin/main`, then ran the old test and the new test against it. Shrinking was off (`phases=[explicit, reuse, generate]`) so failures report quickly. For M4, the old test file is `main`'s and the new one is this head's, each with `test_income_limit_admits_exactly_the_couples_within_it` appended, run in a scratch copy of this head with only the formula mutated.

| Mutation | Old test | New test |
|---|---|---|
| M1: `pension_annual_allowance_threshold_income` reads `pension_contributions_via_salary_sacrifice_adjusted` (capped) in place of `..._from_pay` | both `fixed_ordinary_tax` nodes fail | both `fixed_ordinary_tax` nodes and both `supplied_ani` nodes fail |
| M2: `childcare_term_value` limits working hours by `60 - family_hours` | fails | without the explicit example: **passes** (the new draws missed the class); with it: fails in 13 s |
| M3c: both income-limit conditions (`extended_childcare_entitlement_meets_income_requirements`, `extended_childcare_entitlement_work_condition`) also require `p.income.limit < np.inf` | fails | fails |
| M4: `extended_childcare_entitlement_meets_income_requirements` tests `ani > p.income.limit` in place of `ani <= p.income.limit` | the income-limit property **passes** | the income-limit property **passes**; `test_income_limit_admits_exactly_the_couples_within_it[100000-True]` fails |

Other income-limit mutations survived the old and the new test alike, so neither is a regression. Reversing the comparison in `extended_childcare_entitlement_meets_income_requirements` makes both the baseline and the reform ineligible: the lower earner then fails the reversed test under either limit, and both parents must meet it. Reversing it on the limited-capability route as well changes nothing more in 2026, because the Department for Education stopped applying the limit on that route from 1 December 2022 (`exemption/income_limit_applies.yaml`).

## Differentials

Scratch scripts compared the old per-case path with the new one. This PR commits neither script.

- Statutory charge batching: 245 drawn people in 32 result sets over 2029 and 2030. Per-set simulations and one batched simulation gave identical charges (`np.array_equal`).
- Childcare hours batching: 269 drawn families. The first 165 came from 70 derandomized examples, the other 104 from 30 examples at seed 20261102. Three separate simulations and one simulation with three copies gave identical values for every output.
- Income-limit baseline: 200 drawn couples. A fresh current-law simulation and `reform.baseline` gave identical arrays for the three entitlements and `household_net_income`.

## Checks run

- Both changed files: all nodes pass in each of the three after runs (43 and 20 nodes).
- Both changed files at this head, `-n 4 --durations=0`: 43 passed in 672.26 s (statutory) and 22 passed in 311.98 s (childcare, now 22 nodes with the two new ones). The new nodes' calls take 5.66 s (`[100000-True]`) and 6.07 s (`[100001-False]`).
- `pytest policyengine_uk/tests/code_health -n 4`: 3087 passed.
- `ruff@0.17.2 format --check .`, `ruff check .` and `ruff check --select F811,F821 policyengine_uk` pass, as does `git diff --check`.

## Not changed

Open PRs change these files from the brief, so this PR leaves them alone: `test_tax_credits_pension_credit_passport_properties.py`, `test_uc_single_claim_properties.py` and `test_pension_credit_child_amount_tax_credit_properties.py` (#2077), and `test_council_tax_reduction_young_person_properties.py` (#2301, #2302). No PR has started the rest of the list yet.

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

Generated by Claude Code

@MaxGhenis

MaxGhenis commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator Author

Ready for hub review

Head: 79da970fc25d632efa29d600e0e19424961cd4ed on claude/review-nits-income-limit-test-l0hiwo (no PR; started from c7f54e405). Two commits over c7f54e405:

  • f1783096: P3-3 and P3-4 text fixes.
  • 79da970f: P3-6 test, in its own commit.

I did not push to claude/speed-up-slow-tests-3il0gj.

Before I started, I checked: the last push to claude/speed-up-slow-tests-3il0gj was at 2026-11-02 14:22Z, more than 6 hours earlier, and no comment said anyone was mid-change.

Findings

  • P3-1 (body): I replaced the supplied-ANI sentence with the review's text word for word ("What the tests do now", second bullet).
  • P3-2 (body): I replaced the "Two more income-limit mutations" paragraph with the review's text word for word. I added row M4 to the mutation table: the reversed comparison, where the old property passes, the new property passes and test_income_limit_admits_exactly_the_couples_within_it[100000-True] fails. I also added one sentence after the table's intro that says how I ran M4.
  • P3-3: changelog.d/slow-python-tests-2026-11.changed.md:1 now carries the review's replacement word for word.
  • P3-4: I put in both docstring replacements word for word, at policyengine_uk/tests/test_statutory_pension_edges.py:370-371 and :406-407.
  • P3-5 (body): "Timings" now opens with the review's CI sentence, plus a sentence that names the two sources: the six Python shards of c7f54e405, and run 41579222743's seven. I dropped the scratch-jobs sentence. The rest of the local paragraph and table stay.
  • P3-6: I added test_income_limit_admits_exactly_the_couples_within_it at policyengine_uk/tests/test_childcare_entitlement_floor_properties.py:791-823, exactly as the review proposed it. I also added a "What else this PR adds" section for it, which says the gap predates this PR, and its count and durations under "Checks run".

Citation and figure check (P3-6)

  • The law: the hub's copy of SI 2022/1134 (in force from 1 January 2026) agrees with the docstring. Reg. 14(3)(c)(i) (the parent) and reg. 15(3)(b)(i) (the partner) each read "expect their adjusted net income to exceed £100,000". I changed nothing.

  • The model: extended_childcare_entitlement_meets_income_requirements.py:38 tests ani <= p.income.limit. income/limit.yaml sets 100_000 from 2015-01-01, and the 2026 parameter reads 100000.

  • The test's two couples, from a scratch run of the test's situation in 2026:

    • high=100_000: ANI [100000, 20000, 0], income condition [True, True, False], extended_childcare_entitlement 5893.8.
    • high=100_001: ANI [100001, 20000, 0], income condition [False, True, False], entitlement 0.

    So the two incomes fall either side of the limit.

Mutation (M4)

I ran this in a scratch worktree of this head, with (ani <= p.income.limit) changed to (ani > p.income.limit) at extended_childcare_entitlement_meets_income_requirements.py:38. In that worktree:

  • kill_head_childcare.py is this head's file.
  • kill_main_childcare.py is main's file at 52b30809, which matches 2da212ab, with the new test appended.
$ pytest kill_head_childcare.py kill_main_childcare.py -k "admits_exactly or raising_the_income_limit" -n 4
FAILED policyengine_uk/tests/kill_head_childcare.py::test_income_limit_admits_exactly_the_couples_within_it[100000-True]
FAILED policyengine_uk/tests/kill_main_childcare.py::test_income_limit_admits_exactly_the_couples_within_it[100000-True]
2 failed, 4 passed in 416.51s
  • The four passes are test_raising_the_income_limit_never_lowers_support_or_income in both files and the two [100001-False] nodes. So the property misses the mutation on main too, and the gap predates this PR.
  • I removed the worktree afterwards. git diff --quiet HEAD -- policyengine_uk/variables confirms the formula is unchanged on the branch.

CI timings (P3-5)

I reran .github/test_durations.py --min-seconds 0 over the job logs and summed the rounded per-node figures by file:

Source test_statutory_pension_edges.py test_childcare_entitlement_floor_properties.py
Head c7f54e405, run 41464163535, 6 shards 1022 s, 43 nodes 476 s, 20 nodes
main, run 41579222743, 7 shards 2083 s, 41 nodes 824 s, 20 nodes

This matches the review exactly: −51% and −42%. compare/2da212ab...7967cc66 (run 41579222743's head) lists neither file, so that run tested main's versions.

Tests at 79da970f

Clean runs, one at a time:

  • test_childcare_entitlement_floor_properties.py -n 4 --durations=0: 22 passed in 311.98 s. The new nodes' calls took 5.66 s ([100000-True]) and 6.07 s ([100001-False]). Their setup fell under 0.005 s; one teardown took 0.01 s.
  • test_statutory_pension_edges.py -n 4 --durations=0: 43 passed in 672.26 s.
  • policyengine_uk/tests/code_health -n 4: 3087 passed, 3 warnings in 325.92 s.
  • .github/check_fix_pr.py: I ran it the way pr_fix_evidence.yaml does, on the final title and body with no labels, using git diff --name-status --no-renames -z 2da212ab HEAD. It printed Fix evidence: PASS ('Speed up the slowest property tests'). Not a fix PR: ..., exit 0. The head's checker matches origin/main's.
  • uvx ruff@0.17.2 format --check .: 2164 files already formatted. ruff check . and ruff check --select F811,F821 policyengine_uk: All checks passed.
  • git diff --check c7f54e405..HEAD: clean.
  • git merge-tree --write-tree --name-only HEAD origin/main (origin/main = 52b30809): tree f4140ec1, no conflicts, exit 0.

Judgement calls

  • Title: unchanged.
  • Body footer: I replaced the old session footer with 🤖 Generated with [Claude Code](https://claude.com/claude-code), as the repository's PR-body rules require.
  • Existing passive-voice hits: the grep found three in text I didn't write, and I rewrote each in the active voice:
    • "shard_durations.toml is unchanged" became "needs no change".
    • "Neither script is committed" became "This PR commits neither script".
    • "These files … are changed by open PRs" and "is not started" in "Not changed" became active sentences.
  • The review's paragraph after M4: I kept it word for word, although its "Other income-limit mutations" now sits after an M4 row in the table. The paragraph explains why the property misses M4.
  • "Checks run": I kept the earlier "three after runs (43 and 20 nodes)" line, which describes c7f54e405, and added a separate line for this head with 22 childcare nodes.

Skipped

  • Nothing in the brief. No test needed a survey dataset.

Generated by Claude Code

@MaxGhenis
MaxGhenis marked this pull request as ready for review November 3, 2026 10:03

@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.

Approving exact head 79da970fc25d632efa29d600e0e19424961cd4ed. Review COMPLETE, with no findings or material evidence gaps, and current required CI passes.

The refactors preserve the assertions, strategies, example counts and tolerances, with separate entities and the DC/DB paths retained. Seven focused checks passed, including the full hours property, both supplied-ANI years, the exact childcare boundaries and fresh-versus-reused baseline/input-isolation diagnostics. The omitted full-file local repeats are covered separately by successful exact-head hosted shards/YAML/Test Result and the recorded static coverage checks.

The review establishes functional coverage and isolation; the author's before/after timing percentages and mutation campaign were not rerun or independently certified. This remains a tests-only change with an appropriate policy/Axiom n/a.

@juaristi22
juaristi22 added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 0318775 Nov 5, 2026
21 of 22 checks passed
@juaristi22
juaristi22 deleted the claude/speed-up-slow-tests-3il0gj branch November 5, 2026 10:51
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.

3 participants