Skip to content

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

Open
MaxGhenis wants to merge 18 commits into
is-eligibility-claimant-partner-gatesfrom
is-remunerative-work-jsa-conditions
Open

MaxGhenis wants to merge 18 commits into
is-eligibility-claimant-partner-gatesfrom
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. It is stacked on #2013, which is stacked on #2001 and #1896, and 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)). Retarget to main once those merge.

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. An entered award 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.

Changes

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 direct award 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 a direct 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.
  3. Monotonicity. Raising any claimant's or partner's hours or JSA never turns an ineligible family eligible, because (c) and (f) only bar a claim. This is a new property.
  4. Refactor. jsa_income is unchanged by the helper refactor. On the Enhanced FRS, jsa_income and all 90 other saved arrays 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 (11 tests): 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

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

Four package states, each run in its own environment on a private copy of the dataset: #2013's head (8d8673a), (c) only, (f) only, and this PR (fcbec88). After review, the current 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). Every year 2025-2030 changes by £0 at each step and in the re-run, with all 91 saved arrays identical. 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.

Why nothing moves:

  • No benefit unit eligible before this change 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. All of them were already ineligible, because the award holder is neither a carer nor a lone parent of a young child (s.124(1)(e)).

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.

🤖 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>

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