Skip to content

Value means-tested capital less 10% sale expenses and secured debt (UC reg 49 and legacy equivalents) - #1969

Open
MaxGhenis wants to merge 30 commits into
mainfrom
uc-capital-sale-expenses
Open

MaxGhenis wants to merge 30 commits into
mainfrom
uc-capital-sale-expenses

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #1960.

Apply the statutory 10% sale-expense deduction and secured debt when valuing capital for Universal Credit, Housing Benefit, Income Support, income-based JSA, income-related ESA and Pension Credit. Savings and cash ISAs remain at face value. The helper floors each encumbered source at nil so negative equity in one source cannot reduce another.

Reproducer: policyengine_uk/tests/policy/baseline/gov/dwp/capital_valuation.yaml::UC values a mortgaged holiday home as in ADM H1602, policyengine_uk/tests/policy/baseline/gov/dwp/capital_valuation.yaml::UC shares valued less sale expenses bring capital within the limit, policyengine_uk/tests/test_capital_valuation_properties.py::test_valuation_matches_the_regulation_and_never_adds_capital, policyengine_uk/tests/policy/baseline/gov/dwp/capital_valuation.yaml::A Lifetime ISA value given as an input is counted as given, policyengine_uk/tests/test_capital_valuation_properties.py::test_lifetime_isa_reformed_value_is_kept_as_stored

The law

Programme Great Britain provision Northern Ireland equivalent
Universal Credit UC Regs 2013 reg 49(1) S.R. 2016/216 reg 49(1)
Housing Benefit HB Regs 2006 reg 47; HB (SPC) Regs 2006 reg 45 S.R. 2006/405 reg 44; S.R. 2006/406 reg 43
Income Support IS Regs 1987 reg 49 S.R. 1987/459 reg 49
Income-based JSA JSA Regs 1996 reg 111 S.R. 1996/198 reg 111
Income-related ESA ESA Regs 2008 reg 113 S.R. 2008/280 reg 113
Pension Credit SPC Regs 2002 reg 19 S.R. 2003/28 reg 19

The decision maker takes the 10% from gross value before debt, only where sale incurs expenses, and real property always has sale costs (ADM H1604–H1608). The decision maker then deducts only debt secured on that source (H1615–H1616). Parameters start with each programme's existing capital-source list. Each programme uses the provision in its own instrument, including the later UC and ESA instruments.

What changes

  • Each programme has capital.sale_expenses.rate: 0.1, an explicit list of sources, and its own capital.round_down rule. valued_capital applies the deduction before secured debt, with float64 arithmetic.
  • UC and HB round to the nearest penny, half up (UC reg 6(1); HB reg 80 / SPC HB reg 61(6)). IS, JSA, ESA and PC round down under their respective regulations. Each valuation and final assessment follows that rule; UC holdings and property allocations also follow its penny rule.
  • exact_amount recovers an unambiguous whole penny from float32 storage below £131,072. It preserves other stored amounts. Above that boundary, original pennies can be ambiguous; the result can differ by a penny in either direction. This PR cannot recover arbitrary precision lost before valuation.
  • Lifetime ISAs use the baseline surrender-value calculation in float64, rounded per person under each programme's rule before summing. A £0.26 balance below the charge-free age gives £0.195: UC/HB count £0.20 and the down-rounding programmes count £0.19. The whole-penny property draws every penny, including this half-penny example.
  • The valuation counts explicit LISA inputs as given. Replacement formulas retain their stored results, and neutralization remains effective. Recalculation requires the active baseline formula and the absence of a supplied input for that period and branch. Equality with the baseline float32 value cannot establish provenance: an explicit £0.195 also stores as 0.19499999284744263, but nearest-penny rounding of that supplied amount gives £0.19 where the baseline calculation gives £0.20. A regression covers this collision.
  • The float64 LISA recomputation now applies the active variable's defined_for mask through Core's entity mapping. It keeps the stored result outside the mask. Regressions cover mixed masked/counted people across all six means tests and UC eligibility at the £16,000 capital limit. Commit dbc0c587f calculates the Lifetime ISA mask before its high-precision recomputation. It runs the formula only when at least one person applies and keeps Core's stored value outside the mask. The regression makes lifetime_isa_balance raise when a reform masks everyone and checks capital for all six programmes against a run without a Lifetime ISA.
  • The capital property suite now generates cash_isa as well as savings. Its cash-only property checks their sum at face value. Direct source lookup still exposes a source omitted by the oracle as a KeyError; cash ISAs stay outside both the sale-expense and secured-debt mappings.
  • All six lists name Itemise corporate_wealth's share-like components for the means tests #1974's share_like_capital as the sale-expense source, and corporate_wealth stays its backing household input. UC company-holding deductions use HOLDINGS_SOURCE consistently, including pool and reported-capital budgets read through exact_amount. Property oracles map the renamed source to the backing input.
  • UC's existing property route shares the same sale-expense rate. Regulation 77 removes a disregarded holding at its counted value, while company capital substituted under reg 77(2) stays outside the sale-expense helper. Recorded financial totals and complete reported-capital overrides keep their existing distinct meanings.
  • Three household inputs hold the debt secured on other residential property, non-residential property and land. Retain property in UC capital alongside financial reports #2447 put the first two on main; this PR adds owned_land_secured_debt and deducts all three in every programme that counts the asset. UC counts no land, so the land input changes the five legacy means tests only. Each defaults to zero, and uprating_indices.yaml lists each once.
  • CTR schemes using UC capital inherit its valuation. National schemes testing only savings retain that scope.

Which sources take the 10%

Source Deduction Basis / limitation
Savings and cash ISAs None Account balance / withdrawal value; incidental withdrawal costs are not sale costs (ADM H1675, H1607).
Land and other residential/non-residential property 10% Real property has sale costs (H1606); supplied property values are gross.
share_like_capital 10% Direct shares have sale costs (H1665). The aggregate can also include unit trusts and stocks-and-shares ISAs, whose legal treatment differs (H1674, H1656). The parameter descriptions state this limit, and the data need a source split.

Review responses and integration

Finding Response
v6: LISA half pennies lost on the float32 round-trip Compute the baseline surrender value in float64 and round each person's value under the programme's rule. Widen the whole-penny property beyond multiples of 4p and cover UC boundaries, explicit inputs and equal-float32 provenance collisions.
v6: Incorrect claimant-favourable claim for large float32 values Correct the documentation and test commentary: ambiguous pennies from £131,072 can produce a penny either way after valuation; plain amounts from £262,144 have the same issue.
v6: Float32 cast outside errstate Put the cast inside the overflow/underflow/invalid guard.
v7: Baseline LISA recomputation bypassed defined_for Apply the active mask to the float64 result and keep Core's stored result outside it (696ae34ad). Add mixed-person and UC-limit regressions. The v8 follow-up below also preserves Core's all-masked short circuit.
v7: Property generator omitted main's new cash_isa source Generate cash ISAs, count them at face value, and check savings plus cash ISAs in the cash-only property (b168dc509). Keep direct lookup and existing tolerances.
v8: all-masked LISA recomputation evaluated unused dependencies (P2) dbc0c587f evaluates the mask first and runs the formula only when someone applies. The new raising-dependency regression checks all six programmes.
v9: no finding (APPROVE at dbc0c587f) No change.
CI at dbc0c587f: main's new test_isa_balances.py expected a stocks-and-shares ISA at full value in UC capital fc066bd31 expects UC's sale-expense deduction on it: the ISA reaches capital through share_like_capital, which UC values less 10% until the data separate ISAs from shares. #2131 then values itemised ISAs without the 10%.
v10: no P1 or P2 on this PR; one optional nit in the edited test Optional: read the rate at the instant the formula uses. v10 also records one defect already on main (below) for a separate issue.

Validation

An independent Opus 5.5 review of the merge and a separate run of the changed tests covered 30d48c01b, one file at a time. Every test that finished passed.

Check at 30d48c01b Result
test_capital_valuation_properties.py 18 passed
test_capital_share_components.py 14 passed
test_uc_property_capital.py and test_uc_property_capital_mock.py 91 passed
test_uc_company_owner_properties.py and test_uc_company_override_properties.py 18 passed
test_lifetime_isa_capital.py and lifetime_isa_capital.yaml 3 passed, 24 cases passed
test_uc_tax_free_childcare_choice.py and the two Tax-Free Childcare YAML files 20 passed, 12 cases passed
test_input_records.py 11 passed
test_projection_horizon.py 14 passed
test_fiscal_partition.py and test_aggregate_impact_partition.py 43 and 11 passed
HB accommodation disregard YAML file 50 cases passed

Three files hit local time caps under host load before finishing: test_household_subset.py, test_housing_benefit_accommodation_dates.py and the pair test_isa_accounting.py and test_wealth_components.py. None of those that ran failed, and CI ran them all at 3a3241cf6 and passed.

An earlier independent review (v10, Opus 5.5) ran these checks against fc066bd318f2e059623ab30df56985e3605e24d0, one file at a time. Every unmutated test passed.

Check Result
test_isa_balances.py 2 passed
test_wealth_components.py 3 passed
test_capital_valuation_properties.py 18 passed
test_lifetime_isa_capital.py 3 passed
test_household_subset.py (main's #2732) 9 passed
17 YAML files that git grep -l finds naming cash_isa, stocks_and_shares_isa, share_like_capital or corporate_wealth 210 cases passed
lifetime_isa_capital.yaml 24 cases passed
Six-programme ISA probes on small in-memory datasets 165 of 176 checks match; the other 11 fail identically on main (below)
Head against main on the same 176 checks head never exceeds main; main − head equals 10% of share_like_capital to within 2.8p of penny rounding

The probes cover UC, HB, PC, IS, income-based JSA and income-related ESA in 2025, 2027, 2032 and 2033. They use four kinds of data: stored household ISA totals, person balances, person balances with a Lifetime ISA, and corporate_wealth that already holds the ISA. Each programme counts savings and cash ISAs in full, takes 10% off share_like_capital, and counts a Lifetime ISA once, through valued_person_capital. No source counts twice.

The review ran four scratch mutations on the edited test, made each in place, and restored it byte for byte, checking SHA-256 before and after:

  • Dropping the stocks-and-shares ISA from share_like_capital failed it at £4,288.96 (£4,000 × growth).
  • Taking the 10% twice failed it at £6,025.99 (£5,620 × growth).
  • Removing share_like_capital from UC's sale-expense sources failed it at £6,433.44 (£6,000 × growth).
  • A nil sale-expense rate passed it, because the test reads the rate, but capital_valuation.yaml failed 8 cases.

cash_isa and stocks_and_shares_isa sum person balances through adds, so after the 2032 projection horizon main loses both on a dataset that stores only the household totals, and this PR leaves that as it stands. In the 2033 probe, main and this PR both count £1,310.81 of UC capital, the savings alone, where the stored totals give £7,602.69. policyengine-uk-data's Enhanced FRS writes neither total, but Microcosm's UK exports store both, so their runs for 2033 and later lose them. #2740 tracks it: #2712 replaced the totals' uprating attribute with adds.

The review ran model commands one at a time under a 4 GB and 1,500-second cap, ran mutations under 2 GB, and loaded no survey data.

CI at 3a3241cf6 passed: both required checks, all six Python shards and the YAML job.

Impact on the certified data

The aggregate-impact comment, updated at 2026-10-31T14:10:13Z, compares main 2da212abc with 2eb144a64, which merges 3a3241cf6 into that main. The workflow run reports 3a3241cf6 as its head, and both sides use enhanced_frs_2024_25.h5 (policyengine-uk-data 1.58.1). The job passed: no check broke, sign:vat fails on both sides, and none got fixed.

Change 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Universal Credit +£222.8m +£324.3m +£543.0m +£1.26bn +£1.53bn +£1.08bn +£1.09bn
Universal Credit caseload +12.5k +35.0k +51.0k +91.4k +96.8k +73.8k +69.3k
Housing Benefit +£0.9m +£0.3m +£0.4m +£2.8m +£19.9m +£42.6m +£16.1m
Pension Credit +£1.0m +£1.0m +£1.1m +£1.0m +£0.9m +£0.8m +£0.7m
ESA (income-related) +£0.4m +£0.4m +£0.4m +£2.6m +£0.4m +£0.4m +£0.5m
JSA (income-based) +£13,640 0 0 0 0 0 0
Income Support +£43,929 0 0 0 0 0 0
Council Tax Reduction +£0.8m +£0.8m +£0.3m +£0.3m +£1.0m −£55,441 −£51,533
Scottish Child Payment +£0.3m 0 0 0 +£6.0m +£11.0m +£24.1m
Tax-Free Childcare −£0.6m −£1.4m −£0.7m 0 −£10.2m −£11.2m −£1.7m
Targeted childcare entitlement 0 0 0 0 0 0 +£7.0m
Household net income † +£225.7m +£325.6m +£544.7m +£1.27bn +£1.55bn +£1.13bn +£1.14bn
Government balance † −£225.7m −£325.6m −£545.2m −£1.27bn −£1.55bn −£1.13bn −£1.14bn
Absolute poverty AHC, children −0.01pp −0.17pp −0.21pp −0.27pp −0.28pp −0.09pp −0.35pp

† The report rebuilds these totals without the suppressed Disabled Students' Allowance figure.

The model now assesses less capital, so it pays the same or more of each of the six benefits in every year. The report shows 0.0% of people losing over £1 in every year, and every base-run decile gains or holds. The moves the other way follow from higher awards:

  • Council Tax Reduction counts the UC award as income in the default-scheme formula (council_tax_reduction/_legacy.py:50-53), so a larger award lowers it.
  • Tax-Free Childcare excludes any family with universal_credit_pre_benefit_cap above nil (tax_free_childcare_program_eligible.py; disqualifying_benefits.yaml), unless the family gives up a smaller award through forgoes_universal_credit_for_tax_free_childcare.
  • The TV licence fee falls by £0.1m to £0.3m a year: a household with someone aged 75 or over on Pension Credit gets a free licence (tv_licence_free_licence_share.py).
  • VAT on private school fees moves both ways, from −£1.6m to +£2.0m, because attendance ranks households by net income including benefits (attends_private_school.py).
  • Relative poverty rises in a few cells, such as +0.14pp for children before housing costs in 2030-31, because each run sets its line at 60% of its own median. Absolute poverty falls or holds in every cell.
  • The report's own-decile means move both ways as households change deciles.

The targeted childcare entitlement moves only in 2031-32: a UC award above nil gives a family one route to it (meets_universal_credit_criteria_for_targeted_childcare_entitlement.py).

share_like_capital can also hold unit trusts and stocks-and-shares ISAs, so these aggregates take 10% off some sources whose legal treatment differs.

Coordination

axiom: TheAxiomFoundation/rulespec-uk#395 queued

  • rulespec-uk's pinned signed corpus holds none of these valuation provisions yet. The queued issue carries the legal text, ingestion path and hand-computed companion cases, and the encoder can't run on them until someone ingests them.

🤖 Generated with Claude Code

UC Regs 2013 reg. 49(1) and its legacy equivalents (HB Regs 2006 reg. 47,
HB (SPC) Regs 2006 reg. 45, IS Regs 1987 reg. 49, JSA Regs 1996 reg. 111,
ESA Regs 2008 reg. 113, SPC Regs 2002 reg. 19) calculate capital at market
or surrender value less 10% where a sale would incur expenses and less any
encumbrance secured on it. The model summed every capital source at full
value.

Each programme gets a capital.sale_expenses node (rate 0.1 and the sources
whose sale incurs expenses: land, property and corporate_wealth, not cash
savings). A shared helper applies the 10% before the encumbrance and never
values an encumbered asset below nil. New household inputs carry debt
secured on other residential property, non-residential property and land;
the Enhanced FRS has none, so they default to zero.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
MaxGhenis and others added 2 commits September 30, 2026 22:07
- Each programme's sale-expense parameters now cite its GB and NI
  regulations, the guidance paragraph that orders the 10% before the
  encumbrance, and per-asset guidance with exact PDF page anchors (DMG ch 52
  for ESA, HBGM Annex F for HB savings, H1656/H1665/H1673-H1675 for UC).
- The source-list descriptions and changelog say plainly that
  corporate_wealth also holds unit trusts, ISAs, employee share options and,
  in datasets built before the pension split, non-DB pension wealth.
- The helper and secured-debt docs say debts net within a source category.
- The property-test oracle fixes the sale-expense assets and the 10% itself
  instead of reading the parameters, checks pension-age HB, and runs on
  larger populations. New hand-computed cases cover HB second homes and
  premises, per-source flooring between two encumbered sources, pension-age
  HB, and a monthly secured debt.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
HBGM W1.430 states only the general 10% rule; the rule that real property
always has costs of sale comes from R(IS) 21/93, as DMG 29622 applies it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Coordination note from #1950 (UC capital income). If this PR lands after #1950, please update the "reg 49 is #1960" notes #1950 adds:

  • in uc_income_from_capital.yaml, the header and both let-property cases (GB and NI);
  • in uc_unearned_income.yaml, the savings-0 let-property case.

Those cases enter £10,000 of property as capital. At 90% that is 12 tariff steps, not 16, so either update the expected values or restate the input as a property value of £11,111.11. If #1950 lands second, I'll do it on rebase. #1950's merge is waiting on a decision from Max (over the £1bn/yr line).

🤖 Generated with Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

#1950 merged (770bc06), so this PR is second. Please update the "reg 49 is #1960" notes:

  • in uc_income_from_capital.yaml, the header and the GB and NI let-property cases;
  • in uc_unearned_income.yaml, the savings-0 let-property case.

At 90% valuation, £10,000 of property is 12 tariff steps, not 16, so either update the expected values or enter £11,111.11.

🤖 Generated with Claude Code

Max Ghenis and others added 2 commits October 3, 2026 17:20
Resolve the capital variables onto main's claimant/partner allocation and
person-level sources, valuing household sources through valued_capital.
Update the let-property tests #1950 added: £10,000 of property is now £9,000
of capital under reg. 49(1), so 12 tariff steps, not 16.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two tests main added after this branch's earlier base assumed full-value
capital:

- The CTR savings-credit-only case holds savings 10,000 and a 7,000 second
  property: capital is 10,000 + 90% of 7,000 = 16,300 (SPC Regs 2002 reg.
  19(a)), still over the 16,000 limit, so CTR stays nil. Deemed income is 13
  steps x 52 = 676, Pension Credit income 13,314 + 676 = 13,990, and savings
  credit 60% of (12,376 - 208.07 x 52) less 40% of (13,990 - 12,376) =
  288.22.
- The reported-capital property test's proxy oracle now values land,
  property and shares at 90% and savings at face value.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
MaxGhenis added a commit that referenced this pull request Oct 4, 2026
The six capital sources lists (UC, HB, IS, JSA, ESA, PC) name
directly_held_shares, unit_and_investment_trusts and stocks_and_shares_isa
in place of corporate_wealth, plus a new formula variable,
unitemised_corporate_wealth = max(0, corporate_wealth - components), for
whatever a dataset does not itemise. Datasets without the components keep
counting corporate_wealth in full; datasets that build corporate_wealth as
the components' exact sum count the same total, now itemised, so each
component can carry its own valuation rule (the sale-expense deduction stacked
on #1969 exempts unit trusts and ISAs). No dataset's capital changes: a
differential property test checks that every programme's assessable capital
equals the same holdings entered as one corporate_wealth.

Review nits: the growth-factor sentence now matches uprating_indices.yaml
exactly; the variable docs cite the DMG for the legacy benefits, the expert
valuation of unquoted shares and the quoted-share rule for investment trusts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Stacked follow-up: #2131 lists only directly_held_shares and unitemised_corporate_wealth as share-like sources with sale expenses, so unit and investment trusts (ADM H1673-H1674) and stocks and shares ISAs (ADM H1656) count at full value. It builds on this branch's head 22274ca and includes #1974, which itemises corporate_wealth in the six capital lists. Datasets that don't itemise keep this PR's flat treatment exactly.

🤖 Generated with Claude Code

@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. Reviewed at head 22274cad.

The law is right in every programme. I re-fetched each provision from legislation.gov.uk:

  • UC Regs 2013 reg 49(1): "Capital is to be calculated at its current market value or surrender value less— (a) where there would be expenses attributable to sale, 10%; and (b) the amount of any encumbrances secured on it."

  • The same rule appears, in older wording ("10 per cent", "encumbrance/incumbrance"), in:

    • SPC Regs 2002 reg 19;
    • HB Regs 2006 reg 47 and HB (SPC) Regs 2006 reg 45;
    • IS Regs 1987 reg 49;
    • JSA Regs 1996 reg 111;
    • ESA Regs 2008 reg 113.

    The regulation numbers are right (JSA 111 and ESA 113).

  • CTR Prescribed Requirements SI 2012/2885 Sch 1 para 32 has the same 10% rule for pension-age CTR.

  • SPC Sch V has no disregard that touches share, unit trust or ISA valuation. Para 9 is business assets only.

The helper follows the guidance. valued_capital takes the 10% off gross value before the debt (ADM H1608) and floors each encumbered source on its own (H1615-H1616).

Probes all match hand arithmetic:

  • savings £5,000 + corporate_wealth £12,000 gives £15,800 for UC, HB and IS, and £15,800 for PC at 70 (main: £17,000);
  • a £100,000 second home with £95,000 debt plus £3,000 savings gives £3,000;
  • a second home of £20,000 with £25,000 debt plus premises of £10,000 gives £9,000.

Tests:

  • At the head: properties, PC reported capital and stock periods, 9 passed. DWP, family and CTR YAML, 967 passed.
  • Merged into current main b5b09231 (the merge is clean; the branch is 280 commits behind): the same YAML set, 1,274 passed. But two of the new property tests fail; see finding 1. The green CI is from the stale merge ref.

Findings:

  1. Blocking (rebase). On main + this branch, test_valuation_matches_the_regulation_and_never_adds_capital and test_cash_only_households_have_no_deduction fail (2 failed, 8 passed). The valuation isn't the problem; the test's oracle is out of date.

    • Main now sets HB capital to nil for anyone on UC, IS, income-based JSA or income-related ESA (HB Regs Sch 6 para 5, housing_benefit_on_passporting_benefit). It also switches the pension-age rules on housing_benefit_pension_age_regulations_apply rather than age 67.
    • So a working-age single person with £2,441.62 of cash now has HB capital of nil (on main too), while applies() at tests/test_capital_valuation_properties.py:127 still expects their savings.
    • I reproduced the cash-only failure directly. The other failing test uses the same applies() predicate, so it is most likely the same cause.
    • Fix: rebase and key the pension-age branch on housing_benefit_pension_age_regulations_apply. Then either skip units where housing_benefit_on_passporting_benefit is true, or give the generated units enough earnings to stay off UC. Skipping alone would hollow out the working-age HB branch, because the strategy's no-income singles are nearly all on UC.
  2. Blocking (merge order with #1974). Git merges this branch and #1974 cleanly, but together they undo this PR:

    • #1974 takes corporate_wealth out of every capital.sources list, while this PR's sale_expenses.sources still names it. So the 10% on shares silently stops.
    • On a 1969 + 1974 tree, the probe above goes back to £17,000. Seven YAML cases fail: 3 in capital_valuation.yaml, and 2 each in the IS and ESA capital files.
    • #2131 fixes this. So either merge #1974 first and then this PR and #2131 together, or merge this PR and then #2131 retargeted to main (it carries #1974).
    • A cheap guard in tests/test_capital_valuation_properties.py: assert set(sale_expenses.sources) <= set(capital.sources) for each programme. The existing check at line 160 intersects both sides with counted, so it can't see a stale name.
  3. Should fix (body, disclosure). In "A few records carry each figure", the UC, PC and HB bullets, and the poverty bullet, attribute most of each year's change to one or a handful of records, with percentages and counts. Read against the impact table, that is effectively a record-level weighted amount. Keep the caveat ("driven by a handful of records; not an estimate") and drop the percentages, record counts and the median gain.

  4. Nit. parameters/gov/dwp/income_support/means_test/capital/sale_expenses/rate.yaml:9. This starts on 2010-01-01, to match sources. IS reg 49 has applied the 10% since 11.4.1988. Add a sentence saying the date follows the sources list, as the HB and PC files already explain their own dates.

  5. Nit. utils/capital_valuation.py:18-19. Netting debt across assets within a category is documented. H1616 actually allows it where one debt is secured on several assets. You could cite that, so the approximation reads as partly lawful rather than purely a data limitation.

  6. Coordination with #1965. Merged with #1965 there is no textual conflict, but the holding comes off at 100% while it is counted at 90%: savings £5,000 + a £10,000 holding gives £4,000 instead of £5,000. I've put the fix on #1965, since it rebases second.

Max Ghenis and others added 4 commits October 11, 2026 20:55
Keep main's recorded benefit-unit capital (benunit_reported_capital), which
replaces the valued household sources as recorded, and register the
secured-debt inputs beside main's uprating entries.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
£20,000 of shares is £18,000 of Pension Credit capital (SPC Regs 2002
reg. 19(a)), still over the CTR limit: deemed income 16 x 52 = 832,
Pension Credit income 14,146, savings credit 933.816 - 0.4 x 1,770 =
225.82.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's Housing Benefit passport (#1927 and the Universal Credit limb) gives a
family on a passporting benefit nil HB capital, so the property's
independent reg 47 valuation applies only to non-passported families.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Merge-order note from #1974's owner. #1974 (head 6600477, main a8b2848 merged) is being finished for the Budget cut. If it lands first, this PR's next merge of main needs the following, or the 10% sale-expense deduction silently stops applying to shares.

#1974 renames the share-like source in all six capital sources lists from corporate_wealth to share_like_capital. That variable is corporate_wealth as supplied, or its itemised components where they exceed it beyond float32 rounding. On every released dataset it equals corporate_wealth exactly.

Rename in this PR. valued_capital applies the 10% only to a source listed in sale_expenses.sources, so these must change:

  • All six sale_expenses/sources.yaml: corporate_wealth becomes share_like_capital.
  • Three hardcoded counted_share(p.sale_expenses, "corporate_wealth") calls, in uc_assessable_capital.py, household_uc_pool_company_holdings.py and household_uc_reported_financial_capital.py. Import HOLDINGS_SOURCE from household_uc_company_holding_pool_scale and pass it instead.
  • The exact_amount(person.household("corporate_wealth", period)) budgets, in the pool and reported-capital files. Itemise corporate_wealth's share-like components for the means tests #1974 already points main's versions of these lines at HOLDINGS_SOURCE (= "share_like_capital"), so take Itemise corporate_wealth's share-like components for the means tests #1974's side and keep exact_amount around it.

Tests.

If this PR lands first instead, #1974 does the same reconciliation when it merges main. #2131 is then rebuilt on whichever head results.

🤖 Generated with Claude Code

MaxGhenis added a commit that referenced this pull request Oct 22, 2026
Reg 77 holdings are shares with sale expenses, so HOLDINGS_SOURCE is
share_like_capital_with_sale_expenses: the holdings budget and the 90%
counted share both read it, where #1969 read corporate_wealth. The test file
keeps the round-3 fixes (projected years, the 16,000 overshoot, the
power-of-two precondition, the grouping example, the exempt list's start).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sol review v5 at b3f6f60:
- exact_amount raised an underflow warning on zero inputs, failing
  test_warning_filters. Errors are now ignored inside np.errstate, and
  twice the gap is compared with the spacing.
- IS, JSA and ESA read household values through benunit.max, which is
  float64, so exact_amount used float64 spacing and never removed float32
  storage error: 16,000.01 of savings floored to 16,000.00, at the limit.
  A value equal to its float32 is now judged at float32 spacing.
- From 131,072 float32 cannot hold every penny, so no penny is identified
  and the stored value is kept; rounding down then never goes against
  the claimant.
- The Lifetime ISA's 75% and UC company capital are worked in float64
  from the stored amounts; company capital is rounded under reg. 6(1).
- test_uc_capital_allocation expected exact conservation; each share is
  now its own nearest-penny amount (1.00 three ways is 0.33 each).
- Docstrings: the guard's intended tolerance; no report leaves a holding
  in the pool.

Tests: a model-level property that every programme counts whole-penny
savings, shares and Lifetime ISA balances to the penny; widened-input
and above-131,072 helper checks; YAML for 16,000.01 of savings under IS,
JSA and ESA and a 1,000.04 Lifetime ISA.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
MaxGhenis and others added 2 commits October 24, 2026 03:31
#1974 renamed the share-like capital source from corporate_wealth to
share_like_capital. The six sale-expense lists, the three holding
valuations (counted_share) and the two holding budgets now name it through
HOLDINGS_SOURCE, so shares keep the reg. 49(1)(a) deduction. The sale-list
guard now also requires share_like_capital in each list, the differential's
original lists rename the sale-expense lists too, and a components-only
household counts 1,000 + 90% of 12,000 = 11,800 under UC.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…s rule

The engine stores lifetime_isa_countable_capital as float32, so 75% of a
whole-penny balance that ends in half a penny (75% of 0.26 is 0.195) was
read back just below it and rounded the wrong way under UC and HB: 0.26 of
LISA and 15,999.81 of savings gave UC capital of 16,000.00, eligible,
instead of 16,000.01. valued_person_capital now works a LISA's surrender
value out in float64 from the balance and rounds each person's to the penny
under the programme's rule, like any other valuation, in all six means
tests. The whole-penny property draws any whole-penny balance, with the
half-penny cases as examples, and two YAML cases pin the limit case and
per-partner rounding.

exact_amount's docstring no longer claims that rounding down never goes
against the claimant above 131,072: a figure built from a stored value
there can be a penny either way. The float32 cast moves inside errstate.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

UK hub: adopting this PR to finish it. Its owner session (CLI 8279ead2) stopped on 25 Oct at 00:02Z, after a weekly-limit stop, and can't be reached. Its last plan was to push, refresh the impact from CI and hand the PR to the hub.

What the owner left. None of it was pushed. On 27 Oct at 19:50Z the hub took a snapshot of the work; the owner's worktree is untouched.

  • acafa19f9 merges main as of 24 Oct, Itemise corporate_wealth's share-like components for the means tests #1974's share_like_capital rename included.
  • 812354085 values each Lifetime ISA to the penny in float64 under each programme's rule. It closes the three findings of the last independent review (v6, REQUEST_CHANGES at 9b9ef66).
  • Uncommitted edits to three files keep a LISA value given as an input as given.

Next. A Subfleet build commits those edits and merges current main. It also ports #2688's oracle correction (1193614), which this PR needs once main's couple-role change lands. It then reruns the capital, company and property suites and pushes fast-forward to this branch. After that, an independent review of the delta from 9b9ef66 runs, and the PR lands on the usual gates. #2131, #2688 and #1995 follow it.

🤖 Generated with Claude Code

MaxGhenis and others added 3 commits October 28, 2026 14:52
Reconstruct the baseline surrender value in float64 only when the active
formula is the baseline function and no input was supplied for this period
and visible branch. Numeric equality with a float32 formula result cannot
establish provenance: it can discard a supplied whole penny.

Read the active variable from the tax-benefit system, since an existing
holder can retain a definition from before a reform. Reuse supplied-input
tracking and cover explicit inputs, float32 collisions, replacement and
parameter reforms, late inputs, periods and branches. Reuse the existing
independent-clone test fixture for the capital property suite.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Retain main's claimant and partner role corrections alongside the
programme-specific capital valuations and penny rounding.

Resolve the supplied-input registry hunk by retaining both the Lifetime ISA
entry and main's monthly domestic energy consumption entry.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Port of #2688's 1193614. Model stored-input precision, the raw holdings
budget scale, separately rounded share and holding valuations, each owner's
company capital and each benefit unit's final capital. Sum the rounded
benefit units for the household expectation under UC regulation 6(1).

Pin the original CI witness and the holdings-budget precision boundary,
while preserving main's explicit claimant and partner roles. Production
calculations and comparison tolerances are unchanged by this port.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

UK hub: pushed d5c2a57, a fast-forward from 9b9ef66. These are the owner's two unpushed commits plus three from the hub's adoption build:

  • acafa19f9 (owner): merges main as of 24 Oct, Itemise corporate_wealth's share-like components for the means tests #1974's share_like_capital rename included.
  • 812354085 (owner): values each Lifetime ISA in float64 under each programme's rounding rule, closing the v6 review's three findings.
  • a677cb348: keeps a supplied or reformed LISA value as given.
    • The owner's uncommitted edit told a supplied value from a computed one by float32 equality. That can't work: a £87,381.34 balance and a supplied £65,536.01 both store as 65,536.0078125.
    • This commit recomputes in float64 only when the baseline formula is active and no input was supplied for the period. It uses the existing supplied_input tracking.
  • 646743859: merges main at 41fa5dd. The one conflict was a registry entry in utils/supplied_inputs.py, and both sides are kept.
  • d5c2a57e8: ports Add overseas and other real estate as property components, and count UK land once in household land #2688's company-capital conservation oracle fix (1193614), which this PR needs once main's couple-role change is in. That one test file is byte-identical to the source commit, and no tolerance or production code changed.

Validation so far:

  • Locally: the capital valuation YAML passed 26 of 26 cases and the focused property selection 8 of 8. The Itemise corporate_wealth's share-like components for the means tests #1974 rename audit is clean.
  • On a loaded host, the company-owner property file passed its 45-minute local cap with 4 of 7 nodes passed and no failure. So the full suites run here in CI.
  • An independent review (v7) of the delta since 9b9ef66 is running. It covers the provenance logic and three mutation checks, and decides whether this lands.

🤖 Generated with Claude Code

MaxGhenis and others added 3 commits October 28, 2026 17:11
Main's ISA fix (feb50e4, merged here in 6467438) added cash_isa to
every means test's capital sources. The property file's households had
no cash_isa input, so the differential and the fixed Housing Benefit
units raised KeyError: 'cash_isa' when they read each source.

Households now draw a cash ISA balance. The cash-only property holds
savings and a cash ISA, both deposit balances (ADM H1675), and checks
each programme counts the two in full, so a source list that drops
cash_isa fails it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The float64 recompute called the baseline LISA formula directly, so a
reform that only adds a defined_for mask (keeping the baseline formula)
still counted the ISA where Core stores the default. A masked 0.26 ISA
then added 0.20 to Universal Credit capital, and with 15,999.81 of
savings made the claimant ineligible (v7 review, P2-1).

Apply the mask the way Core does: where the defined_for variable,
mapped to the person, is above zero, keep the float64 value; elsewhere
keep the stored value, which is Core's default. New tests cover a mixed
masked/counted pair across the six means tests and the UC eligibility
case at the limit; both fail on the previous helper.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Core computes a variable's defined_for mask first and, when it applies
to nobody, stores the default without running the formula. The exact
float64 valuation ran the formula before masking, so under a reform
that masks everyone it calculated dependencies Core never needs, and
could fail where Core returns zero. It now works out the mask first and
skips the formula when nobody is applicable. A regression gives the
balance a formula that raises and checks every programme's capital.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Pushed dbc0c587f, which fixes the one P2 from the v8 review (GPT-6.1 Sol, REQUEST_CHANGES at 696ae34ad).

  • What was wrong: Core works out a variable's defined_for mask first. When the mask applies to nobody, Core stores the default and runs no formula. The exact float64 Lifetime ISA valuation ran the formula before masking. So under a reform that masks everyone, it calculated dependencies Core never needs, and it could raise where Core returns zero.
  • The fix: the valuation now works out the mask first and skips the formula when nobody is applicable. Outside the mask it still keeps the stored value.
  • The regression: test_a_lifetime_isa_masked_for_everyone_runs_no_formula gives the balance a formula that raises, then checks every programme's capital against a run with no LISA. With the helper at 696ae34ad the test fails ("A masked Lifetime ISA balance was calculated"). With the fix it passes.

Local runs, each under a 4 GB RSS cap at -n 0:

Check Result
test_capital_valuation_properties.py + test_lifetime_isa_capital.py 21 passed
capital_valuation.yaml 26 passed
lifetime_isa_capital.yaml 24 passed
ruff format, ruff check clean

Next: a v9 delta review (GPT-6.1 Sol) reruns v8's nine mask probes and its provenance probe at this head, runs three mutations, and drafts the final body.

Merging waits: on 28 October Vahid asked for no automatic agent merging, and the hub holds every merge until that is resolved.

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits October 29, 2026 02:41
Brings in main through 2d437eb, including #2710's per-person ISA
balances (3d5e7cc). The merge has no conflicts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…pital

Main's test_isa_balances expected Universal Credit capital to count a
stored stocks-and-shares ISA at its full projected value. This branch
values share_like_capital, which carries that ISA, less UC's 10%
sale-expense rate until the data separate ISAs from shares (see the
sale_expenses.sources description), so the merged test failed with
£6,218.99 against £6,433.44: £5,800 rather than £6,000 times growth.
The test now reads the rate from the parameters.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

UK hub: merged main and fixed one test it brought in. Head fc066bd31.

CI at dbc0c587f failed one test that came from main, not from this branch: test_isa_balances.py::test_stored_household_totals_without_person_balances_project_with_savings, from #2710 (3d5e7cc). It expected Universal Credit capital of £6,433.44 and got £6,218.99.

Why: the test counts £1,000 savings, a £3,000 cash ISA and a £2,000 stocks-and-shares ISA, all projected by the same growth (1.07224 in 2027). This PR values share_like_capital, which carries the stocks-and-shares ISA, less UC's 10% sale-expense rate, as sale_expenses/sources.yaml says, until the data separate ISAs from shares. So capital is (1,000 + 3,000 + 0.9 × 2,000) × 1.07224 = £6,218.99, not 6,000 × 1.07224. #2131 then values itemised ISAs without the 10%.

Commits:

  • 5efb62cef merges main through 2d437eb, with no conflicts;
  • fc066bd31 makes the test expect the deduction, reading the rate from the parameters.

Local runs on the merged tree:

  • The test failed before the edit with the same £6,218.99 that CI got.
  • After the edit:
    • test_isa_balances.py: 2 passed;
    • test_wealth_components.py: 3 passed;
    • test_capital_valuation_properties.py: 18 passed;
    • test_lifetime_isa_capital.py: 3 passed.
  • YAML: capital_valuation 26, cash_isa_capital 8, lifetime_isa_capital 24 and isa_balances 2, all passed.
  • Ruff format and check passed.

A delta review is running. The merge into main is still on hold under the team's no-automatic-merge request.

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits October 31, 2026 05:11
UC lists no land among its capital sources, so the land-debt input
changes the five legacy means tests only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
MaxGhenis added a commit that referenced this pull request Oct 31, 2026
#2131 last merged #1969 at b3f6f60. The merge brings in #1969's float64,
penny-rounded valuation, Lifetime ISA surrender values, cash ISAs in every
capital list, the land secured-debt input and main at 2da212a.

Conflicts and how they were resolved:

- Six sale-expense lists: #1969 renamed corporate_wealth to
  share_like_capital; this branch names share_like_capital_with_sale_expenses.
  Kept this branch's lists (land, both property sources and the part with
  sale expenses) and descriptions, and restored #1969's note that an
  unitemised corporate_wealth still folds in unit trusts, ISAs and, on older
  datasets, pension wealth. Savings and cash ISAs stay out of every list.
- UC capital/sources.yaml: savings, cash_isa (from #1969) and both
  share-like parts.
- household_uc_pool_company_holdings, household_uc_reported_financial_capital
  and uc_assessable_capital: #1969's code (exact_amount, valued_person_capital,
  penny rounding, the recorded-total consumption rule) with this branch's
  wording: a reg. 77 holding sits in, and is budgeted against, the part with
  sale expenses (HOLDINGS_SOURCE).
- test_capital_share_components.py: this branch's version, which already
  carries #1969's 11,800 and 27,000 expectations and replaces each #1974 test
  #1969 adapted with its split equivalent.
- test_capital_valuation_properties.py: #1969's oracle (asset() lookup, cash
  ISAs generated, per-person LISA rounding) with the split mapped into it:
  the part with sale expenses reads corporate_wealth, the part without is nil.
- growthfactors.md: #1969's text and savings-income section, with the
  per-capita GDP row as the union of both sides less other_investment_income
  and savings_interest_income, which main moved to per_capita.savings_income.

Long description lines in five legacy capital lists are wrapped, and Pension
Credit's list description names the two parts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
MaxGhenis added a commit that referenced this pull request Oct 31, 2026
#1969's fc066bd made main's test_isa_balances.py expect UC's 10% on a
stored stocks-and-shares ISA total, because the ISA then reached capital
through share_like_capital, which #1969 values less 10%. With the split, an
itemised ISA sits in share_like_capital_without_sale_expenses and counts at
its withdrawal value (ADM H1656), so UC capital is (1,000 + 3,000 + 2,000) x
growth: 6,000 x 1.07224 = 6,433.44 in 2027.

A new parametrised test pins the person-balance route in all six means
tests: a £1,000 saver with a £3,000 cash ISA and a £2,000 stocks-and-shares
ISA has £6,000 of capital, the ISA wholly in the part without sale expenses.

The changelog fragment loses its leading list marker, which towncrier would
render as a double bullet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

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.

UC capital ignores reg 49 valuation (10% sale costs and secured debts)

2 participants