Skip to content

Apply the Income Support conditions to the claimant and partner only - #2013

Draft
MaxGhenis wants to merge 11 commits into
legacy-carer-premium-claimant-onlyfrom
is-eligibility-claimant-partner-gates
Draft

MaxGhenis wants to merge 11 commits into
legacy-carer-premium-claimant-onlyfrom
is-eligibility-claimant-partner-gates

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2023

Summary

income_support_eligible applied three Income Support conditions to every member of the benefit unit. SSCBA 1992 s.124(1) names only the claimant and, for some conditions, the partner. This PR applies each condition to the people the law names. It is stacked on #2001, which is stacked on #1896, so it stays a draft until they merge.

The independent review of #2001 found the three defects (finding 2). They reproduce on #2001's head (63b633c). In each case a caring single claimant has one extra adult in the benefit unit with is_claimant_or_partner: false:

Extra adult Before After Law
Aged 70 not eligible eligible s.124(1)(aa): "he has not attained the qualifying age for state pension credit"
Reports £5,000 income-related ESA not eligible eligible s.124(1)(h): "he is not entitled to an employment and support allowance and, if he is a member of a couple, the other member of the couple is not entitled to an income-related employment and support allowance"
Reports Income Support; the claimant does not eligible not eligible No new claims (UC (Transitional Provisions) Regs 2014 reg 6A(1)), so the award must be the claimant's

What changed

The claimant is the partner with the existing award. Three provisions combine:

  • UC (TP) Regs 2014 reg 6A(1): "a person may not make a claim for housing benefit, income support, or a tax credit". This has been in force since 25 July 2022, and the 2025 text is the same.
  • Claims and Payments Regs 1987 reg 4(3): a couple choose which of them claims.
  • Reg 4(4): a partner who takes over an award does so by claiming: "where one of a couple is entitled to income support under an award and, with his agreement, his partner claims income support that entitlement shall terminate".

The model takes the award holder to be the claimant or partner who reports Income Support. That is a model convention: if both report it, either can be the claimant. That person must:

  • be under state pension age (s.124(1)(aa); SPCA 2002 s.1(6));
  • fall within a prescribed category the model covers (s.124(1)(e), reg 4ZA, Sch 1B):
    • para 1, a lone parent of a young child;
    • para 2, new here: "a single claimant or a lone parent with whom a child is placed" by a local authority, using is_looked_after_by_local_authority as the proxy;
    • para 4, a carer;
  • have no ESA of either kind (s.124(1)(h), first limb).

In addition:

  • Income-related ESA. Neither the claimant nor the partner may have income-related ESA (s.124(1)(h), second limb). An income-related award covers the couple, so it bars Income Support whichever partner has it. The model reads it as the award on their reported amounts after the esa_income capital test. Both calculations share one helper, income_related_esa_award.
  • ESA the reports do not explain. The gate looks at the value of esa_income:
    • Reads the reports. If the value is what the reported awards give, the gate reads the claimant's and partner's reports. That means either the award after the capital test (the formula) or their plain total (the disable_simulated_benefits reform).
    • Treats it as theirs. Any other value is taken to be the claimant's or partner's award: one entered directly, or one set by a reform that replaces or removes the variable.
    • Effect. Another member's reported ESA cannot switch this off, and neutralising esa_income (abolishing income-related ESA) removes the bar.
    • Intended boundary. An entered award equal to what the reports give is read through them. The comparison is to within half a penny, in the float32 precision esa_income is stored in. So another member's report can change how a direct entry is read: £4,000 entered with no reports bars the claim, but if an excluded adult also reports £4,000 it does not.
    • Earlier attempts. One decided by whether esa_income was a simulation input, using core's record of inputs (_user_input_keys). Reviews of this PR and Apply the Income Support remunerative work and JSA conditions (s.124(1)(c), (f)) #2025 showed that record goes wrong across delete_arrays, clone, disk storage and core-built simulations. A UK Simulation built with no inputs (for example from a Scenario alone) has no record at all, and the gate raised AttributeError. That approach is gone, and simulation.py is unchanged by this PR.
  • Capital. The capital test is unchanged.
  • Placed children. youngest_child_age_for_legacy_benefits now leaves out a child flagged is_looked_after_by_local_authority. That flag is the model's proxy for a child placed with the family, or living away in a local authority's care, who is not a member of the claimant's household (IS Regs 1987 reg 16(4)-(6)). Such a child is no longer a "young child" for para 1, but brings a single claimant within para 2.

Four behaviour changes come from reading the conditions as written. All four move £0 on the Enhanced FRS (below).

  1. A mixed-age couple keeps Income Support when the younger partner has the award.
    • Before, the gate barred the claim if any member was over state pension age.
    • s.124(1)(g) bars it only if the partner "is entitled to state pension credit". SPCA 2002 s.4(1A): "A claimant is not entitled to state pension credit if he is a member of a couple the other member of which has not attained the qualifying age".
    • is_pension_credit_eligible already requires every claimant and partner to be over state pension age; it does not model the SI 2019/37 art. 4 savings. So (g) holds in the model whenever the claimant is under that age.
    • Reading Pension Credit in the gate would create a dependency cycle. I reproduced it with a synthetic mixed-age family working 30 hours in 2024: income_support_eligible → is_pension_credit_eligible → is_guarantee_credit_eligible → pension_credit_income → working_tax_credit → wtc_entitlement → tax_credits → working_tax_credit_pre_minimum → tax_credits_reduction → tax_credits_applicable_income → income_support → income_support_entitlement → income_support_eligible.
  2. The claimant's own contributory ESA bars the claim (s.124(1)(h): "not entitled to an employment and support allowance", of either kind; WRA 2007 s.1(7) defines both kinds). Before, the gate checked only income-related ESA. A partner's contributory ESA does not bar the claim.
  3. A partner cannot take over the other partner's award.
  4. A single claimant with a placed child qualifies through para 2. This includes a lone parent whose only young child is placed.

Fix at the gate, not in esa_income's summation

esa_income sums every member's esa_income_reported. I checked all of its readers (git grep -nw esa_income -- policyengine_uk) before deciding where to make the fix:

  • Income and spending totals. An excluded member's own award is still real money in these: esa, household_benefits, hbai_benefits, hbai_household_net_income, gov_spending, pre_budget_change_household_benefits, the disable_simulated_benefits reform and benefit_cap_reduction.
  • Means tests and passports. These use the benefit unit's award as income or as a qualifying benefit:
    • housing_benefit_applicable_income and council_tax_reduction_applicable_income, through add_for_members, which adds benefit-unit variables as they are;
    • council_tax_reduction_relevant_income_based_benefit;
    • the CTR non-dependant exemption in _legacy.py;
    • the exempt-benefit passport in tax_credits_applicable_income;
    • is_scp_eligible;
    • maintenance_loan_entitled_to_benefits;
    • is_benefit_cap_exempt_health_disability;
    • the qualifying-benefit lists for targeted childcare, cost of living, Winter Fuel and PAWHP.
  • Unused reads. is_benefit_cap_exempt_earnings and is_benefit_cap_exempt_other compute an esa_income sum but do not use it in their result.

Restricting the sum would remove an excluded member's award from household income. So the fix is in the Income Support gate. The other means-test and passport readers keep their current behaviour; whether each should count only the claimant's and partner's award depends on its own rules, and is a follow-up.

would_claim_IS still reads every member's report. That cannot change eligibility, because the gate now needs the claimant's or partner's own report.

Not changed: s.124(1)(c) (remunerative work) and (f) (JSA) are not gates here. #2025, stacked on this PR, adds them.

Invariants

  • No excluded-adult effect. Adding an adult who is neither the claimant, the partner nor a young person in the family never changes income_support_eligible. Their age, ESA, Income Support and caring do not matter. There is one declared exception: a directly entered esa_income that the new adult's report makes equal to what the reports give. That entry is then read through the reports, and a test pins it.

  • Matches a family-by-family reading of the gate (differential test). income_support_eligible is true exactly when:

    • one of the claimant and partner reports Income Support, is under state pension age, is in a covered prescribed category and has no contributory ESA;
    • neither has income-related ESA (on their reported amounts after the capital test, or an esa_income the reports do not explain);
    • capital is within the limit.

    This checks the bounded model gate, not legal entitlement. Caring is is_carer_for_benefits (receipt or 35+ hours), ESA and Income Support are reported awards, placement is a proxy flag, the other Sch 1B categories are not modelled, and the means test is out of scope.

Tests

  • YAML. income_support_claimant_partner_gates.yaml has 23 cases, and Pay the legacy carer premium per qualifying claimant or partner; IS carer route through them only #2001's income_support_carer_route.yaml puts the award on the caring partner.

  • test_income_support_esa_entered_directly.py (15 tests) covers:

    • an esa_income entered in the situation, set after construction, set on a branch or a clone, or entered for another year;
    • a deleted input, and nested branches;
    • a zero entry, a neutralised esa_income, and the plain reported total with a binding capital test;
    • the intended equal-value case, and a report that changes how an entry is read;
    • large awards compared in the stored float32 precision;
    • a simulation with no inputs.
  • Hypothesis: test_income_support_eligibility_properties.py. Both properties run 25 examples, about 70 seconds together.

    • Invariance. The added adult is not in education. Most are primed with what used to bar or open the claim when every member counted: over state pension age, income-related ESA, an Income Support report, or caring. The test checks that the model gives that adult no role in the family.

    • Differential. It reads the gate family by family. Families include:

      • primed carers;
      • lone parents of older children;
      • split couples, where the award holder fails a condition and the qualifying partner has no award;
      • children placed by a local authority;
      • capital entered as assessable capital or household savings;
      • directly entered ESA (£0 or £4,000; £4,000 is never a report total).
    • Mutation check. Four deliberately broken gates, with properties run without shrinking:

      Mutant IS YAML failures ESA regression failures Invariance Differential
      Award and conditions on any partner 2 0 pass fail
      ESA reports from every member 3 10 fail fail
      Direct esa_income ignored 3 8 pass fail
      Any member over state pension age bars 4 0 fail fail

      At 12 examples, the differential passed the first three mutants, so it now runs 25. Logs: ~/reviews/is-eligibility-gates-2026-10-01/mutants/ (RESULTS.md).

  • Full suites at 97b7a1c. YAML: 1,514 passed. pytest: 2,032 passed, 45 skipped. Later commits only change:

    • the differential example count;
    • comments;
    • the float32 comparison;
    • documentation;
    • two ESA regression tests.

    The targeted suites pass at the head.

Enhanced FRS impact: £0 in every year

These are real microsimulation runs on private copies of the Enhanced FRS 2024-25 (sha256 e433e532…). Each package state was run for 2025 to 2030:

  1. Pay the legacy carer premium per qualifying claimant or partner; IS carer route through them only #2001's head;
  2. the three gates restricted to the claimant and partner;
  3. step 2 plus either partner as claimant;
  4. 973cfba;
  5. 8d8673a;
  6. 06e186b;
  7. 97b7a1c;
  8. 3402629, this head's logic. Later commits are tests and comments only.

Every step changes nothing. Income Support, Housing Benefit, council tax reduction, Universal Credit, Pension Credit, esa_income, household net income and poverty are identical, and no benefit unit's eligibility changes. Income Support is £0.51bn in 2025 and £0 from 2026, when the model stops paying it.

Why nothing moves (base run, 2025):

  • Excluded members. About 18k people are neither claimant, partner nor dependant. None of them is over state pension age, has ESA or reports Income Support.
  • Mixed-age couples. No mixed-age couple reports Income Support. No benefit unit in which the claimant or partner reports Income Support has any member over state pension age.
  • Award holder. In every eligible benefit unit, the claimant or partner who reports Income Support is the one who meets the conditions.
  • Contributory ESA. About 3k benefit units (weighted) report Income Support with a claimant or partner on contributory ESA. None of them has a carer claimant or partner, and none is a lone parent, so they were already ineligible.
  • Placed children. The dataset has no is_looked_after_by_local_authority or esa_income column, so para 2 and the direct-ESA rule cannot move it.

So this PR corrects household calculations and non-standard benefit units; it does not move published totals.

The scripts are in ~/reviews/is-eligibility-gates-2026-10-01/impact/. Only the JSON summaries and the figures here are aggregates; the dataset copies and the per-record arrays the scripts save stay private.

Axiom

axiom: TheAxiomFoundation/rulespec-uk#403 queued (addendum, updated for the award-holder rule and Sch 1B para 2). It adds the following to the s.124 module that issue already queues, with companion cases 17-33:

  • s.124(1)(aa), (g) and (h);
  • UC (TP) Regs 2014 reg 6A and C&P reg 4(4);
  • Sch 1B paras 1, 2 and 4 as the covered categories.

SPCA 2002 s.1(6) qualifying age is encoded-correct in uk/statutes/ukpga/2002/16/1.yaml. The corpus does not yet hold s.124, SPCA s.4, WRA 2007 s.1 or UC (TP) reg 6A, so the encoder path is blocked until they are ingested.

Follow-ups

  • Other readers of the benefit-unit awards esa_income, jsa_income and income_support (the means tests and passports listed above, plus would_claim_IS) still count an excluded member's award. Each needs checking against its own rules.
  • s.124(1)(c) (remunerative work) and (f) (JSA) are not modelled here; Apply the Income Support remunerative work and JSA conditions (s.124(1)(c), (f)) #2025, stacked on this PR, adds them.
  • The other Sch 1B categories are not modelled, including para 2A (placement for adoption).

Merge

This PR is stacked on #2001, which is stacked on #1896. PE-UK CI runs only on PRs into main, so checks start once both have merged and this PR is retargeted (gh pr edit 2013 --base main, then gh pr ready 2013).

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits October 1, 2026 14:28
SSCBA 1992 s.124(1) sets each Income Support condition for the claimant
and, in paras (c), (f), (g) and (h), for the other member of a couple.
income_support_eligible applied three of them to every benefit-unit member:

- (aa) no member could be over state pension age, so an excluded elderly
  adult barred a caring claimant;
- (h) any member's income-related ESA barred the claim (esa_income sums
  every member's reported award);
- any member's reported Income Support counted as an existing award.

A couple choose which of them claims (Claims and Payments Regs 1987
reg 4(3)), so the benefit unit is now eligible when the claimant or the
partner could claim: under state pension age, in a prescribed category
(carer, or lone parent of a young child) and with no ESA of either kind.
Neither may have income-related ESA after the ESA capital test, and one
of them must report an existing award (UC (TP) Regs 2014 reg 6A(1)).

The fix is at the gate, not in esa_income's summation: esa_income is also
the benefit unit's income-related ESA income for household income, HBAI,
government spending and the benefit cap, where an excluded member's own
award is still real income. An award entered directly as esa_income, with
no reported awards, is taken to be the claimant's or partner's.

Two behaviour changes follow from reading the conditions as the law
names them: a mixed-age couple can claim through the younger partner
(s.124(1)(g) bars it only if the partner is entitled to State Pension
Credit, which SPCA 2002 s.4(1A) rules out), and the claimant's own
contributory ESA now bars the claim (s.124(1)(h)).

Tests: 17 YAML cases (8 fail on the previous formula), a Hypothesis
property that adding an excluded member never changes eligibility, and a
differential test against a direct per-family reading of s.124(1). Both
properties fail on the previous formula.

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

Review r1 of this PR found three ways a member outside the claimant and
partner, or the wrong partner, still decided eligibility:

- An excluded member's reported ESA switched off the fallback for an
  esa_income entered directly. The gate now asks the simulation whether
  esa_income is an input (simulation.input_variables, as
  marginal_tax_rate_on_capital_gains does). If it is, it reads it as the
  claimant's or partner's award. Otherwise it applies the esa_income
  calculation, now the shared income_related_esa_award helper, to the
  claimant's and partner's reported amounts.
- The award and the conditions could sit on different partners. No new
  claim can be made (UC (TP) Regs 2014 reg 6A(1)), and a partner who
  takes over an award makes a claim (Claims and Payments Regs 1987
  reg 4(4)). So the claimant is the partner who reports the existing
  award, and that person must satisfy (aa), (e) and the first limb of
  (h).
- youngest_child_age_for_legacy_benefits counted a child placed by a
  local authority, who is not a member of the claimant's household
  (IS Regs 1987 reg 16(4)). It now uses
  is_child_or_young_person_for_legacy_benefits.

#2001's "partner is the carer" case now puts the award on the caring
partner. YAML: 21 cases, 5 of which fail on this PR's previous head and
10 on #2001's. The Hypothesis generators add placed children, esa_income
entered directly and household savings, and the differential test
follows the award holder.

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

With 12 random families, the invariance property never reached a placed
young child in a non-caring lone parent's family, or a directly entered
esa_income beside an added member's ESA, so it passed on this PR's
previous head. Half the families are now a caring claimant with an award,
or such a lone parent with only children over 5. Placed children and
direct esa_income are drawn half the time, and the invariance property
runs 25 examples.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 1, 2026
#2013 (b1977d9) primes half the generated families at the edge of
eligibility and draws a direct esa_income half the time. Resolve against
this branch's JSA generators: a direct jsa_income is now also drawn half
the time, and a primed claimant has no paid work or JSA, so it sits at the
edge of eligibility under (c) and (f) as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Simulation.input_variables is fixed when the simulation is built and
ignores the period. Three synthetic cases showed the gate getting it
wrong:

- an esa_income set with set_input after construction did not bar IS;
- the same on a branch did not bar IS either;
- an esa_income input for 2024 made the 2025 gate read esa_income as
  entered directly, even though 2025's value came from the formula over
  every member's reports, an excluded adult's included.

The #2025 review found this on that PR's JSA copy of the pattern.
policyengine_uk/utils/inputs.py (entered_directly, identical to #2025's
copy) reads core's own record of explicit inputs (_user_input_keys:
variable, branch, period), the record that to_input_dataframe reads. It
counts an input set on this branch or one it inherits from, provided the
value is still stored. test_income_support_esa_entered_directly.py covers
all three cases; each fails on the previous gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 2, 2026
Review r1 of #2025 (finding 1): the gate detected a direct jsa_income
through Simulation.input_variables. That list is fixed at construction and
ignores the period, so it missed an input set later with set_input or on a
branch, and treated an input for one year as an input for every year. An
excluded member's report then counted in a year with no direct input.

entered_directly (policyengine_uk/utils/inputs.py) reads core's record of
explicit inputs, (variable, branch, period) in _user_input_keys, for the
branches the simulation can see, and requires the value still be stored.
#2013 adopts the same helper for esa_income.

Also from the review:
- jsa_income returns zero before reading reports when income-based JSA is
  not active, as before the helper was introduced (finding 4);
- income_support_remunerative_work_hours says weekly_hours is a usual
  week, not the reg 5(2)/(3B) average for fluctuating hours (finding 3);
- YAML cases for a partner at 23 hours, inferred roles with an excluded
  grandparent, two caring award holders with contribution-based JSA, and a
  direct jsa_income for another year; pytest regressions for late,
  branch and other-year inputs (finding 5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 2, 2026
MaxGhenis added a commit that referenced this pull request Oct 2, 2026
…was input

claimant_or_partner_esa_income and claimant_or_partner_jsa_income took
esa_income / jsa_income as the claimant's or partner's award whenever the
name was in Simulation.input_variables. That list is fixed at construction
and ignores the period: an input for 2024 alone made 2025 read the
benefit-unit total, so another member's 2025 award counted, and an award set
later with set_input or on a branch was ignored. Both now use
entered_directly (policyengine_uk/utils/inputs.py, byte-identical to #2013
and #2025), which reads core's record of each input's variable, branch and
period.

jsa_income.py is re-copied from #2025 (6337f31): the formula returns early
while income-based JSA is inactive, before reading reports.

Tests: YAML cases for an award entered for an earlier year only, and Python
tests for a late set_input, a set_input after the value was cached, a branch
and a late zero entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cherry-picked from #2025 (48abd88).
simulation.py and utils/inputs.py are identical to that commit. Its
lifecycle test file is parametrised over jsa_income too, and this PR has
no JSA gate, so it stays in #2025; the ESA cases are mirrored in
test_income_support_esa_entered_directly.py.

entered_directly read core's _user_input_keys, but in policyengine-core
3.32.9 that record drifts from storage:

- clone() copies the holders' stored arrays but shares the record. An
  input set on a clone, or on a parent after a branch was made, was
  recorded against arrays it never reached, so an excluded adult's
  reported ESA then barred Income Support on the original or on the
  earlier branch.
- delete_arrays() removes arrays but keeps their record, so a formula
  result calculated later for the same period looked like an input.

The UK Simulation (and so Microsimulation) now gives each clone its own
copy of the record, and drops the entries for deleted arrays. Both
changes only remove stale or foreign entries.

entered_directly now asks about the stored value the simulation reads:
the first visible branch (its own, then its parents', then the default)
that stores one, which is the order the holder reads in.

The 11 ESA cases pass. With the new inputs.py on the previous
simulation.py, 3 of them fail: deletion, clone, and an earlier branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r2 of this PR, and review r3 of #2025, showed that deciding
"entered directly" from core's record of inputs (_user_input_keys) is
unsound in policyengine-core 3.32.9. The record drifts across
delete_arrays, clone, disk storage and core-built simulations, and a UK
Simulation built with no inputs has no record, so the gate raised
AttributeError. The record is no longer read: utils/inputs.py is deleted
and simulation.py is restored to its state before this PR.

The gate now reads the value of esa_income. If the value is what the
reported awards give, either the award after the capital test (the
formula) or their plain total (disable_simulated_benefits), the gate
reads the claimant's and partner's reports. Any other value (an award
entered directly, or a reform that replaces or neutralises esa_income) is
taken to be theirs. Two consequences are intended and tested: abolishing
income-related ESA removes the bar, and an entered award exactly equal to
what the reports give is read through them. The rule is the same as
#2025's for JSA.

IS Regs 1987 Sch 1B para 2 is now a prescribed category: "A single
claimant or a lone parent with whom a child is placed" by a local
authority, with is_looked_after_by_local_authority as the proxy. A lone
parent of an 8-year-old with a placed 3-year-old is eligible through
para 2, not para 1 (the placed child is outside the household, reg
16(4)). The YAML case now expects true; new cases cover a single claimant
with only a placed child, and a couple, who are not covered.

The documentation calls the award-holder rule a model convention (if both
partners report IS, either can be the claimant). The youngest-child
documentation cites reg 16(4)-(6) and calls the flag a proxy.

Property tests:
- the invariance property adds only adults outside the family, primed as
  over state pension age, ESA, an IS report or a carer;
- placed children are family members, read through para 2 in the oracle;
- a new split-couple shape: the award holder fails a condition and the
  qualifying partner has no award;
- direct ESA entries (0 or 4,000; 4,000 is never a report total) are
  drawn more often.

The oracle's signature is unchanged; the caller applies the value rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 2, 2026
#2013 (97b7a1c) replaces entered_directly with a value rule for
income-related ESA, deletes policyengine_uk/utils/inputs.py, restores
policyengine_uk/simulation.py (no clone/delete_arrays overrides), and adds
IS Sch 1B para 2 (a single claimant with a placed child).

Review r3 of #2025 showed provenance via core's _user_input_keys is
unsound in policyengine-core 3.32.9: core-built simulations skip the UK
overrides, disk clones share files, default-named branches and ETERNITY
diverge, input-free simulations crashed, and delete_arrays became O(keys).

Income-based JSA now follows the same rule. jsa_income counts as the
couple's award only when it differs from both the award on everyone's
reported amounts after the capital test and their plain total (which
disable_simulated_benefits sets); otherwise the claimant's and partner's
reports decide. An entered award, or a reform that removes or replaces
jsa_income, therefore applies, whatever the lifecycle that stored it.

Conflicts: the gate's documentation and its ESA/JSA block, and the
property test (both sides kept: #2013's shapes, placed children and para
2; this branch's work and JSA limbs; direct values {0, 4000} for both
awards, with explained_by_reports taking the JSA capital rules).
test_income_support_direct_inputs.py and test_entered_directly_lifecycle.py
are rewritten to the value rule, including the raw reported total and a
neutralised jsa_income.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 2, 2026
Takes is-remunerative-work-jsa-conditions at bfff081 (with #2013 at
97b7a1c). In test_income_support_eligibility_properties.py, #2025's version
is kept and the work tests are re-applied: the reference's award on the
claimant's and partner's reports, and explained_by_reports' award on
everyone's reports, pass the ESA and JSA work screens; monotonicity allows a
gain only where a calculated award was lost. Excluded members are now adults
only, so the invariance property needs no joint-claim hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 2, 2026
#2013 and #2025 replaced entered_directly and the Simulation overrides with a
value rule, after #2025's round-3 review found the record of direct inputs
unsound in policyengine-core 3.32.9:
- a Simulation without inputs has no record;
- the YAML runner and SimulationBuilder bypass the UK Simulation;
- disk-backed clones share files;
- branch names and ETERNITY periods diverge;
- delete_arrays becomes O(keys).

This merge takes their removal of policyengine_uk/utils/inputs.py and their
simulation.py (identical to 63b633c's). claimant_or_partner_esa_income and
claimant_or_partner_jsa_income now mirror the gate. Where esa_income /
jsa_income holds what the reported awards give, either after the capital
test (the formula) or as their plain total (disable_simulated_benefits), they
use the claimant's and partner's reported awards. Otherwise they take the
stored value to be the claimant's or partner's: an award entered directly, or
a reform that replaces or removes it. An entered award equal to either amount
is read through the reports.

test_claimant_or_partner_awards_entered_directly.py gains cases for an
entered award equal to the reports, the raw total with capital (partner
£200, other adult £3,000, savings £10,000: £0), and a neutralised award. The
lifecycle cases (deletion, clone, branches) keep their expectations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At 12 derandomized examples it never met a directly entered ESA award in an
otherwise eligible family, so mutants that ignore the direct award (M4) or let
the award and the conditions sit on different partners (M1) passed it. At 25
it fails both. The invariance property already ran 25.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 2 commits October 2, 2026 03:03
esa_income_eligible is the screen esa_income applies. It is a capital test
here, but #2031 (on #2025) adds remunerative-work tests to it, so the
comments now name the variable rather than the test. Comment-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 2, 2026
Matches #2013's wording for income_related_esa_award (44d59d9).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 2, 2026
Brings #2013's DIFFERENTIAL_SETTINGS (25 examples) for the Income Support
family-by-family property. Clean merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 2, 2026
…pers

Comment-only. In esa_income.py and jsa_income.py the helpers keep #2025's
docstrings ("fails the screen in esa_income_eligible" / "jsa_income_eligible"),
so they stay identical to #2025 and #2013; the variables' documentation names
the capital and remunerative work tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 2, 2026
MaxGhenis added a commit that referenced this pull request Oct 2, 2026
Docstring only: jsa_income.py is byte-identical to #2025's ffe3a58, matching
#2013's esa_income.py (44d59d9). No code changed.

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

#2025's review round 4 found two things that apply to this ESA limb.

- esa_income is stored as float32, but the award and plain total the gate
  recomputes are float64. With two reports of £65,536.01 and £65,536.00,
  the stored award (£131,072.00) is 0.0078 below the float64 total, so the
  formula's own award looked entered and barred the claim. The gate now
  casts both to the stored dtype before comparing. The new test fails on
  the previous gate.
- Under the value rule, a directly entered award is read through the
  reports when it equals what they give, so another member's report can
  change how an entry is read. For example, £4,000 entered with no
  reports bars the claim; once an excluded adult reports £4,000, the claim
  is eligible. That is the declared convention. The variable
  documentation and the invariance bullet in the property docstring now
  say so, and a test pins the transition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 2, 2026
Brings #2013's 3402629 and #2025's round-4 changes: the IS gate compares
direct awards in the stored dtype, documents the value convention's
boundary, and adds direct-input tests. Clean merge; the property file's
docstring and the work-screen reference sit side by side.

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.

1 participant