Skip to content

Apply the Income Support remunerative work and JSA conditions (s.124(1)(c), (f)) - #2025

Merged
MaxGhenis merged 24 commits into
mainfrom
is-remunerative-work-jsa-conditions
Oct 9, 2026
Merged

MaxGhenis merged 24 commits into
mainfrom
is-remunerative-work-jsa-conditions

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2024

Summary

income_support_eligible did not model two of the SSCBA 1992 s.124(1) conditions:

  • (c) "he is not engaged in remunerative work and, if he is a member of a couple, the other member is not so engaged";
  • (f) "he is not entitled to a jobseeker's allowance and, if he is a member of a couple, the other member of the couple is not, and the couple are not, entitled to an income-based jobseeker's allowance".

This PR adds both, for the claimant and partner only. #2013 (with #2001 and #1896) has merged, and this PR now targets main. It keeps #2013's structure: the claimant is whichever of the claimant and partner has the existing award (UC (TP) Regs 2014 reg 6A(1); C&P Regs 1987 reg 4(4)). It also keeps the conditions main has added since: the claimant's age test on the State Pension Credit qualifying age (s.124(1)(aa)) and the s.124(1)(g) bar where the couple is taken to be on State Pension Credit.

Law and the rule

(c) Remunerative work.

  • Claimant: paid work of at least 16 hours a week (IS Regs 1987 reg 5(1): "not less than 16 hours a week being work for which payment is made or which is done in expectation of payment").
  • Partner: 24 hours (reg 5(1A): "In the case of any partner of the claimant paragraph (1) shall have effect as though for the words "16 hours" there were substituted the words "24 hours"").
  • Carers: reg 6(4)(c) takes "a person to whom paragraph 4 of Schedule 1B applies" out of remunerative work. That paragraph describes a person (the carer), so it covers a caring partner as well as a caring claimant.
  • Candidate claimants: the partner threshold depends on who claims, so the gate tests it for each candidate claimant against the other member of the couple. That stays exact even if a reform puts the partner threshold below the claimant's.

(f) JSA.

  • Claimant: must not be entitled to a jobseeker's allowance of either kind.
  • Partner, and the couple: must not be entitled to income-based JSA. JSA 1995 s.1(4) counts a joint-claim JSA as income-based. The partner's contribution-based JSA does not bar the claim.

Income-based JSA follows #2013's income-related ESA pattern. It is the award on the claimant's and partner's reported amounts, after the same capital screen as jsa_income, through a new income_related_jsa_award helper that jsa_income now also uses. A jsa_income that holds an award the reported amounts do not give is taken to be theirs. That means neither that award on everyone's reports nor their plain total, which disable_simulated_benefits sets. It covers an award entered directly (when the simulation is built, later, or on a branch) and a reform that removes or replaces jsa_income. The rule reads only the stored value, so no input-provenance tracking is involved. A stored award (entered directly or replaced by a reform) equal to what the reports give (to within half a penny, compared at the float32 precision the award is stored in) is read through the reports, which say whose it is. An award within half a penny of zero is no award, as main now does for ESA. So a stored zero never bars the claim, and nor does the float residual of reports that exactly match the tariff income (£10.01 + £41.99 against £52).

Changes

  • income_support_eligible: the two claimant limbs (no remunerative work at 16 hours, no contribution-based JSA) join Apply the Income Support conditions to the claimant and partner only #2013's per-person claimant conjunction, along with the other member's remunerative work at 24 hours. Income-based JSA of either partner bars the claim next to income-related ESA and main's s.124(1)(g) Pension Credit bar.

  • New parameters gov.dwp.income_support.eligibility.remunerative_work.claimant_hours and partner_hours, with their history from legislation.gov.uk point-in-time texts of reg 5:

    Period Claimant Partner
    from 11 April 1988 (as made) 24 24
    from 7 April 1992 16 16
    from 7 October 1996 (reg 5(1A) inserted by SI 1996/1944) 16 24
  • New income_support_remunerative_work_hours (Person): weekly_hours, a usual week's hours in paid jobs. The Enhanced FRS hours_worked is FRS TOTHOURS (usual weekly hours across current jobs) × 52, and weekly_hours divides by 52. Enter it directly for fluctuating hours, which reg 5(2) and (3B) average over a cycle or recent weeks, and to leave out the reg 6(1) employments that reg 5(6) excludes from the count.

  • jsa_income: the screen moves into income_related_jsa_award, unchanged. The JSA active check is inside the helper, so every caller agrees with jsa_income. jsa_income keeps its own early return, so an inactive scheme still reads no reports. The parallel passport-scoping branch (Count only the claimant's and partner's legacy awards in means tests and passports #2027) carries a byte-identical copy.

  • No new Simulation machinery. Review rounds 1-3 tried deciding direct entry from core's record of inputs (_user_input_keys), and that proved unsound in policyengine-core 3.32.9: core-built simulations, disk clones, default-named branches, ETERNITY periods, an input-free crash, and O(keys) deletes. Apply the Income Support conditions to the claimant and partner only #2013 and this PR replace it with the value rule above, for ESA and JSA alike; Count only the claimant's and partner's legacy awards in means tests and passports #2027 follows. The core bookkeeping bugs are _user_input_keys drifts from stored values after delete_arrays and clone policyengine-core#559; a fix is open as Add total wealth #561.

Which reg 5 and 6 rules the model applies

Rule Status
reg 5(1), (1A): 16 and 24 hours of paid work modelled
reg 6(4)(c): a Sch 1B para 4 carer, claimant or partner modelled with is_carer_for_benefits, the same proxy the gate uses for the para 4 category
reg 5(2), (3B): expected weekly hours, or for fluctuating hours the average over a complete recognisable cycle or the five weeks before the claim or a superseding decision (or another more accurate period); a school-year cycle leaves out school holidays and other periods the person is not required to work approximated by a usual week's hours; enter income_support_remunerative_work_hours directly for cycle or recent-period averages
reg 5(7): paid meal and refreshment time counts included only so far as survey usual hours include it
reg 5(6) with reg 6(1)(b)-(m): child minding at home, volunteering, training allowance schemes, the self-employment route, retained fire-fighters and similar, councillors, placement carers, sports awards not identifiable in the data; enter the hours directly
reg 5(3A): days of maternity, paternity, adoption, shared parental, neonatal care or bereavement leave, or illness not identifiable by day in annual data; enter the hours directly
reg 5(3), (4), (5), (5A): treated as engaged during unexcused or holiday absence, for 7 days of a trade dispute and for the period earnings cover (except earnings disregarded under Sch 8 para 1) not modelled
reg 6(4)(b): trade disputes, subject to reg 5(4) and (5) not modelled
reg 6(5)-(8): the four-week run-on, its entitlement-period and joint-claim rules not modelled

Invariants

These are property-tested. Invariants 1 and 3 hold for every input except at the value convention's boundary. A stored award (entered directly or replaced by a reform) equal (to within half a penny, at the precision it is stored in) to what the reported amounts give is read through them, so another member's report, or the claimant's own, can change how such an award is read. The property generators stay off that boundary, and example tests pin it with the intended results.

  1. Excluded members. Adding a member who is neither the claimant, the partner nor a child or young person in the family never changes income_support_eligible, whatever that member's work or JSA. The existing Apply the Income Support conditions to the claimant and partner only #2013 property now also draws hours, JSA and a directly entered jsa_income.
  2. Differential. income_support_eligible equals an independently written family-by-family reading of the gate. The oracle in test_income_support_eligibility_properties.py now reads (c) and (f), with the JSA tariff and capital limit from the parameters, alongside main's qualifying age and Pension Credit proxy, which it reads from the model as the gate does.
  3. Monotonicity. Raising any claimant's or partner's hours or JSA never turns an ineligible family eligible. (c) and (f) only bar a claim. The one other path is main's s.124(1)(g) proxy: reported income-based JSA can end the Housing Benefit route to the inferred SI 2019/37 saving when neither member reports Pension Credit, which can lift the (g) bar. That route already fails when either member reports Income Support, so this happens only for a couple in which neither does. Such a family has no award holder and is never eligible. This is a new property.
  4. Refactor. jsa_income is unchanged by the helper refactor. On the Enhanced FRS, jsa_income and every other saved array (92 in the run against current main) are identical in every year 2025-2030.
  5. Direct awards by value. The JSA (and ESA) limb depends only on the stored value the simulation reads, so it holds through set_input at any time, deletion, clones and branches. This is covered by example tests, not a property:
    • test_income_support_direct_inputs.py (17 tests), including the half-penny tolerance endpoints (£100.004 vs £100.006 against a direct £100), a stored zero that never bars the claim, three half-penny zero cases (the formula's own residual; £0.003 vs £0.006 entered; the couple's residual beside another member's £3,000) and the couple's award tolerance (£52.004 vs £52.006 against £52 of tariff income). It also covers a late set_input, a set_input after the gate was cached, a branch, a late zero overriding reports, an input for another year, a neutralised jsa_income, the raw reported total, large reports compared at storage precision, and the two convention-boundary transitions (an excluded adult's report, or the claimant's own, explaining a direct award);
    • test_entered_directly_lifecycle.py (16 tests): eight cases for both awards, with an excluded adult reporting the award, including an entered award equal to the reports (read through them, as intended).

Tests

  • YAML: 23 cases in income_support_work_and_jsa.yaml, each hand-computed from the law. At f159918 (after merging main twice; the head, 198adea, changes only a docstring since), the finance/benefit and gov YAML folders pass: 2,474 cases (2,439 at de0ece0, before main's Model a Universal Credit claim by a member of a couple as a single person (UC Regs 2013 reg 3(3)) #2084 added its Universal Credit single-claim cases). Main's s.124(1)(g) and qualifying-age cases in income_support_claimant_partner_gates.yaml pass with this PR's conditions.
    • With Apply the Income Support conditions to the claimant and partner only #2013's 8d8673a gate swapped in (all other files from this branch), 8 fail: the lone parent at 16 hours; the partner at 24 hours, with explicit and with inferred roles; the claimant's contribution-based and income-based JSA; both caring partners with contribution-based JSA; the partner's income-based JSA; and a directly entered jsa_income.
    • With this PR's pre-review gate (430bd63), the case with a jsa_income entered for another year fails.
  • Direct awards: the 33 tests above pass. Reverting the JSA zero guard to an exact zero fails all three new half-penny tests. With Apply the Income Support conditions to the claimant and partner only #2013's original 8d8673a gate swapped in, 18 of the first 24 fail. The storage-precision test fails on the round-4 gate (bfff081). Apply the Income Support conditions to the claimant and partner only #2013's test_income_support_esa_entered_directly.py passes here too, with one new test that pins the ESA couple's award tolerance the same way. Setting either couple's-award guard (JSA or ESA) to > 0 fails its new tolerance test at the eligibility assertion.
  • Hypothesis: all three properties pass at f159918, with Apply the Income Support conditions to the claimant and partner only #2013's generators (primed and split-couple families, placed children, adults-only excluded members) and direct awards drawn from {0, 4,000}. No total of the reported amounts can equal 4,000.
  • Parameter history: test_legacy_benefit_parameters.py checks both thresholds on either side of 7 April 1992 and 7 October 1996.
  • pytest (all of policyengine_uk/tests except microsimulation/): 2,052 passed, 19 skipped at 48abd88; 2,036 at 6337f31; 2,024 at fcbec88. At 081ec14, the targeted files (properties, direct awards, lifecycle, ESA, parameters, carer premium, code health) passed: 1,745 tests. At f159918, the properties, direct-award, lifecycle, ESA and parameter files pass: 60 tests (58 at de0ece0 and 195c719, before the two tolerance tests). The value rule removed the Simulation overrides 48abd88 had added, so the head touches no shared simulation code.

Impact (Enhanced FRS 2024-25, real PolicyEngine runs)

Against current main. This PR at 195c719 was run against main at the merged commit (9f92d53), each package state in its own environment on a fresh private copy of the dataset (sha256 e433e532…). Every year 2025-2030 changes by £0, with all 92 saved arrays identical in every year. The same comparison at the first merge of main (de0ece0 against f1a9a3c) also gave £0, with all 92 arrays identical. The head (198adea) changes only test files since 195c719, so this run covers its code. That covers Income Support (£0.51bn in 2025; the model pays it until 2025-26), income-related ESA and income-based JSA, Housing Benefit, CTR, Universal Credit, Pension Credit, household net income and poverty.

Earlier runs. Four package states: #2013's head (8d8673a), (c) only, (f) only, and this PR (fcbec88). After review, the heads were re-run twice: #2013 at 06e186b against this PR at 6337f31, and #2013 at 97b7a1c against this PR at bfff081 (the value rule). Each step and each re-run was £0 in every year, with all 91 saved arrays identical.

Why nothing moves (2025, base run on main):

  • No benefit unit eligible on main has a claimant or partner reporting JSA of either kind, a non-carer claimant working 16 or more hours, or a non-carer partner working 24 or more.
  • About 4.7 thousand (weighted) benefit units report Income Support with a non-carer claimant or partner working 16 or more hours. None of them is eligible on main. None has a caring award holder or is a lone parent of a child aged 5 or under, the two prescribed categories (s.124(1)(e)) the diagnostic checks; it does not check Sch 1B para 2 (a placed child).

The JSON summaries are aggregates. The dataset copies and per-record arrays stay private.

Axiom

axiom: TheAxiomFoundation/rulespec-uk#403 queued (addendum)

The addendum queues s.124(1)(c) and (f), IS Regs regs 5 and 6 and JSA 1995 s.1(4). It has outputs, a review_finding and 24 companion tests: 22 match the YAML cases, and 2 cover the reg 5(4) trade-dispute precedence that PolicyEngine does not model. None of those citations is in the corpus yet. JSA Regs reg 116, which PolicyEngine's income-based JSA screen uses, is encoded-correct (uk/regulations/uksi/1996/207/116.yaml).

Review

Round 1 (Subfleet, GPT-6.1 Sol): REQUEST CHANGES.

  1. Blocking: a direct jsa_income was detected through Simulation.input_variables. That missed late or branch inputs and leaked across years. Fixed with entered_directly and regression tests, since superseded by the value rule (round 3). The same defect in Apply the Income Support conditions to the claimant and partner only #2013's ESA fallback was fixed there and merged here.
  2. Axiom: reg 6(4)(b) is subject to reg 5(4)-(5). Now explicit in output 9 and the review_finding, with companions 50-51.
  3. The hours proxy overstated reg 5(2) averaging, and the table was incomplete. Documentation, table and Axiom counterpart corrected.
  4. An inactive JSA scheme read reports before short-circuiting. jsa_income's early return is restored.
  5. Boundary and role cases. Added: partner at 23 hours, inferred roles with an excluded grandparent, two caring award holders with contribution-based JSA.
  6. Stale Hypothesis statistics. Refreshed.

Round 2 (same reviewer): findings 2, 4, 5 and 6 fixed; 1 and 3 partly fixed, with two new defects found.

  1. Blocking: deleting an input left its record, so a later formula result looked direct. Fixed in Simulation.delete_arrays.
  2. High: clones and earlier branches shared the input record with their parent. Fixed in Simulation.clone, with entered_directly checking the provenance of the stored value read; both were superseded by the value rule in round 3.
    • Lifecycle tests cover both cases for JSA and ESA.
    • The helper no longer falls back silently when core's record is missing; core ≥ 3.32.9, which pyproject requires, has it.
  3. Legal precision: expected weekly hours and the other averaging periods, the Sch 8 para 1 qualification on reg 5(5A), and reg 5(7) and reg 6(7)-(8) in the Axiom brief; companions 50-51 now state the claim.
  4. Statistics denominators now count invalid attempts, and the old-gate logs are saved.

Round 3 (same reviewer): REQUEST CHANGES.

Round 4 (same reviewer): REQUEST CHANGES. Round-3 findings 1, 2, 4 and 6 resolved, 5 moot; finding 3 (disk clones share files) is a core storage bug, now PolicyEngine/policyengine-core#558. All round-4 findings are addressed:

  1. The float32 stored award was compared with float64 sums, so very large reports looked entered. Both limbs now compare at the stored precision, with regressions in both test files.
  2. The invariants were overclaimed. They are now stated with the convention's boundary in the variable documentation, the property module and here, and the boundary transitions are pinned as intended behaviour.
  3. The reg 5 table wording now carries reg 5(3B)'s excluded periods and the superseding decision.

Round 5 (same reviewer): APPROVE WITH NITS. No runtime defects; the precision fix is confirmed in both limbs and the merges lost nothing from #2013. The nits are addressed:

  1. The documentation, the property module and the Axiom summary state the convention for any stored award (entered or reform-replaced) with the half-penny tolerance after storage rounding; the tolerance endpoints are tested.
  2. The mutation count now matches the saved logs (two, not three).
  3. A deterministic oracle check covers both direct-award modes.

After round 5: merged #2013's 0953c8a, which limits Sch 1B para 2 to placed children under 16 and guards the ESA bar so a stored zero never bars the claim. The JSA limb now has the same guard, pinned by test_a_stored_zero_never_bars_the_claim, which fails on the previous gate.

Round 6b (confirmatory, delta da1a798..081ec14): APPROVE WITH NITS. No runtime defect, and nothing from #2013's merge was lost. The two PR-body nits (a stale Hypothesis statistic; two sentences that described only entered awards) are fixed above. Its informational finding 4, that the zero guard caught only an exact zero so a float residual of the reports still barred the claim, is fixed by the half-penny rule below.

After merging main (de0ece0): two conflicts, in the gate and the property module. Kept this PR's rule and main's qualifying-age test, s.124(1)(g) bar and half-penny ESA zero guard (#2013's after-merge review). The JSA limb now treats an amount within half a penny of zero as no award too, with three new tests. That is the one change beyond a mechanical merge, and an independent review of it is recorded below.

After merging main again (195c719): main's #2084 (a Universal Credit single claim by a member of a couple) merged without conflicts. It changes no Income Support, JSA or ESA code. The tests and the impact comparison above were rerun at that commit.

vahid-ahmadi's review (at 081ec14): both points are done. The merge of main (de0ece0) takes main's has_attained_state_pension_credit_qualifying_age for the claimant's age test and keeps main's s.124(1)(g) on_pension_credit bar. The 23 YAML cases and the property tests pass with them, and the summary no longer describes a stack.

After-merge review (Subfleet job 20261008-123448-is-work-jsa-review-after-merge, Opus 5.5, at de0ece0): APPROVE WITH NITS. No runtime defect. Nothing from either side of the merge was lost, and the JSA half-penny guard has the ESA limb's form and is right in law and in code. Its findings are addressed:

  1. The monotonicity docstrings now give the s.124(1)(g) path (f159918, narrowed in 198adea).
  2. This review record replaces a placeholder.
  3. core#561 is described as open, not merged.
  4. The tests and impact are stated at the commits they ran on.
  5. The diagnostic's coverage is stated exactly (it does not check Sch 1B para 2).
  6. Two wording fixes in this body.
  7. Couple's-award tolerance tests at £52.004 and £52.006 for JSA and ESA (f159918). Setting either couple's-award guard to > 0 fails its test.
  8. The formula's-residual test cross-references core's float64 sum (f159918).
  9. The JSA and ESA limbs differ by design from 2026, when income-based JSA is inactive. That comes from round 1's active check, not from this delta, and belongs with a separate follow-up that fixes the disable_simulated_benefits reform.

Confirmatory review (Subfleet job 20261008-184614-is-work-jsa-review-after-merge-r2, GPT-6.1 Sol, delta 195c719..f159918): APPROVE WITH NITS, safe to merge.

  • It confirmed that 195c719 is a clean merge (no file differs from both parents) and that f159918 touches test files only.
  • It confirmed the float64 explanation and the monotonicity argument against the gate, the Pension Credit saving and core 3.32.13.
  • It reran test_income_support_direct_inputs.py and test_income_support_esa_entered_directly.py (37 passed) and both mutants (each fails at assert eligible(below) after its preconditions pass).
  • Its one finding was that the (g) sentence overstated when the saving ends, because reported Pension Credit keeps it on its own. 198adea narrows the docstring to the Housing Benefit route, and invariant 3 above says the same.

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits October 1, 2026 17:58
SSCBA 1992 s.124(1)(c) bars Income Support when the claimant is engaged in
remunerative work or the other member of a couple is: paid work of 16 hours
a week or more for the claimant (IS Regs 1987 reg 5(1)) and 24 for the
partner (reg 5(1A)). A person within Sch 1B para 4, a carer, is not treated
as engaged in it (reg 6(4)(c)). The partner's threshold applies to whoever
is not the claimant, so the gate tests it for each candidate claimant.

s.124(1)(f) bars it when the claimant is entitled to a jobseeker's allowance
of either kind, or the partner (or the couple) to income-based JSA. The
partner's contribution-based JSA does not bar it. Income-based JSA follows
the income-related ESA pattern: the award on the claimant's and partner's
reported amounts after the JSA capital test, through a new
income_related_jsa_award helper that jsa_income also uses, or a jsa_income
entered directly, taken to be theirs.

- Parameters gov.dwp.income_support.eligibility.remunerative_work
  claimant_hours (24, 16 from 7 April 1992) and partner_hours (24, 16 from
  7 April 1992, 24 from 7 October 1996), from legislation.gov.uk
  point-in-time texts of reg 5.
- income_support_remunerative_work_hours: weekly_hours, enterable directly
  to leave out the employments reg 6(1) excludes (reg 5(6)).
- 17 YAML cases hand-computed from the law; 6 fail on the previous gate.
- The Hypothesis differential oracle now reads (c) and (f), and both
  properties draw hours, JSA and direct jsa_income.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
s.124(1)(c) and (f) only bar a claim, so raising any claimant's or
partner's hours or JSA cannot turn an ineligible family eligible. The new
Hypothesis property builds each family twice, the second time with more
paid work or JSA, and checks eligibility never rises.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#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>
MaxGhenis added a commit that referenced this pull request Oct 1, 2026
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 and others added 3 commits October 2, 2026 00:18
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>
…gates' into is-remunerative-work-jsa-conditions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
MaxGhenis and others added 2 commits October 2, 2026 00:54
Review r2 of #2025: 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, so an
  input set on a clone, or on a parent after a branch was made, was
  recorded against arrays it never reached. An excluded adult's report
  then barred Income Support on the original or 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; they also correct what core's own
_invalidate_all_caches and to_input_dataframe read from the record.

entered_directly now asks about the stored value the simulation reads: the
first visible branch (own, parents', default) that stores one, as the
holder reads. It no longer falls back silently when core's record is
missing.

test_entered_directly_lifecycle.py covers both awards (jsa_income,
esa_income) with an excluded adult reporting the award. Six of its sixteen
cases fail without the Simulation changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r2 of #2025: reg 5(2) takes expected weekly hours or, for
fluctuating hours, a complete recognisable cycle or the five weeks before
the claim (or a more accurate period), and reg 5(7) counts paid meal
breaks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 2, 2026
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>
…gates' into is-remunerative-work-jsa-conditions
MaxGhenis added a commit that referenced this pull request Oct 2, 2026
Takes policyengine_uk/simulation.py and policyengine_uk/utils/inputs.py
byte-identical from 48abd88 on #2025 (is-remunerative-work-jsa-conditions).
In policyengine-core 3.32.9, delete_arrays keeps the deleted arrays'
_user_input_keys entries, and clone shares the record. A formula value could
then look like a direct input, after a deletion or an input on a clone or on
a parent after a branch. The UK Simulation now gives each clone its own copy
of the record and drops the entries for deleted arrays, and entered_directly
asks about the stored value the simulation actually reads.

test_claimant_or_partner_awards_entered_directly.py mirrors #2025's lifecycle
cases on claimant_or_partner_esa_income and claimant_or_partner_jsa_income.
6 of its 12 cases fail without the Simulation changes. #2025's own test file
asserts its s.124(1)(f) JSA bar, which this branch does not have, so it is
not copied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 2, 2026
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 and others added 3 commits October 2, 2026 02:18
Review r3 of #2025 (finding 7): a school-year cycle leaves out school
holidays and other periods the person is not required to work; reg
5(2)(b)(ii)'s five weeks run to the claim or a superseding decision; paid
refreshment time counts with meal breaks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#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>
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>
MaxGhenis and others added 2 commits October 2, 2026 02:54
…gates' into is-remunerative-work-jsa-conditions
The award on the reported amounts is whatever screen jsa_income applies
(jsa_income_eligible), so the comment stays true when that screen gains
conditions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 2, 2026
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>
MaxGhenis and others added 2 commits October 2, 2026 03:04
…gates' into is-remunerative-work-jsa-conditions
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
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>
MaxGhenis added a commit that referenced this pull request Oct 2, 2026
…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 and others added 2 commits October 2, 2026 04:11
… boundary

Review r4 of #2025:
- jsa_income is stored as float32, while the report sum and the helper's
  award are float64. With large reports (£65,536.01 + £65,536.00) the
  formula's own award looked entered and barred the claim. Both references
  are now cast to the stored dtype before comparison.
- Under the value convention, another member's report can change how a
  directly entered award is read. The variable documentation and the
  property module now state that boundary and the half-penny tolerance
  instead of claiming invariance for every input.
  test_income_support_direct_inputs.py pins the intended transitions: an
  excluded adult's report equal to a direct award, and a claimant's report
  that explains it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gates' into is-remunerative-work-jsa-conditions

# Conflicts:
#	policyengine_uk/tests/test_income_support_eligibility_properties.py
#	policyengine_uk/variables/gov/dwp/income_support_eligible.py
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>
…d direct-award modes

Review r5 of #2025 (APPROVE WITH NITS):
- The documentation and property module describe the convention for a
  stored esa_income or jsa_income however it was supplied (entered or
  replaced by a reform), compared within half a penny after rounding to
  the stored precision.
- test_the_half_penny_tolerance_endpoints pins £100.004 (explained) and
  £100.006 (the couple's) against a direct £100.
- test_direct_awards_match_the_reference_deterministically checks both
  direct-award modes against the oracle for a fixed carer; it kills a
  mutant that ignores direct ESA, which the 25-example differential let
  survive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 2, 2026
Review r3 of this PR, and #2025's review round 5:

- Sch 1B para 2 covers a placed "child", which is a person under 16
  (SSCBA s.137(1)). The gate now requires the placed member to be under
  16 (is_child_for_child_benefit). YAML pins the boundary: a placed 15
  year old counts, a placed 16 year old does not. The property generator
  draws placed members aged up to 17, and the oracle applies the limit.
- A zero esa_income could still bar the claim. Reports of £10.01 and
  £41.99 sum a little above £52 in stored precision, so after £52 of
  tariff income the formula leaves a sub-penny residual. A zero entry, or
  a neutralised esa_income, then matched that residual within half a
  penny and was read through the reports. The bar now also requires
  esa_income > 0.
- The documentation now says the exception covers any stored value that
  is not the formula's own result: entered directly, or replaced, scaled
  or removed by a reform. The comparison is within half a penny after
  rounding to the stored precision. A tolerance test pins £100.004
  against £100.006.
- The para 2 comment says the looked-after flag also marks children
  living away in care, whom para 2 does not cover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#2013 (0953c8a) limits Sch 1B para 2 to placed children under 16 and
guards the ESA bar so a stored zero never bars the claim. The documentation
conflict is resolved with wording covering both awards. JSA gets the same
guard: a stored jsa_income of zero within half a penny of a sub-penny award
on the reports no longer bars the claim. test_a_stored_zero_never_bars_the_claim
pins it for an entered zero and a removed (neutralised) award; it 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
…t cap left unscoped

Review r2 of #2027 (subfleet job 20261002-044711) and #2013/#2025's latest
value-rule refinements (0953c8a, 081ec14):

- claimant_or_partner_{esa,jsa}_income: a stored zero is zero. References
  are compared at the stored float32 precision. When the stored award is
  the plain total of the reports (the disable_simulated_benefits reform),
  the claimant's and partner's plain reported total passes through
  unscreened, so a claimant's own £200 at £10,000 capital is no longer
  zeroed. Entering these variables directly is the explicit way to state
  ownership.
- is_on_income_related_esa / _based_jsa: another member's own report counts
  only while the benefit unit's modelled award is positive, so a reform
  that removes the benefit takes the status with it. is_on_income_support:
  likewise only while Income Support is active and not neutralised.
- is_benefit_cap_exempt_health_disability is reverted to the benefit unit's
  esa_income. benefit_cap_reduction (out of scope) still caps every
  member's ESA and JSA, so scoping the exemption alone would cap the
  claimant with another adult's own award. Both go to the benefit-cap
  follow-up.
- The property generator fixes claims_all_entitled_benefits per benefit
  unit (it sums reports across the simulation). SCP comments name the
  kinship route. The childcare citation is reg 1(2), not 2(1). Changelog
  wording matches the paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 2, 2026
Brings #2025's stored-award convention wording, its tolerance tests and
test_direct_awards_match_the_reference_deterministically. In
test_income_support_eligibility_properties.py the monotonicity waiver for a
lost calculated award is kept and the new deterministic test follows it;
reference_eligibility's signature is unchanged.

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 under-16 limit on Sch 1B para 2 and the zero-award guard on
both income-related bars, with their tests. Clean merge.

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. This reads s.124(1)(c) and (f) correctly:

  • 16 hours for the claimant (IS Regs reg 5(1)) and 24 for the partner (reg 5(1A): "as though for the words '16 hours' there were substituted the words '24 hours'");
  • carers are taken out of remunerative work whether they are the claimant or the partner (reg 6(4)(c): "a person to whom paragraph 4 of Schedule 1B applies");
  • the partner's JSA bars the claim only if it is income-based ("the other member of the couple is not, and the couple are not, entitled to an income-based jobseeker's allowance").

Verified at 081ec14:

  • Law: the point-in-time texts of reg 5 give 24 hours on 6 April 1992, 16 from 7 April 1992, and (1A) in force at 7 October 1996, so both parameter histories are right. (The current page's F1 note on reg 5(1) is mislabelled, but the values hold.)
  • Tests: the income_support YAML passes 86/86. Lifecycle 16/16, direct inputs 13/13, properties 4/4, parameters 3/3.
  • Probes for 2025, comparing #2013's head with this one:
    • A lone parent with a 3-year-old is still eligible at 15 hours. At 16 hours she becomes ineligible (#2013 left her eligible).
    • A carer claimant whose partner works 23 hours is eligible; at 24 hours, ineligible.
    • A partner who is a carer and works 30 hours does not bar the claim.
    • A partner on contribution-based JSA leaves Income Support at £10,663.43, the same as on #2013.
  • Merges cleanly with #2027 and #2075. The body gives aggregates only.
  1. Should fix (inherited from #2013, but this PR carries the same gate): #2013 conflicts with main in income_support_eligible.py and income_support_carer_route.yaml. The cause is #2001, which merged on 4 Oct. In the rebase:
    • income_support_eligible.py:101 should take main's has_attained_state_pension_credit_qualifying_age, not is_SP_age.
    • Decide the s.124(1)(g) limb. Main keeps on_pension_credit (main :52-61); the stack drops it.
    • Then re-run the 23 cases and the three properties.
  2. Nit: #2001 and #1896 have both merged, so update the stack line in the body: this is now stacked only on #2013.

Base automatically changed from is-eligibility-claimant-partner-gates to main October 7, 2026 15:57
MaxGhenis and others added 4 commits October 8, 2026 11:48
Brings in #2013 (a094911) and everything else main added. Two conflicts:

- income_support_eligible.py: keeps this branch's rule (in the claimant
  conjunction, no remunerative work at claimant_hours, no contribution-based
  JSA, and no other member of the couple in remunerative work at
  partner_hours; carers never in remunerative work; income-based JSA of the
  claimant or partner, read by value, bars the claim). Takes main's age test
  (has_attained_state_pension_credit_qualifying_age), its s.124(1)(g) bar
  (meets_pension_credit_age_conditions and would_claim_pc) and its
  half-penny ESA zero guard, with their comments; the documentation states
  all of them.
- JSA limb: an amount within half a penny of zero is no award, as main now
  does for ESA (#2013's after-merge review, finding 3; review r6b finding 4
  here). Reports that exactly match the tariff income leave a float
  residual that otherwise barred the claim. Three new tests in
  test_income_support_direct_inputs.py pin the formula's residual, an
  entered £0.003/£0.006, and the couple's residual beside another member's
  report; the changelog says so.
- test_income_support_eligibility_properties.py: the oracle takes main's
  attained_qualifying_age and on_pension_credit as well as this branch's
  entered JSA and work conditions; an entered JSA within half a penny of
  zero is no award; the docstring states both sets of conditions; the
  deterministic test passes on_pension_credit=False.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The monotonicity docstrings now say why more JSA cannot help through
  s.124(1)(g): reported income-based JSA ends the inferred SI 2019/37
  saving only for a couple in which neither member reports Income
  Support, and such a family has no award holder.
- The JSA formula's-residual test cross-references the couple's-residual
  test, as main's ESA test does, and notes the residual comes from core
  summing the reports in float64.
- New tests pin the couple's award tolerance at half a penny for JSA and
  ESA (£52.004 is no award, £52.006 bars the claim). Each fails when its
  guard is set to > 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Confirmatory review (job 20261008-184614-is-work-jsa-review-after-merge-r2,
finding 1): reported income-based JSA ends only the Housing Benefit route
to the inferred SI 2019/37 saving; reported Pension Credit keeps the saving
on its own (has_mixed_age_couple_pension_credit_saving). Docstring only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis merged commit 4ed8713 into main Oct 9, 2026
6 checks passed
@MaxGhenis
MaxGhenis deleted the is-remunerative-work-jsa-conditions branch October 9, 2026 01:50
MaxGhenis added a commit that referenced this pull request Oct 9, 2026
…acy-award-readers-claimant-partner

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 9, 2026
#2025 merged as 4ed8713. Conflict: test_income_support_eligibility_properties.py,
whose docstrings now carry both main's s.124(1)(g) Pension Credit reading and
this branch's remunerative work tests and lost-award waiver. The Class 4 lower
profits limit merged cleanly: this branch's 2023-04-06 £12,570 (SSCBA 1992
s.15(3)(a)) plus main's 2026-31 entries, all £12,570.

Main (#1919) now charges ni_class_4_main on ni_class_4_profits: after capital
allowances, the trading allowance and Sch 2 loss relief. ESA reg 99(3)(b)
charges the notional Class 4 on reg 99(4)'s chargeable income, the reg 98
profit, from which reg 98(5)(a), (b), (d) and (11) bar capital expenditure,
depreciation and losses from another trade or an earlier period. So
esa_exempt_work_earnings now computes it on the profit, under the model's
Class 4 age rule (ni_class_4_liable), with two YAML cases (capital allowances,
another trade's loss) where the two differ across the higher limit.

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

Conflicts:
- claimant_or_partner_{esa,jsa}_income documentation: main's wording (young
  persons with their own award, the half-penny comparison) plus this PR's
  (the Income Support gate reads it; disable_simulated_benefits sets it for
  each year).
- income_support_eligible: keep main's Pension Credit bar (s.124(1)(g)) and
  the SPC qualifying age; the ESA and JSA limbs read the claimant-or-partner
  awards as before (> 0).

Main's gate also gained a half-penny rule after #2013's after-merge review
(6eb8db7): an award within half a penny of zero never bars the claim, so
the float residual left by reports that exactly match the tariff income is
no award. To keep that rule while the limbs read the shared variables,
claimant_or_partner_award now gives zero when the stored award or the
claimant's and partner's part of it is within half a penny of zero. Every
reader of the two variables (passports, means tests, the gate) now reads
the residual the same way; on the formula path the gate equals main's in
every case except the declared plain-total one.

The differential test's copy of the replaced code is now main's gate at
e89b8ab (half-penny limbs, has_attained_state_pension_credit_qualifying_age,
on_pension_credit), and pre_switch_limb uses the same half-penny thresholds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Income Support eligibility omits the remunerative work and JSA conditions (SSCBA 1992 s.124(1)(c), (f))

2 participants