From 71a7a28f319e3181426835a124b892be5a0754cd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 22:05:32 +0000 Subject: [PATCH 01/11] Correct #2225 Housing Benefit special disregard comments Follow-ups N1-N3 from the review of #2225: the test comment names the disability premium route, the eligibility docstring names the carer (paragraph 4) route, and the disregard comment lists paras 3 and 4/5. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6FaRutJG2o4ATFgPYn --- ..._tax_reduction_pensioner_earnings_disregard_properties.py | 5 +++-- .../housing_benefit_applicable_income_disregard.py | 4 +++- .../housing_benefit_special_earnings_disregard_eligible.py | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/policyengine_uk/tests/test_council_tax_reduction_pensioner_earnings_disregard_properties.py b/policyengine_uk/tests/test_council_tax_reduction_pensioner_earnings_disregard_properties.py index c40751e943..dafb5dd601 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_pensioner_earnings_disregard_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_pensioner_earnings_disregard_properties.py @@ -370,8 +370,9 @@ def test_housing_benefit_disregard_is_the_statutory_weekly_sum_in_every_year( def test_disabled_working_age_single_gets_the_special_hb_disregard(): # The property above reads special eligibility from the model, so pin it - # here. With no hours the additional sum cannot apply, and tax credits - # have ended by 2026, so the working tax credit route is off. + # here. A disabled working-age single has the disability premium, which + # gives the £20 (SI 2006/213 Sch 4 para 3); with no hours the additional + # sum cannot apply. unit = dict( shape="single", nation="ENGLAND", diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py index 7de5c94a3d..ed00e229ab 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py @@ -42,7 +42,9 @@ class housing_benefit_applicable_income_disregard(Variable): def formula(benunit, period, parameters): p = parameters(period).gov.dwp.housing_benefit.means_test.income_disregard net_earnings = benunit("housing_benefit_net_earnings", period) - # Working age Sch 4 paras 4, 7 and 10; pension age Sch 4 paras 2 and 7. + # Working age Sch 4 paras 3, 4, 7 and 10; pension age Sch 4 paras 2, + # 4, 5 and 7. Other full-amount £20 routes are pre-assessed + # (housing_benefit_special_earnings_disregard_eligible). standard = benunit("housing_benefit_special_earnings_disregard", period) # Working age Sch 4 para 18 (NI Sch 5 para 18), from 5 October 2026: # specified or temporary accommodation. It applies alongside paras 3 diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_special_earnings_disregard_eligible.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_special_earnings_disregard_eligible.py index f1735a222f..47569ec0e2 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_special_earnings_disregard_eligible.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_special_earnings_disregard_eligible.py @@ -3,7 +3,7 @@ class housing_benefit_special_earnings_disregard_eligible(Variable): """Implement the working-age premium route and the modelled pension-age - paragraph 5 qualifying routes. + carer (paragraph 4) and paragraph 5 qualifying routes. GB SI 2006/214 Sch. 4 and NI SR 2006/406 Sch. 5 para. 5(1)(a) list the following receipts implemented here: From f0d9ba4958e9d096a33e8e8eff37aca2b0fc4b9f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 22:05:41 +0000 Subject: [PATCH 02/11] Remove duplicate HB applicable income reference from #2211 The reference tuple listed uksi/2006/214/regulation/29 twice; keep the first. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6FaRutJG2o4ATFgPYn --- .../applicable_income/housing_benefit_applicable_income.py | 1 - 1 file changed, 1 deletion(-) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py index 7d393e0df8..ebbb00573e 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py @@ -147,7 +147,6 @@ class housing_benefit_applicable_income(Variable): "https://www.legislation.gov.uk/uksi/2006/213/regulation/40", "https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/16", "https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/47", - "https://www.legislation.gov.uk/uksi/2006/214/regulation/29", "https://www.legislation.gov.uk/uksi/2006/214/regulation/26", "https://www.legislation.gov.uk/uksi/2006/214/regulation/27", "https://www.legislation.gov.uk/nisr/2006/406/regulation/24", From eeb483f0a9e58878148b9183a5228e707bd343f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 22:06:03 +0000 Subject: [PATCH 03/11] Correct the stale SCP worked figure from #2575 scottish_child_payment annualises by WEEKS_IN_YEAR (52), so the expected 1518.40 is 29.20 x 52, not 29.20 x 365.25 / 7. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6FaRutJG2o4ATFgPYn --- .../gov/social_security_scotland/september_cpi_uprating.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/social_security_scotland/september_cpi_uprating.yaml b/policyengine_uk/tests/policy/baseline/gov/social_security_scotland/september_cpi_uprating.yaml index a2f8077707..236b9110f1 100644 --- a/policyengine_uk/tests/policy/baseline/gov/social_security_scotland/september_cpi_uprating.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/social_security_scotland/september_cpi_uprating.yaml @@ -6,7 +6,7 @@ # Annual model periods use the fiscal-year parameter amounts. - name: Scottish Child Payment projects to 29.20 a week in April 2027 - # 28.20 x 1.035 = 29.187 -> 29.20, then 29.20 x 365.25 / 7 = 1523.614286. + # 28.20 x 1.035 = 29.187 -> 29.20, then 29.20 x 52 = 1518.40. period: 2027 absolute_error_margin: 0.01 input: From 7edc2a45727ea4eabddff7330f944c3596a96989 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 22:06:13 +0000 Subject: [PATCH 04/11] Name statutory paternity pay in the relevant UK earnings docstring relevant_uk_earnings reads taxable_employment_income, which adds employment_benefits: SSP, SMP and SPP. The docstring named only SSP and SMP (found in the review of #2066). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6FaRutJG2o4ATFgPYn --- policyengine_uk/utils/pensions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/policyengine_uk/utils/pensions.py b/policyengine_uk/utils/pensions.py index 8b5ecb3ff0..be14ef3e7c 100644 --- a/policyengine_uk/utils/pensions.py +++ b/policyengine_uk/utils/pensions.py @@ -27,9 +27,9 @@ def relevant_uk_earnings(person, period, hmrc): """FA 2004 s.189(2) relevant UK earnings. Employment income chargeable to income tax is net taxable earnings - (ITEPA 2003 ss.9(2) and 11): pay and employer-paid taxable SSP/SMP, which - the model holds separately from wages, less the allowable expenses in - s.327(3). s.327(5) also lists the deductions for pension contributions + (ITEPA 2003 ss.9(2) and 11): pay and employer-paid taxable SSP, SMP and + SPP (employment_benefits), which the model holds separately from wages, + less the allowable expenses in s.327(3). s.327(5) also lists the deductions for pension contributions (FA 2004 ss.188-194), but those are the relief that these earnings limit, so they are not taken off; the limit would otherwise depend on itself. Trading income is the profit chargeable under ITTOIA 2005 Part 2, before From 3f427f4f4a6ca5c39c5a5c13ff7bb575f9b18cdb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 22:07:06 +0000 Subject: [PATCH 05/11] Rewrap the September CPI uprating README paragraph #2535 left a long line in the economic assumptions README; rewrap it to the file's 79-character width. Wording unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6FaRutJG2o4ATFgPYn --- .../parameters/gov/economic_assumptions/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/policyengine_uk/parameters/gov/economic_assumptions/README.md b/policyengine_uk/parameters/gov/economic_assumptions/README.md index b0db05c16f..a7ef5d5f4f 100644 --- a/policyengine_uk/parameters/gov/economic_assumptions/README.md +++ b/policyengine_uk/parameters/gov/economic_assumptions/README.md @@ -237,9 +237,9 @@ and so do the Universal Credit standard allowance and health element indices built from it. Housing Benefit non-dependant deduction income bands, DfE funding rates, the Childcare Offer for Wales hourly rate and the council tax high value surcharge follow the lagged calendar-year CPI projection through -`indices.obr.lagged_cpi`, pending their own uprating models. The deduction amounts continue to follow September CPI. -The savings credit threshold and Housing -Benefit pension-age allowances are within the s150 review, under +`indices.obr.lagged_cpi`, pending their own uprating models. The deduction +amounts continue to follow September CPI. The savings credit threshold and +Housing Benefit pension-age allowances are within the s150 review, under [s150(1)(l) and (h)](https://www.legislation.gov.uk/ukpga/1992/5/section/150), even though their increases in practice are linked to earnings rather than September CPI. From 791fa6344e1744971c5a08b1ea54c1d813873b31 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 22:07:19 +0000 Subject: [PATCH 06/11] Rewrap the reflowed Wales childcare default-education comment #2535 left a short line in this YAML comment; join it so the block wraps to 79 characters. Wording unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6FaRutJG2o4ATFgPYn --- .../childcare_offer/wales_childcare_offer.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/welsh_government/childcare_offer/wales_childcare_offer.yaml b/policyengine_uk/tests/policy/baseline/gov/welsh_government/childcare_offer/wales_childcare_offer.yaml index ea2e486d61..71d1e84660 100644 --- a/policyengine_uk/tests/policy/baseline/gov/welsh_government/childcare_offer/wales_childcare_offer.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/welsh_government/childcare_offer/wales_childcare_offer.yaml @@ -484,8 +484,8 @@ wales_childcare_offer: 0 # current_education's default formula puts an 18- or 19-year-old in tertiary -# education when nothing is supplied, so a parent of that age -# counts as a parent in education (guidance para 71). Survey data supplies +# education when nothing is supplied, so a parent of that age counts as a +# parent in education (guidance para 71). Survey data supplies # current_education, so this default matters only for household calculations. - name: A 19-year-old lone parent with no education input counts as in education by default period: 2026 From d6f580c42d13c6b4a65fbd95ce5cca88434493d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 22:07:28 +0000 Subject: [PATCH 07/11] Name director and apprentice status in the salary sacrifice NI properties The rows strategy draws is_full_year_category_a_director and is_apprentice for every person; the module docstring's 'for any' list now names both (follow-up from the review of #2505). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6FaRutJG2o4ATFgPYn --- .../tests/test_salary_sacrifice_ni_measure_properties.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/policyengine_uk/tests/test_salary_sacrifice_ni_measure_properties.py b/policyengine_uk/tests/test_salary_sacrifice_ni_measure_properties.py index 5251df7f06..d737326620 100644 --- a/policyengine_uk/tests/test_salary_sacrifice_ni_measure_properties.py +++ b/policyengine_uk/tests/test_salary_sacrifice_ni_measure_properties.py @@ -13,8 +13,9 @@ the model's own liability variables, so it does not depend on the formulas under test. -Invariants, for any pay, salary sacrifice, age, statutory sick pay and -employer pension contribution: +Invariants, for any pay, salary sacrifice, age, statutory sick pay, employer +pension contribution, full-year Category A director status and apprentice +status: 1. Difference of complete liabilities: the employee measure equals ni_class_1_employee with the excess less ni_class_1_employee From 680183822b06a71fdb63ac46fd09ce813ff8c738 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 22:08:09 +0000 Subject: [PATCH 08/11] Check the Wales childcare income sources expand to disjoint variables Follow-up r9 P3-3 from the review of #2535: the 40 listed sources' adds/subtracts closures are pairwise disjoint, so no income is counted twice. They are today. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6FaRutJG2o4ATFgPYn --- ...st_wales_childcare_offer_income_sources.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/policyengine_uk/tests/test_wales_childcare_offer_income_sources.py b/policyengine_uk/tests/test_wales_childcare_offer_income_sources.py index fd3ae03c78..3508c8ffed 100644 --- a/policyengine_uk/tests/test_wales_childcare_offer_income_sources.py +++ b/policyengine_uk/tests/test_wales_childcare_offer_income_sources.py @@ -14,8 +14,8 @@ would just lose its case; the lists are also pinned to the reviewed sources below, so a removal fails here too. Each source is also checked not to add another listed source (through adds or subtracts, at any depth), which would -count it twice. That check does not see a formula that reads another listed -source. +count it twice, and the sources are checked to expand to disjoint variables. +Those checks do not see a formula that reads another listed source. """ from pathlib import Path @@ -156,3 +156,18 @@ def test_each_list_holds_the_reviewed_sources_once(name): def test_no_listed_source_adds_another(entity, source): listed = {other for _, other in CASES} assert not components(source) & listed + + +def test_listed_sources_expand_to_disjoint_variables(): + # Each source's closure is the source and every variable it adds or + # subtracts, at any depth. Overlapping closures would count the shared + # income twice, even where neither source adds the other. + closures = {source: {source} | components(source) for _, source in CASES} + overlaps = { + (first, second): sorted(closures[first] & closures[second]) + for i, first in enumerate(closures) + for second in list(closures)[i + 1 :] + if closures[first] & closures[second] + } + assert len(closures) == 40 + assert not overlaps, overlaps From 1df19f3948fa3209517ced1ab83935953499f0c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 22:19:24 +0000 Subject: [PATCH 09/11] Add claimant role properties from the review of #2082 Supplied role vectors are returned unchanged; widening an unflagged member's gap never makes a presumed child the partner; once identified parents take priority, presumed children and partners are disjoint. The reference's presumption is factored out so the properties can read it. All three pass; no model code changes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6FaRutJG2o4ATFgPYn --- ..._child_and_adult_definitions_properties.py | 129 ++++++++++++++++-- 1 file changed, 116 insertions(+), 13 deletions(-) diff --git a/policyengine_uk/tests/test_child_and_adult_definitions_properties.py b/policyengine_uk/tests/test_child_and_adult_definitions_properties.py index 24140408c9..bc6ee58fbf 100644 --- a/policyengine_uk/tests/test_child_and_adult_definitions_properties.py +++ b/policyengine_uk/tests/test_child_and_adult_definitions_properties.py @@ -44,6 +44,13 @@ claimant's child either way (the any-age gap). Before/after families share one Simulation to avoid repeated model construction, but occupy distinct households and benefit units. +5. Supplied claimant/partner role vectors, valid or not, are returned + unchanged on any benefit unit. Widening an unflagged member's gap below + the claimant, with the claimant, flags, other members and HBAI adulthood + fixed, never makes a member the presumption excludes the partner. + Identified parents take priority over the presumption; no other presumed + child is a claimant or partner. The two-other-parents exception to the + head being the claimant is kept. Scope: these are tests of the ENCODED statutory limbs, not full entitlement. Education status proxies full-time non-advanced study: hours, employment @@ -249,21 +256,36 @@ def claimants_or_partners(family, any_age_gap=20): # couple instead. Age ties go to the earlier member. any_age_gap=None is # the rule without the any-age gap, kept for the differential test. n = len(family) - adult = [not hbai_dependent_child(p, family) for p in family] - ages = [p["age"] for p in family] - heads = [i for i in range(n) if family[i]["is_benunit_head"] and adult[i]] - adults = [i for i in range(n) if adult[i]] - if not adults: + claimant, adult, presumed = presumption(family, any_age_gap) + if claimant is None: return [False] * n - pool = heads or adults - claimant = max(pool, key=lambda i: (ages[i], -i)) + ages = [p["age"] for p in family] parent = [adult[i] and family[i]["is_parent"] for i in range(n)] other_parents = [i for i in range(n) if parent[i] and i != claimant] if len(other_parents) >= 2 and not parent[claimant]: couple = sorted(other_parents, key=lambda i: (-ages[i], i))[:2] return [i in couple for i in range(n)] + pool = other_parents or [ + i for i in range(n) if adult[i] and i != claimant and not presumed[i] + ] + partner = max(pool, key=lambda i: (ages[i], -i)) if pool else None + return [i == claimant or i == partner for i in range(n)] + + +def presumption(family, any_age_gap=20): + # The reference claimant (None without an HBAI adult), HBAI adulthood and + # the members presumed to be the claimant's child, before identified + # parents take priority. + n = len(family) + adult = [not hbai_dependent_child(p, family) for p in family] + ages = [p["age"] for p in family] + heads = [i for i in range(n) if family[i]["is_benunit_head"] and adult[i]] + adults = [i for i in range(n) if adult[i]] + if not adults: + return None, adult, [False] * n + claimant = max(heads or adults, key=lambda i: (ages[i], -i)) young_child = any(ages[j] < 20 and ages[claimant] - ages[j] >= 16 for j in range(n)) - flag_unexplained = parent[claimant] and not young_child + flag_unexplained = family[claimant]["is_parent"] and not young_child def presumed_child(i): gap = ages[claimant] - ages[i] @@ -271,11 +293,7 @@ def presumed_child(i): return True return (ages[i] < 20 or flag_unexplained) and gap >= 16 - pool = other_parents or [ - i for i in adults if i != claimant and not presumed_child(i) - ] - partner = max(pool, key=lambda i: (ages[i], -i)) if pool else None - return [i == claimant or i == partner for i in range(n)] + return claimant, adult, [presumed_child(i) for i in range(n)] def legacy_child_or_young_person(p, claimant): @@ -992,3 +1010,88 @@ def test_wide_gap_couples_by_head_order_and_with_supplied_roles(): supplied = simulate(families) supplied.set_input("is_claimant_or_partner", YEAR, [True] * 4) np.testing.assert_array_equal(supplied.calculate("is_couple", YEAR), [True] * 2) + + +@PROPERTY_SETTINGS +@given(arbitrary_benefit_units(), st.data()) +def test_supplied_roles_are_returned_unchanged(families, data): + # Any Boolean role vector, valid or not, on any benefit unit: a supplied + # is_claimant_or_partner is an input, so the model never recomputes it. + size = sum(map(len, families)) + roles = data.draw(st.lists(st.booleans(), min_size=size, max_size=size)) + sim = simulate(families) + sim.set_input("is_claimant_or_partner", YEAR, roles) + assert_values(sim, "is_claimant_or_partner", roles) + + +@st.composite +def widened_gap_pairs(draw): + # An adult head (the claimant), flagged or not, an unflagged member aged + # 20+ at least 16 years younger, and any other members. The pair is the + # unit before and after the member is made younger, holding everything + # else fixed. + pairs = [] + for _ in range(draw(st.integers(1, 8))): + claimant_age = draw(st.integers(36, 90)) + family = [ + person(claimant_age, is_parent=draw(st.booleans()), is_benunit_head=True) + ] + for _ in range(draw(st.integers(0, 3))): + family.append( + person( + draw(st.integers(0, claimant_age)), + current_education=draw(st.sampled_from(EDUCATIONS)), + is_in_approved_training=draw(st.booleans()), + is_parent=draw(st.booleans()), + ) + ) + j = draw(st.integers(1, len(family))) + age = draw(st.integers(20, claimant_age - 16)) + family.insert(j, person(age)) + younger = [dict(p) for p in family] + younger[j]["age"] = draw(st.integers(0, age - 1)) + pairs.append((family, younger, j)) + return pairs + + +@PROPERTY_SETTINGS +@given(widened_gap_pairs()) +def test_a_wider_gap_never_makes_a_presumed_child_the_partner(pairs): + # Metamorphic: an unflagged member made younger, so their gap below the + # claimant widens, with the claimant, every flag, every other member and + # everyone's HBAI adulthood fixed. A member the presumption excludes from + # the partner pool stays excluded, so never becomes the partner. The + # two-other-parents exception is not asserted away: it never selects an + # unflagged member either. + cases = [] + for family, younger, j in pairs: + before, after = presumption(family), presumption(younger) + held = before[0] == after[0] == 0 and before[1] == after[1] + if held and before[2][j]: + cases.append((family, younger, j)) + if not cases: + return + combined = [unit for family, younger, _ in cases for unit in (family, younger)] + roles = simulate(combined).calculate("is_claimant_or_partner", YEAR) + offset = 0 + for family, younger, j in cases: + assert presumption(younger)[2][j] + assert not roles[offset + j], family + assert not roles[offset + len(family) + j], younger + offset += 2 * len(family) + + +@PROPERTY_SETTINGS +@given(arbitrary_benefit_units()) +def test_effective_children_and_partners_are_disjoint(families): + # An identified parent (an HBAI adult flagged is_parent) takes priority + # over the presumption. Every other presumed child is an effective child, + # and no effective child is ever the partner (nor the claimant). + roles = simulate(families).calculate("is_claimant_or_partner", YEAR) + offset = 0 + for family in families: + _, adult, presumed = presumption(family) + for i, p in enumerate(family): + effective_child = presumed[i] and not (adult[i] and p["is_parent"]) + assert not (effective_child and roles[offset + i]), family + offset += len(family) From 6cfab561d8ea2a9ec72e86a5ea2eef634690f7d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 22:19:24 +0000 Subject: [PATCH 10/11] Add changelog fragment for the review follow-ups Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6FaRutJG2o4ATFgPYn --- changelog.d/hub-review-followups-2026-10-25.changed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/hub-review-followups-2026-10-25.changed.md diff --git a/changelog.d/hub-review-followups-2026-10-25.changed.md b/changelog.d/hub-review-followups-2026-10-25.changed.md new file mode 100644 index 0000000000..504ff14594 --- /dev/null +++ b/changelog.d/hub-review-followups-2026-10-25.changed.md @@ -0,0 +1 @@ +Corrected comments, docstrings and citations from reviews of recently merged pull requests, and added property tests for claimant roles and the Childcare Offer for Wales income sources. From ae1c533a8952541c7475dc50fddf46d5f03f8887 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 28 Oct 2026 03:47:25 -0400 Subject: [PATCH 11/11] Apply the source-checked follow-ups for items 2, 5, 6, 7 and 8 - Item 2: cite SR 2009/276 reg 3, which inserts the full NI Child Benefit disregard as Sch 6 para 64, instead of the old fractional text. - Item 5: document the even-pound base-income assumption beside the invariant-2 margin assertion. - Item 6: the non-dependant exemptions are reg 74(6), (7), (8) and (10); reg 74(9) is a gross-income disregard. - Item 7: add the NI equivalent, SR 2006/405 reg 6(1), to remunerative_work_hours. - Item 8: name question 8's current-tax-year test as the alternative reading of the Welsh lump-sum treatment. Comments and references only; no formula or value changes. Co-Authored-By: Claude Opus 5.5 --- .../non_dep_deduction/remunerative_work_hours.yaml | 2 ++ .../childcare_offer/income/gross_income_sources.yaml | 5 +++-- .../housing_benefit_non_dep_remunerative_work.yaml | 2 +- ...t_statutory_paternity_pay_income_tax_properties.py | 3 +++ .../housing_benefit_applicable_income.py | 2 +- ...household_benefits_individual_non_dep_deduction.py | 11 ++++++----- 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/remunerative_work_hours.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/remunerative_work_hours.yaml index 0fecad832e..a481b43cca 100644 --- a/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/remunerative_work_hours.yaml +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/remunerative_work_hours.yaml @@ -10,3 +10,5 @@ metadata: href: https://www.legislation.gov.uk/uksi/2006/213/regulation/6 - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 reg. 6(1) href: https://www.legislation.gov.uk/uksi/2006/214/regulation/6 + - title: The Housing Benefit Regulations (Northern Ireland) 2006 reg. 6(1) + href: https://www.legislation.gov.uk/nisr/2006/405/regulation/6 diff --git a/policyengine_uk/parameters/gov/welsh_government/childcare_offer/income/gross_income_sources.yaml b/policyengine_uk/parameters/gov/welsh_government/childcare_offer/income/gross_income_sources.yaml index f1f1afff81..1e14c7c83a 100644 --- a/policyengine_uk/parameters/gov/welsh_government/childcare_offer/income/gross_income_sources.yaml +++ b/policyengine_uk/parameters/gov/welsh_government/childcare_offer/income/gross_income_sources.yaml @@ -53,8 +53,9 @@ values: # Maternity Grant, lump_sum_income) or to the household (cost of living # payments, the energy rebate, bill credits), because question 10 works out # annual income from regular pay and the bonuses a parent expects, read here - # as a test of recurring income (question 9's "all payments" points the - # other way); and loans, which are repaid (student_loans). + # as a test of recurring income (question 8's current-tax-year test and + # question 9's "all payments" point the other way); and loans, which are + # repaid (student_loans). # # Data limits, immaterial near £100,000: for a Welsh student the FRS # education_grants residual can still hold Childcare Grant and Disabled diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml index e12d79e9f4..bb515785bf 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml @@ -3,7 +3,7 @@ # A non-worker or 15-hour worker with banding income pays 20.40 * 52 = # 1,060.80 in 2026-27. One with no banding income keeps the scale's zero: # the gate only lowers a deduction until #2007 models the exemptions in -# reg 74(6)-(10) and reg 55(6)-(9) with the flat charge. +# reg 74(6), (7), (8) and (10) and reg 55(6)-(9) with the flat charge. # A 16-hour worker's wages of 9,000 alone are below 192/week; adding # IIDB or child maintenance of 12,000 gives 403.85/week, so 105.20 * 52. diff --git a/policyengine_uk/tests/test_statutory_paternity_pay_income_tax_properties.py b/policyengine_uk/tests/test_statutory_paternity_pay_income_tax_properties.py index e57c68a792..ad32b4fee5 100644 --- a/policyengine_uk/tests/test_statutory_paternity_pay_income_tax_properties.py +++ b/policyengine_uk/tests/test_statutory_paternity_pay_income_tax_properties.py @@ -291,6 +291,9 @@ def test_income_tax_never_falls_as_statutory_paternity_pay_rises(population): ) + results.family_total("CB_HITC", i, LOW) ) + # This margin relies on even-pound base incomes: PAY's + # 388.64 -> 389 pair cannot cross a personal-allowance + # taper-rounding step. assert gain <= family["high"] - family["low"] + TOLERANCE, family diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py index ebbb00573e..49c6d82786 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py @@ -136,7 +136,7 @@ class housing_benefit_applicable_income(Variable): "https://www.legislation.gov.uk/uksi/2006/214/regulation/23", "https://www.legislation.gov.uk/uksi/2006/214/regulation/29", "https://www.legislation.gov.uk/nisr/2006/405/regulation/22", - "https://www.legislation.gov.uk/nisr/2006/405/schedule/6/paragraph/64", + "https://www.legislation.gov.uk/nisr/2009/276/regulation/3/made", "https://www.legislation.gov.uk/nisr/2006/405/schedule/6/paragraph/32", "https://www.legislation.gov.uk/nisr/2006/406/regulation/21", "https://www.legislation.gov.uk/nisr/2006/406/regulation/27", diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py index 36d14136a9..2bec92df06 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py @@ -33,11 +33,12 @@ def formula(person, period, parameters): # instead of an income band. Reg 6(6) excludes income-related # recipients from remunerative work. The gate only lowers a # deduction: a non-worker with no banding income keeps the scale's - # zero, because the exemptions in reg 74(6)-(10) and reg 55(6)-(9) - # (a claimant on Attendance Allowance; a non-dependant on Pension - # Credit, or under 25 on Income Support; a full-time student) are not - # modelled, and the income-related benefits are not banding income, - # so many such non-dependants have none. + # zero, because the exemptions in reg 74(6), (7), (8) and (10), + # and reg 55(6)-(9) (a claimant on Attendance Allowance; a + # non-dependant on Pension Credit, or under 25 on Income Support; + # a full-time student) are not modelled, and the income-related + # benefits are not banding income, so many such non-dependants + # have none. # #2007 adds the exemptions and the flat charge together. deduction = where( in_remunerative_work,