Repository navigation
Fix benefit-cap statutory-pay earnings exemption - #2399
Conversation
|
Handoff at e656430 on The cap now uses actual UC net earnings before the MIF. The release family is pinned in a plain regression and Local model validation is incomplete. The sandbox rejected Outstanding: pinned regression, full property file, remaining targeted UC/cap Python and YAML tests, controlled constant-pool comparison, microsim impact and the hub's independent review. No in-job review or merge was performed. The PR body includes cause, law, source/wheel local-constant mining and the additional stale-base gap in #2119's original check. Axiom composition cases are queued in rulespec-uk#387.
|
|
UK hub independent review (in-session Opus 5.5; the Codex and Claude lanes were closed), head VERDICT: REQUEST_CHANGES (UK hub in-session independent review, Opus 5.5, 2026-10-13 ~18:00Z. Used because every Codex lane was held and the Claude lane was closed.) I ran no tests, simulations or builds. Everything below comes from reading code, hand arithmetic, legislation.gov.uk, and the logs of the PR's own CI run 38437021879, which finished during this review. Findings
Check 1: the law
Check 2: the falsifying familyHand arithmetic with the repo's parameters: personal allowance 12,570, basic rate 20%, primary threshold 241.73 × 52 = 12,569.96, Class 1 rate 8%,
Check 3: the six YAML casesAll six are right by hand, and CI's YAML job passed at this head. The single-claimant cap outside London is 14,753.
Check 4: the property file
Check 5: who moves
Check 6: overlap with #1820
Check 7: the CI-gap claim
Check 8: anything else
|
- The basic-income case now enters an award of 25,000, above the couple cap outside London (22,020), so its zero cap reduction shows the exemption working. A control case without the reform toggle shows basic income not counting: no exemption, reduction 2,980. - Say that counting basic income in the earnings exception is new in this PR (main never counted it), not existing behaviour. - The capital-income property compares the capped award only where the exemption is unchanged because capital income can change another exemption route, not because of the allowance taper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release workflow tests the installed wheel without coverage, so
test_a_simulation_leaves_coverage_no_new_code_objects skips there
("coverage is not measuring with sys.monitoring"). It had no allowlist
entry, so every wheel-mode Release test / Result failed on it, behind the
statutory-pay failure this PR fixes. The entry is limited to wheel mode
and to that skip reason. The test still runs and passes in source mode on
every PR.
Replaying this PR's own release run (38479445636) through
check_test_shards.py with the entry: every collected test ran in exactly
one shard and passed, apart from allowlisted outcomes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged by the UK hub (audit). Merged with a merge commit at the tested head
This PR fixes the first in the model and allowlists the second for wheel mode only. Gates:
|
#2399 landed the same benefit-cap earnings exception change this branch made (is_benefit_cap_exempt_earnings reads uc_individual_earned_income_before_mif). The only conflict was in that file's comments; take main's version, so this PR no longer touches it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve conflicts with #2399, #2076 and #1965: - is_benefit_cap_exempt_earnings: keep main's per-claimant uc_individual_earned_income_before_mif test and documentation; keep this PR's dated threshold parameter in place of 10_152 and its reg 57 and FA 2004 s.188 references. - is_benefit_cap_exempt_health_disability: take main's version (#2076) and add the UC route for AFIP: on a Universal Credit award only a claimant's counts; otherwise Housing Benefit's young person counts too. - is_benefit_cap_exempt: take main's label. - test_uc_single_claim_properties: keep main's AFCS assertions and this PR's partner-only AFIP example. Pin main's young-person AFIP YAML case to the Housing Benefit route, and give the specified-benefits property oracle the family's scheme. Rewrite the changelog fragment to what the PR still changes relative to main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…shared-cap-lha-rules #2399 changed the shared earnings exception this PR deletes. Its measure, uc_individual_earned_income_before_mif summed over the assessed claimants (reg. 82(1)(a), excluding MIF income under reg. 82(4)), is carried over unchanged into the UC-only is_uc_benefit_cap_exempt_earnings; the shared file stays deleted. Main's new tests that read the deleted shared names now read the UC ones, with their expected values unchanged: - test_uc_statutory_pay_properties.py (#2399; conflict): main's text and regression case, reading is_uc_benefit_cap_exempt(_earnings). - benefit_cap_earnings_exception_earned_income.yaml (#2399) and uc_trading_loss.yaml (#2085). - test_trading_loss_properties.py (#2085). Targeted YAML (benefit_cap folder, benefit_cap_by_scheme, uc_trading_loss): 121 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebased onto main after #2399, which made the benefit cap earnings fix. Only the test-environment change remains: conftest stops Hypothesis drawing literal constants from policyengine_uk's own modules, and a guard test fails if a Hypothesis upgrade bypasses that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #2397
Raising statutory sick pay could end the benefit-cap earnings exemption because it used a separate calculation that omitted statutory pay while deducting its tax and NI. The exemption now sums the assessed claimants' existing actual UC net earnings before the minimum income floor. This addresses the benefit-cap portion of #2370 and fixes item 1 of #1986 apart from its earnings threshold.
The release family is pinned in a plain regression and an explicit Hypothesis example. The monotonic property is retained, and statutory pay is compared with equivalent wages after the cap as well as before it. Policy cases cover all three modelled statutory payments, pension tax, relievable contributions, the MIF exclusion, and basic income with and without its means-test toggle (the award is set above the couple cap, so both cases bite). The dividend-tax earnings-exception regression now runs as a normal test, with its obsolete strict xfail and outcome-allowlist entry removed. The private-pension case identifies the lowest-slice tax allocation as the model's convention, as documented in
uc_income_tax_on_earnings, rather than wording prescribed by reg. 55(5)(b).Law: UC Regulations 2013 reg. 82(1)(a) and (4) test actual claimant/couple earned income and exclude earnings imputed by the minimum income floor. Reg. 55(4)–(5) includes statutory pay and permits employment-specific deductions. The existing £10,152 annual threshold is retained in this release fix. #1820 uses the same expression,
benunit.sum(uc_individual_earned_income_before_mif * is_uc_assessed_claimant), plus a dated National Living Wage threshold parameter; its version ofis_benefit_cap_exempt_earnings.pysupersedes this file when it merges.Scope: reusing UC's earned-income variable changes the old measure in each of these ways, including current main's #1965:
taxable_miscellaneous_incomecounts as earnings; its classification is the open question in UC counts royalties, trust income and overseas pensions in miscellaneous_income as earned income #1961.gov.contrib.ubi_center.basic_income.interactions.include_in_means_testsis enabled. Main's formula never counted it, so this is new behaviour under that reform toggle (default off); see Open questions below.History: the pinned family already loses its earnings exemption at the 2.127.0 version commit e01f2f5. #2119 did not change the cap earnings formula. Commit 70851ab fixed the UC earnings variable through #2380 but left the cap's duplicated calculation behind.
CI gap: both source and installed-wheel modes collect the property with locked Hypothesis 6.169.0. Hypothesis's
function_digesthashes cleaned function source and signature, not the module name or file path. However,internal/conjecture/providers.py::_get_local_constantsmines literals from imported local modules, andinternal/constants_ast.py::is_local_module_fileexplicitly excludes/site-packages/. Source model modules therefore supply a different constant pool from installed-wheel modules, changing integer draws despite the same derandomized seed. Source #2394 collects and passes the property; release 38366204407 fails it. Separately, #2119's original green PR run tested a synthetic merge based on e01f2f5, which lacked the property. The explicit example covers the release family in both modes regardless of generated inputs.Validation:
c0f501b5e, the earnings-exception YAML file passed 8 cases, including the basic-income case and its toggle-off control (run by the hub; log kept with the hub's review files).uv buildwith.github/build-constraints.txt; installed with--no-depsinto a scratch venv synced with--frozen --extra dev; run from an empty directory with the CI Hypothesis profile and strict xfail handling; installed files checked byte-identical to05610e4d7; the later commits change only the earnings-exception YAML file, one comment in a test and the CI allowlist, so these results carry over):test_uc_statutory_pay_properties.pyandtest_uc_income_from_capital_properties.py: 14 passed;test_uc_company_owner_properties.py,test_uc_claimant_income_properties.py,test_benefit_cap_and_ctr_pension_age_properties.pyandtest_benefit_cap_specified_benefits_properties.py: 42 passed.ruff format --checkandruff checkpass.c0f501b5e(run 38479445636) ran the whole suite from the wheel: 5,537 passed, 69 skipped, 1 xfailed.Release test / Resultstill failed on one more release blocker, which main has had since 10/12.test_variable_code_sharing.py::test_a_simulation_leaves_coverage_no_new_code_objectsskips in wheel mode ("coverage is not measuring with sys.monitoring") and had no allowlist entry. The statutory-pay failure hid it on main's release runs 38306431347 and 38366204407. The final commit,8b997692f, adds a wheel-only allowlist entry limited to that skip reason; the test still runs and passes in source mode. Replaying run 38479445636's outcomes throughcheck_test_shards.pywith the entry gives: every collected test ran in exactly one shard and passed, apart from allowlisted outcomes.Impact (measured by the UK hub, 2026-10-13): a real
Microsimulationpair for 2026 onenhanced_frs_2024_25.h5from uk-data 1.58.0 (sha2565ba399ea5ab9…), with stored weights. The branch is05610e4d7, which includes main a98f8f0 and so #1965. The later commitsc0f501b5eand8b997692fchange only tests and the CI allowlist. The baseline is the same code with main's version ofis_benefit_cap_exempt_earningsswapped in, which is this PR's only model change. A first run ate6564306b, before the main merge, gave the same figures to the pound.Open questions:
gov.contrib.ubi_center.basic_income.interactions.include_in_means_testsmakes basic income count toward this earnings exception through the existing UC earned-income variable. The default is false. Main's formula never counted basic income, so this PR introduces it under the toggle, and two YAML cases pin it. Pin benefit cap exemption rules with regression tests and legal references (#1818) #1820 uses the same expression and should settle its treatment alongside the remaining threshold part of Benefit cap, targeted childcare and local CTR use the wrong UC earned income figure #1986 item 1.Reproducer: policyengine_uk/tests/test_uc_statutory_pay_properties.py::test_statutory_sick_pay_preserves_benefit_cap_earnings_exemption
axiom: TheAxiomFoundation/rulespec-uk#387 queued
That existing
pe-parityissue queues reg. 55 statutory pay and earnings deductions; the pinned corpus lacks reg. 55, so the signed encoder is blocked. The existing reg. 82 module reads actual earnings excluding the MIF; cap-composition companion cases are added to the same queue. No billed encoder run or in-job review was started.🤖 Generated with Claude Code