Skip to content

Presume a member 20+ years younger is the claimant's child when no roles are supplied - #2082

Open
MaxGhenis wants to merge 14 commits into
mainfrom
claimant-partner-unflagged-gap
Open

MaxGhenis wants to merge 14 commits into
mainfrom
claimant-partner-unflagged-gap

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Refs #2039. Follow-up to #2040 (merged 3 October), kept separate because it changes calculator results for unflagged inputs, which is a methodology call (Max ruled d842: adopt).

What changes

is_claimant_or_partner gains one more presumption for households entered without relationships. A member at least 20 years younger than the claimant is presumed to be their child at any age, flagged or not (new parameter household.demographic.benefit_unit.presumed_child.minimum_age_gap_at_any_age = 20). It is OR'ed onto #2040's rule (now on main), which is unchanged: 16+ years younger and either under 20, or below a flagged claimant whose flag no young child explains.

Unflagged household (ages) main This PR
50, 25 couple single claimant; the 25-year-old is not claimant or partner
50, 31 (19 apart) couple couple
50, 30 (20 apart) couple single claimant
60, 38, child 10 couple with child lone parent (trade-off below)
60, 38, child 10, roles supplied couple with child couple with child

A member flagged is_parent still takes priority as the partner, so a flagged couple 25 years apart stays a couple. With flags, it also fixes two cases #2040 documents as limitations, provided the adult is 20+ years younger:

  • a flagged lone parent (50) with an adult child (25) and a younger child (12) is now single, not a couple;
  • a flagged grandparent (70) with a daughter (45) and grandchild (10) no longer has the daughter as partner.

At 16-19 years apart the limitation stays (38/20/12 is still read as a couple, YAML case).

Why 20 years

Once a dataset supplies is_claimant_or_partner (PolicyEngine/policyengine-uk-data#524 does, from the FRS adult table), the presumption no longer touches the microsimulation. It only applies to inputs without roles, so it can follow how often a large age gap means a couple. Measured on the enhanced FRS 2024-25 with the survey's own roles (couple = both in one benefit unit as claimant and partner), co-resident HBAI adults with the younger aged 20+:

Age gap Couple share Survey households (couple pair / other pair)
16-19 54.3% 78 / 132
20-24 13.3% 52 / 371
25-29 0.5% 12 / 553
30+ 0.2% 10 / 932
16+ overall 5.4% 152 / 1,516

Shares are of weighted pairs (each pair takes its household's weight). #2040's evidence cited 61% / 12% / 0.6% / 0.2% from the released eFRS snapshot using benefit-unit membership alone; the 20-year boundary is the same either way.

Below 20 years apart, a couple is the more likely reading; from 20, a parent and adult child is. The table describes co-resident adults in the population, not calculator submissions.

Trade-off and who it reaches

An unflagged couple 20+ years apart entered without roles, whose elder is the claimant (the head, or the eldest adult when no adult head is given), is now read as a single claimant plus a non-member adult. A younger explicit head keeps the couple: the gap is measured below the claimant.

Impact (real microsimulation runs)

Seeded build of policyengine-uk-data#524's branch (enhanced FRS 2024-25 with is_claimant_or_partner from the adult table), simulated for 2025 and 2026. Latest run: main at 493f0d2 (which includes #2040) against this branch at 34c83a9. Two earlier runs, at earlier heads of both, gave the same figures. Cells under 10 survey households are suppressed; SPI-synthetic and donor copies don't count toward the 10.

Role supplied (the data this PR should ship with): base (main) vs branch identical. Every compared array is identical in 2025 and 2026 across the versions run. The arrays are: person is_claimant_or_partner, income tax and NI; benefit-unit UC, HB, Pension Credit, Child Benefit, CTC, WTC, IS, benefit cap and relation_type; household CTR, net income, HBAI net income BHC/AHC, benefits, tax and BHC/AHC poverty.

Role dropped (any enhanced FRS without the column):

  • main infers the survey roles exactly for all 113,567 people.
  • This branch splits 33 survey households (108 person records; 96k weighted households in 2025, 97k in 2026). All are survey couples 20+ years apart, the younger 20+, with no parent flag. It makes no other error: no partner added, and no flagged couple, couple under 20 years apart or single claimant affected.
  • relation_type changes in those 33 households. UC, Pension Credit, CTR, household net income and income tax change in fewer than 10 survey households (suppressed).

Calculator-style input: each eFRS household entered as one benefit unit with no roles, flags or heads (2025). Truth is the survey claimant and partner of the eldest member's own benefit unit, because the model's claimant is the eldest adult.

Rule Claimant and partner right (weighted households) Wrong
main 98.13% 919 survey households (587k weighted)
this PR 98.88% 379 survey households (351k weighted)

Where the rules disagree, this PR is right in 612 survey households (382k weighted) and main in 72 (146k weighted).

Sequencing (Max ruled d842: adopt; merge on gates, ship with uk-data#524)

  • policyengine-uk has no default dataset (Simulation() needs one explicitly or via POLICYENGINE_UK_DEFAULT_DATASET). policyengine.py's bundle manifest pins model and data together. On policyengine.py main (6a9c878) the UK default is enhanced_frs_2024_25 from policyengine-uk-data 1.56.16, the dataset policyengine-uk-data#524 changes.
  • The bundle also offers three non-default UK overlays that carry no is_claimant_or_partner: populace_uk_2023 (policyengine/populace-uk-private), enhanced_frs_2023_24 and frs_2023_24 (policyengine-uk-data at 655dd07e). Real runs, Presume a much younger member is a flagged parent's child at any age #2040's head (c18c836) vs this branch (f43e5ec), 2025. Cells under 10 source households are suppressed; populace's ten OA clones count as one source household, and in enhanced_frs_2023_24 only households whose ids are raw FRS 2023-24 serial numbers count (the file doesn't flag its copies).
    • populace_uk_2023: roles change in 18 source households (about 25k weighted households). Benefit and income amounts move in fewer than 10 (suppressed).
    • enhanced_frs_2023_24: roles change in 18 survey households (64 households in the file, counting its unflagged synthetic and donor copies; about 13k weighted). Amounts move in fewer than 10 (suppressed).
    • These are role-less inputs like any calculator household. Max ruled d871: merge on gates and accept this. Any published number based on an overlay should be re-run before that overlay carries is_claimant_or_partner. A separate task adds the role to microcosm's populace UK build.
  • A merge to main publishes policyengine-uk to PyPI on its own (versioning workflow). A standalone consumer pairing that release with an enhanced FRS older than Set default basic income threshold to £0 #524 would see the 33 couples above split. Bundles should take this release together with the data release carrying Set default basic income threshold to £0 #524 (the d833 batch).
  • policyengine-app-v2#1240 adds roles when a builder household is saved; it does not backfill households saved earlier with ages only. Those keep the presumption until re-saved.

Invariants

Executed as Hypothesis properties in test_child_and_adult_definitions_properties.py, plus the exhaustive age × education × flag grid against the updated reference claimants_or_partners:

  1. Any benefit unit: at most two claimants or partners, all HBAI adults (existing).
  2. A flagged claimant: an unflagged member 16+ years younger is never their partner unless a young child explains the flag (Presume a much younger member is a flagged parent's child at any age #2040, unchanged).
  3. Metamorphic: flagging the claimant as a parent never adds a partner (Presume a much younger member is a flagged parent's child at any age #2040, unchanged).
  4. Differential (changed): on FRS-shaped units, the rule equals the rule without the any-age gap except in childless two-adult units 20+ years apart, where the younger adult is no longer the partner if the elder is the claimant. This is the intended violation of Presume a much younger member is a flagged parent's child at any age #2040's "FRS-shaped units are unchanged" property, and is why the data must supply the role.
  5. New: supplied is_claimant_or_partner roles are always kept, and is_couple follows them.

Mutation check (local log; run at 0448203, 31 YAML cases and 13 properties):

Tests

  • YAML (full suite: 2,006 passed locally at this head; benefit_unit_structure.yaml, 32 cases):
    • unflagged 50/25 is single;
    • the 20-year boundary (50/31 couple, 50/30 and 90/70 single);
    • a flagged partner 25 years younger stays the partner;
    • unflagged 60/38/10 is a lone parent, but a couple when roles are supplied;
    • Presume a much younger member is a flagged parent's child at any age #2040's two documented limitations at 20+ gaps are now read correctly, and a 16-19-year variant (38/20/12) keeps the limitation;
    • the under-20 boundary moves to an 18-year gap so it still tests the age limit;
    • a younger explicit head (50) with a partner 20 years older keeps the couple.
  • Other fixtures: every test on main that enters a couple 20+ years apart without roles now supplies is_claimant_or_partner for the two partners, with expectations unchanged. YAML: Lifetime ISA capital (70/45), Pension Credit mixed-age (79/54), and ten 70/50 mixed-age cases (benefit cap for pension-age claimants, CTR pensioner status, HB earnings disregard). Hypothesis generators: Pension Credit reported capital, the mixed-age couple invariants, the benefit cap pension-age partner test and the CTR pensioner earnings disregard (main already supplies roles in the UC State Pension generator). Without roles the presumption splits these couples, as intended.
  • Properties: valid families are now checked against the reference rule exactly, with the one intended exception stated: an unflagged couple whose head is 20+ years older. Two new tests: the exhaustive test_unflagged_adult_pairs_follow_the_20_year_gap (2,556 unflagged pairs aged 20-90), and a 70/45 couple under each head order, inferred and with supplied roles. The reference HBAI helper now needs an identified parent aged 16+, as is_hbai_dependent_child does; the under-16-parent regression also checks the reference.
  • Full suites: PR CI on 73a7b90 (run 37151606551) is in progress. The last fully green run of the whole suite (YAML + pytest) was 37120834812 on f43e5ec, before main added the fixtures listed above.
  • Lint: ruff format and ruff check on the changed Python files.

axiom: n/a: a PolicyEngine presumption for households entered without relationships, not a statutory rule. Benefit law defines the claimant and partner by relationship, not age.

🤖 Generated with Claude Code

MaxGhenis and others added 3 commits October 2, 2026 09:54
…lags

Unflagged inputs (calculators, policyengine.py, the APIs) kept the under-20
limit because survey childless couples carry no flags. Once the dataset
supplies is_claimant_or_partner from the FRS adult table
(policyengine-uk-data), the presumption only applies to inputs without
roles, so it can follow the FRS base rate: among co-resident HBAI adults
(younger 20+), pairs 16-19 years apart are mostly couples (61%) and pairs
20+ apart mostly not (12% at 20-24, under 1% at 25+).

- New parameter presumed_child.minimum_age_gap_at_any_age (20): a member at
  least that much younger is presumed the claimant's child at any age. A
  member flagged as a parent can still be the partner.
- YAML: 50 + 25 unflagged is now single; the 20-year boundary (19 vs 20,
  and at 90/70); a flagged partner 25 years younger stays the partner; an
  unflagged 60/38 couple with a child is a lone parent (documented
  trade-off) unless roles are supplied. The under-20 boundary case moves
  to an 18-year gap so it still tests the age limit.
- Properties: the reference rule follows; the FRS-shape differential now
  says the change touches only childless couples 20+ years apart
  (intended), and supplied roles are always kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…imant-partner-unflagged-gap

The any-age 20-year gap is OR'ed onto #2040's presumption. It changes two of
#2040's documented limitations where the adult is 20+ years younger (a
flagged lone parent with an adult child and a younger child; a flagged
grandparent with a daughter and grandchild): both are now read correctly.
A 16-19-year case keeps the documented limitation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exhaustive over two unflagged adults aged 20-90 (head the elder): the
younger is the partner exactly when under 20 years younger. The Hypothesis
properties rarely draw an exact 20-year adult pair, so threshold mutants
(21, 19, strict >) were caught only by one YAML boundary case; this test
kills all three on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 6 commits October 2, 2026 15:20
…riants

- The 70/45 Lifetime ISA and 79/54 Pension Credit couples supply
  is_claimant_or_partner and assert it, keeping their capital and
  eligibility expectations (the new presumption would otherwise split them).
- The valid-family property now checks membership against the reference
  rule exactly and states the one intended exception (an unflagged couple
  whose head is 20+ years older); module docstring updated.
- Deterministic test: a 70/45 couple with each head order, inferred and
  with supplied roles. YAML: a younger head 20 years below the other adult
  keeps the couple.
- Docstring and differential comment: the split applies when the elder is
  the claimant.
- Reference HBAI helper: an identified parent must be 16 or over, as in
  is_hbai_dependent_child; the under-16-parent regression now also checks
  the reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI on d8fc744: two property tests build couples with random ages and no
roles, so the 20-year presumption split the wide-gap ones (30/67, 78/49,
93/35) and their independent expectations failed. Their generators now
supply is_claimant_or_partner (true for the adults, false for children in
the UC test), since the units are couples by construction. The strict
xfail for #1942 keeps failing only for its own reason.

Review r2: the changelog, test comments and YAML header no longer claim
survey datasets supply the role. Only datasets that carry it (the enhanced
FRS once policyengine-uk-data#524 ships) are unaffected; the bundle's
populace_uk_2023 and 2023-24 FRS overlays carry no role. The FRS-shape
differential exempts only units whose head is 20+ years older, and the
head-order test checks every person.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#2040's docstring example (a flagged 50 with a 25-year-old becomes a
couple when a 12-year-old is added) does not hold with the any-age gap:
a member 20+ years younger stays the claimant's child either way. The
example now uses a 16-19-year gap (38 and 20), checked against the model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merging main brought ten YAML cases (benefit cap for pension-age claimants,
CTR pensioner status, the HB earnings disregard) that enter a mixed-age
couple aged 70 and 50 without roles. Exactly 20 years apart, the younger
partner is presumed the claimant's child, so each case now supplies
is_claimant_or_partner for the two partners. Expectations are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Full YAML suite on the result: 1,935 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from claimant-partner-adult-child to main October 3, 2026 19:07
MaxGhenis and others added 4 commits October 3, 2026 15:19
test_mixed_age_couple_properties and the P6 test in
test_benefit_cap_and_ctr_pension_age_properties build couples with random
ages (for example 67 and 25) and no roles, so the 20-year presumption split
them. Both now supply is_claimant_or_partner for the two partners.

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

One conflict: main also supplies claimant/partner roles in the UC State
Pension property generator; kept main's code and widened its comment to
mention partners 20 or more years younger. Full YAML suite: 1,985 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Full YAML suite on the result: 2,006 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's new test_council_tax_reduction_pensioner_earnings_disregard_properties
builds two-adult families with independently drawn ages and no roles; the
20-year presumption splits the wide-gap ones. The adults now supply
is_claimant_or_partner (children false). 7 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 3, 2026 20:27
PR CI on 73a7b90: test_scottish_carer_supplement_is_not_means_tested_income
drew a pension-age couple 25 years apart (67 and 92) with no roles, so the
20-year presumption split it. Every member of these units is an adult of
the couple, so each now supplies is_claimant_or_partner. 3 passed; it was
the only failure among 2,693 Python tests and 2,006 YAML cases in that run.

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

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

Thanks Max. The rule itself is right and tightly scoped. At 12962b0 I checked it against the docstring and the YAML: gap >= 20 ORed onto #2040's rule (is_claimant_or_partner.py:99-108), with a flagged younger member still taking the partner slot through other_parent. Ages 50/31 stay a couple, 50/30 and 90/70 split, and the boundary uses >=. At the head, the six changed YAML files pass (32 + 24 + 13 + 11 + 8 + 30 cases), and so do the seven changed property files (36 passed, 1 xfail), each run in its own process. WRA 2012 s.39(1) and SSCBA 1992 s.137(1) define a couple by marriage, civil partnership or living together "as if they were a married couple or civil partners", with no age condition, so this is purely a presumption for role-less input. The parameter files say that, and they don't cite statute.

I compared calculator-style situations for 2026 (ages only, one benefit unit and one household) on main b5b0923 and on this branch with main merged in. In every case below, results are identical to the penny once the adults are flagged is_parent, roles are supplied, or the younger adult is the head. With ages only, they move as follows:

Ages (no roles) main this PR
50, 30 couple, UC £8,003.64 single, UC £5,098.80; net income −£2,904.85
50, 30, younger earns £20k couple, UC £0 single, UC £5,098.80; net income +£4,846.80
60, 38, child 10 couple, UC £12,226.20 lone parent, UC £9,321.36
45, 24, child 2 couple, UC £11,650.92 lone parent, UC £8,746.08; net income −£2,904.84
70, 48 mixed-age couple on UC £8,003.64 single pensioner on Pension Credit £12,376.00; net income +£4,372.36
67, 45, rent £9,600 UC £17,003.64 PC £12,376.00 + HB £9,600.00; net income +£4,972.36
67, 45, rent £9,600, younger earns £15k UC £8,989.26 PC £12,376.00 + HB £9,600.00; net income +£12,734.74
  1. Blocking: main has added role-less mixed-age couples that this rule now splits. I merged origin/main (b5b0923) into the head locally. The full YAML suite then gives 2,444 passed and 4 failed. Each failure is a fixture added on main after your last merge (325d585) that enters a 20+ year-gap couple without roles:

    • finance/benefit/family/housing_benefit/housing_benefit_universal_credit_passport.yaml:240 (70/40)
    • finance/benefit/family/housing_benefit/pension_age_premiums.yaml:281 (50/70)
    • finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml:62 (70/45; uc_earned_income comes out 0 against an expected £12,879.60)
    • gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml:544 (Case 23, 50/70; pension_credit comes out £12,376 against an expected 0)
      Also tests/test_council_tax_reduction_carer_income_properties.py:155-165, a generator added on main that draws working-age couples aged 25 to 60 without roles. Hypothesis fails it on a 25/57 pair (full pytest on the merged tree: 2,725 passed, 1 failed).
      All five pass on main. Fix: merge main and supply is_claimant_or_partner: true for both partners in these four YAML cases and in that generator, as you did for the others. That merge also clears the current red Test job: the failure is code_health/test_cached_arrays_not_written_in_place.py (core 3.32.16's put(..., derived=...)), which main fixed at 4ac62cb and which passes after the merge (35 passed). The only textual conflict is a comment in tests/test_uc_state_pension_properties.py (about line 114 after the merge). Resolve just that hunk and don't take --ours for the whole file: main's #1947 rework of invariants 2 and 4 auto-merges into the same file, and taking your side whole makes test_tax_on_state_pension_does_not_reduce_partners_earned_income fail. With the hunk-only resolution, the file passes (5 passed, 1 xfail).
  2. Blocking (merge gate): don't merge until policyengine-app-v2#1240 is merged and deployed. Today the app sends "you" and a declared partner with ages only, and #1240 is still a draft. A merge to main publishes to PyPI by itself, so as soon as the API picks the release up, a real mixed-age couple entered in the calculator (67 and 45) moves from UC to Pension Credit plus full HB, and the partner's earnings drop out of the means test (+£12,735 above). The body already says #1240 "should land before or with this PR"; I'd make that a hard gate in the PR description and hold the merge until #1240 is live. Households saved earlier with ages only also need re-saving (or a backfill) before they pick up roles.

  3. Should fix: the split reading leaves the "adult child" uncharged. Once the 45-year-old is presumed the claimant's child, they are an adult in the claimant's benefit unit but not a claimant or partner, and main charges no non-dependant deduction for that. For the 67/45/£15k case, housing_benefit_non_dep_deductions is £0 and HB is the full £9,600. The same people in separate benefit units get a £3,346.20 deduction and HB of £6,253.80. So the PR's intended reading (parent plus adult child) is costed more generously than the law would cost it, until in-unit non-dependant deductions land (#2017, and #2077 for local CTR). Either sequence this after #2017, or add a sentence to the docstring (is_claimant_or_partner.py:39-46) and the changelog saying the presumed adult child is not yet charged as a non-dependant.

  4. Nit: pin the young-child case. 45, 24, child 2 with ages only now reads as a lone parent with an adult child, and UC drops by £2,904.84. #2040's young-child exception only fires when the claimant is flagged, so it doesn't help unflagged input. The body's evidence is for adult pairs without conditioning on a young child in the unit, so I'm not claiming this is wrong. But it's a common calculator shape, so it deserves a YAML case in benefit_unit_structure.yaml (next to the 60/38/10 trade-off case at line 502) so the behaviour is explicit.

  5. Nit: overlaps. Once main is in, test_uc_state_pension_properties.py is also touched by #2081, #1963, #1999 and #2131, and test_child_and_adult_definitions_properties.py by #2030 and #2016. Whichever lands second should keep the supplied roles in the generators.

The PR body is clean on disclosure: survey-household counts suppressed under 10 and weighted totals only, with no record ids, weights or record-level amounts.

MaxGhenis added a commit that referenced this pull request Oct 6, 2026
The review of 94041e9 found that holding every family's own awards still
let property 4 fail through the same channel, and hid cross-family reads of
awards:
- In Newham, when the head's family stops claiming Universal Credit, the
  non-dependant moves into the sharer's claim and raises its
  uc_maximum_amount, which the four _legacy councils use as a UC family's
  applicable amount. The sharer's council award moved from 241.67 to 417.02
  with its awards held.
- Holding every family's awards also gives a perturbed 85-year-old head the
  award of the 47-year-old it replaced, and a mutant that read another
  family's UC survived.

Property 4 now holds, with each family's CTR means test, the two household
allocations through which other families reach a family's own awards:
uc_non_dependants_counted (UC Regs 2013 Sch 4 para 9(2)) and
has_non_dependant_for_severe_disability_premium. Each family's awards are
computed. The body passes on CI's population, its minimal household and
the reviewer's counterexample, and catches all five mutants the reviewer
and author built.

The generator also sets is_claimant_or_partner on every family's adults, so
wide-gap couples stay couples once #2082 presumes a member 20 or more years
younger to be a child. build() keeps each held value's own type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vahid-ahmadi pushed a commit that referenced this pull request Oct 8, 2026
… and exemption (#2015)

* Assess each Council Tax Reduction claim under its own family's scheme and exemption

In a shared-rent household every family liable for the rent claims, but the
pensioner scheme and the non-dependant exemption were picked once per
household from the head's family. SI 2012/2885 reg 3 (and SI 2012/2886 Sch
para 3, SI 2013/3029 reg 3, SSI 2021/249 reg 3, SSI 2012/319 reg 12) define
"pensioner" per person and partner, and the exemption applies where "the
applicant or his partner" is blind or gets a listed benefit (SI 2012/2885 Sch 1
para 8(6) and equivalents).

- council_tax_reduction_pensioner (BenUnit) replaces the household flag; the
  name and entity match #1944 and #1966, which extend the same definition.
- council_tax_reduction_applicant_has_non_dep_exemption (BenUnit) replaces
  the household exemption; the councils' schemes apply it per claim.
- council_tax_reduction_scheme_supported is per family, so each family falls
  back to its reported reduction only where its own scheme is unmodelled.
- Councils' person-level non-dependant deductions no longer depend on the
  head's family; each claimant's own scheme decides whether to use them.

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

* Reconcile reported and simulated reductions within a household

With the scheme chosen per family, one household can hold a simulated claim
and a family whose scheme is not simulated. Review r1 found that a family
that cannot claim then kept its reported reduction beside a head whose
simulated award already covered the whole bill: £3,000 against £1,800.

- A family that cannot claim keeps a reported reduction only where no claim
  in its household is simulated (new household flag
  council_tax_reduction_household_has_simulated_claim). Without sharers this
  is the base behaviour.
- A jointly liable claimant's reported reduction is limited to its own share
  of the council tax (SI 2012/2885 Sch 1 para 7(3)-(4)), the most any scheme
  could give it.

Tests: three YAML cases; the property tests now generate reported
reductions, check the fallback and the household bound, and toggle one
claimant's exempting benefit to show it leaves the other claim unchanged.

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

* Set a claim's scheme and exemption from the applicant and their partner

#2009 now names the liable people directly. A household head can be liable
without being the claimant or partner of their family (a grandparent heading
a family formed around a young couple), so "the applicant or his partner"
(SI 2012/2885 reg 3, Sch 1 para 8(6)) is not always the family's claimant and
partner. New person-level council_tax_reduction_applicant_or_partner: the
liable member, and the other member of the couple where the liable member is
the claimant or partner, whatever the partner's age; in a family that cannot
claim, its claimant and partner. The pensioner flag and the applicant
exemption read it.

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

* Use #2078's CTR applicant-or-partner variable

#2078 (also stacked on #2009) adds
is_council_tax_reduction_applicant_or_partner and
council_tax_reduction_head_applies_alone for the CTR means test. They define
the same people as this PR's council_tax_reduction_applicant_or_partner: the
claimant and partner where the liable person is one of them, the liable head
alone otherwise, and the claimant and partner in a family that cannot claim.
Take #2078's two files byte for byte, so whichever PR lands second merges
without conflict, and point the pensioner flag and the applicant exemption
at it.

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

* Cap a reported reduction at the claimant's share only beside a simulated claim

Review r2 found that capping every jointly liable claimant's reported
reduction at its share could discard a lawful report: the model's divisor
(#2006's joint-liability share) counts students that SI 2012/2885 Sch 1
para 7(5) leaves out. The cap exists to stop a reported claim and a simulated
claim together exceeding the bill, so it now applies only where another claim
in the household is simulated, on the share that claim uses. Where nothing is
simulated, reports are kept as reported, as on the base.

Tests:
- YAML: a jointly liable claimant keeps its whole report where nothing is
  simulated; a pension-age grandmother in working-age applicants' family does
  not make their claim a pensioner's (an intended change under reg 3(1): the
  base paid £1,800 on the pensioner scheme).
- Properties: the fallback oracle follows the narrowed cap, and property 4
  compares council_tax_benefit wherever the household's reconciliation is
  unchanged.

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

* Apply the reported-reduction cap only beside a simulated claim that pays

On the payer build, the only remaining change came from heads whose reported
reduction was capped beside a sharer whose simulated claim paid nothing. With
no simulated reduction competing for the bill there is nothing to reconcile,
so the cap now applies only where the simulated claims in the household pay
something. The household bound still holds: each simulated award is within
its share, and a capped report within its own.

Tests: a YAML case where the simulated claim beside a report pays nothing;
the fallback oracle, the bound and property 4's comparison follow the paid
condition.

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

* Test the per-family pensioner flag against #1944's reg 3 conditions

council_tax_reduction_pensioner is now #1944's SI 2012/2885 reg 3 test
on this PR's members: the qualifying age for State Pension Credit, and no
Income Support, income-based JSA, income-related ESA or Universal Credit
award. Property 1's oracle computes that directly. A family's own Universal
Credit award depends on the rent it pays, which changes when it lives
alone, so property 2 compares the families whose own awards are the same
either way (and checks there are some).

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

* Address the after-merge review: student share, head's own awards, P5

- council_tax_benefit: the share leaves out only people in higher
  education, so where a jointly liable person it counts is in other
  education (who may be a student SI 2012/2885 Sch 1 para 7(5) leaves out),
  the share may be too small; a report is then kept rather than capped.
- council_tax_reduction_pensioner: where the household head applies alone,
  the benefit unit's Income Support, income-based JSA, income-related ESA
  and Universal Credit are its claimant and partner's, not the applicant's
  (as #2078 does for council_tax_reduction_relevant_income_based_benefit).
- Two YAML regressions: a report beside a paying simulated claim where a
  further-education student is counted in the share, and a grandmother
  head who stays a pensioner when the young parents get Income Support.
- Property test: the alone simulation is given each family's own awards
  instead of filtering; some generated adults are further-education
  students, and properties 5 and 6 follow the new rule.
- test_benefit_cap_and_ctr_pension_age_properties P5 compares against a
  reform of the cap exception alone, so the CTR pensioner test (which also
  sets the applicable amount) is held fixed.

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

* Hold the means test fixed in property 4

CI found that swapping the other claiming families' ages and disability
benefits can move a family's simulated award through its applicable
amount: the model's severe disability premium counts the other adults in
the household (has_non_dependant_for_severe_disability_premium), joint
occupiers included. That is main's means test, not the scheme or the
exemption, so the perturbed run is given each family's applicable amount
and income from the unperturbed one.

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

* Read the student test and in_FE for the cap's student guard

The re-review of 38364ca found that the guard missed in_FE, and that
current_education's age default (tertiary at 18 and 19) turns it on with
no education input. The guard now uses the model's student test for
non-dependants (_legacy.is_full_time_student_non_dep) or in_FE, for a
jointly liable person the share counts. The age default is kept
deliberately: in the enhanced FRS current_education is an observed input,
and the same test already treats an 18- or 19-year-old non-dependant as a
student. The documentation says so, including that part-time students and
students para 75(2) brings back in are treated the same way.

Regressions: the student shown by in_FE (fails on 38364ca), and a
jointly liable sharer with no education input at 19 (report kept) and 20
(capped). The property generator makes students by current_education or
by in_FE.

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

* Hold each family's own awards fixed in property 4

CI on d988725 (run 37383946508) failed property 4 on a Newham household:
a working-age head (47) claiming Universal Credit, a sharer couple aged 100
and 63 with earnings, and a non-dependant. Perturbing the head to 85 moves
the non-dependant into the sharer's claim (uc_non_dependants_counted: the
household's non-dependants count in one claim only, UC Regs 2013 Sch 4 para
9(2), the head family's where it claims). The sharer's size criteria rise
from one bedroom to two, its LHA cap from 9,000 to 10,860 and its award
from 0 to 1,288.29, and reg 3(1)(a)(ii) then makes it not a pensioner.

That is the family's own award, which the pensioner test is meant to follow,
so the change is intended. Property 4 now holds each family's own UC, IS,
JSA and ESA awards fixed, as property 2 already does, along with its means
test. The same population fails on a9466b8, so this predates the merge
of main; CI on a9466b8 did not generate it. With the awards held, a
household-level pensioner flag still fails property 4.

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

* Hold the household allocations, not the awards, in property 4

The review of 94041e9 found that holding every family's own awards still
let property 4 fail through the same channel, and hid cross-family reads of
awards:
- In Newham, when the head's family stops claiming Universal Credit, the
  non-dependant moves into the sharer's claim and raises its
  uc_maximum_amount, which the four _legacy councils use as a UC family's
  applicable amount. The sharer's council award moved from 241.67 to 417.02
  with its awards held.
- Holding every family's awards also gives a perturbed 85-year-old head the
  award of the 47-year-old it replaced, and a mutant that read another
  family's UC survived.

Property 4 now holds, with each family's CTR means test, the two household
allocations through which other families reach a family's own awards:
uc_non_dependants_counted (UC Regs 2013 Sch 4 para 9(2)) and
has_non_dependant_for_severe_disability_premium. Each family's awards are
computed. The body passes on CI's population, its minimal household and
the reviewer's counterexample, and catches all five mutants the reviewer
and author built.

The generator also sets is_claimant_or_partner on every family's adults, so
wide-gap couples stay couples once #2082 presumes a member 20 or more years
younger to be a child. build() keeps each held value's own type.

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

* Hold the CTR capital in property 4 and pin its counterexamples

Pension Credit's guarantee credit sets a family's CTR capital to nil, and
the model lets other families reach that guarantee credit twice: the
severe disability addition's residence condition counts every other adult
in the household, and pension_credit_assessable_capital divides the
household's savings among its pension-age adults. Property 4 held the CTR
applicable amount and income but not the capital, so a head reaching
pension age, or gaining Attendance Allowance, could move a pension-age
sharer's award from nil to its £900 share of an £1,800 bill (review r5,
finding 1).

Hold council_tax_reduction_assessable_capital with the rest of the means
test, name the two Pension Credit routes in the docstring, and pin as
Hypothesis examples the four populations that broke earlier versions of
the property: CI's Universal Credit non-dependant move, the
uc_maximum_amount move, and the two Pension Credit routes.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants