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 28 commits into
mainfrom
uc-capital-sale-expenses
Open

MaxGhenis wants to merge 28 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 10% applies to gross value before debt, only where sale incurs expenses; real property always has sale costs (ADM H1604–H1608). Deduct 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 is £0.19 rather than the baseline calculation's £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.
  • Itemise corporate_wealth's share-like components for the means tests #1974's share_like_capital is the sale-expense source in all six lists; corporate_wealth remains 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's valuation. 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 disclose this data limitation; a source split remains necessary.

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 review (v10, Opus 5.5, after the GPT-6.1 Sol lanes reached capacity) 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.

One defect already on main, which this PR neither causes nor changes: cash_isa and stocks_and_shares_isa sum person balances through adds, so after the 2032 projection horizon a dataset that stores only the household totals loses both. In the 2033 probe, UC capital is £1,310.81, the savings alone, where the stored totals give £7,602.69; main gives the same £1,310.81. 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 the 4 GB / 1,500-second cap, and mutations under the 2 GB cap. It reused the adoption environment read-only, with this checkout on PYTHONPATH. It ran no microsimulation over a survey dataset and loaded none.

At 2026-10-29T08:16:41Z, gh pr checks 1969 -R PolicyEngine/policyengine-uk reported 17 passed and 2 failed for fc066bd318f2e059623ab30df56985e3605e24d0 (exit 1). All six Python shards and Aggregate impact passed. Test / YAML passed all 5,266 cases, but its policyengine-core process had not exited when the job reached its 45-minute limit, so GitHub cancelled the job. Test / Result then failed on the cancelled YAML result. Every YAML job the review checked takes 11.8 to 15.0 minutes to exit after its summary, main's merge queue included. This run's tests took 37 minutes, which left 8. Re-running the failed jobs should clear both checks.

Impact on the certified data

The aggregate-impact comment, updated at 2026-10-29T07:46:28Z, compares main 2d437ebba with 0ab53939232733dffcefc96e3477f6d3a08ebd4f. That commit merges fc066bd31 into 2d437ebba; the review checked its parents, and the workflow run reports fc066bd31 as its head. Both runs use enhanced_frs_2024_25.h5 (policyengine-uk-data 1.58.1). The job passed: no check broke, one 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 +£546.2m +£1.26bn +£1.53bn +£1.08bn +£1.09bn
Universal Credit caseload +12.5k +34.7k +51.0k +91.6k +97.6k +74.0k +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.3m −£0.7m −£0.1m −£10.3m −£11.4m −£2.5m
Household net income † +£225.7m +£325.7m +£548.0m +£1.27bn +£1.55bn +£1.13bn +£1.14bn
Government balance † −£225.7m −£325.7m −£548.4m −£1.27bn −£1.55bn −£1.13bn −£1.14bn
Absolute poverty AHC, children −0.01pp −0.19pp −0.34pp −0.27pp −0.28pp −0.09pp −0.35pp

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

Lower assessed capital raises each of the six benefits whose capital the PR values, or leaves it unchanged, 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).
  • 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 −£2.2m to +£3.0m, because attendance ranks households by net income including benefits (attends_private_school.py).
  • Relative poverty rises in a few cells, such as +0.17pp 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 bundled share-like-capital data still limits the legal reading of any aggregate result.

Coordination

axiom: TheAxiomFoundation/rulespec-uk#395 queued

  • These valuation provisions remain outside the pinned signed rulespec-uk corpus. The queued issue carries the legal text, ingestion path and hand-computed companion cases; the encoder route remains blocked until ingestion.

🤖 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@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 <noreply@anthropic.com>
£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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

UK hub: #1995 (head 3727eb8) now depends on this PR. Its r3 finding 3, the Pension Credit savings-credit exit cliff, is Pension Credit's gross property valuation, which this PR fixes. So #1969 should land first; #1995 will then merge main and add two property boundary cases. This PR's latest review (Subfleet 20261019-181511-pr1969-review-v3b, 2026-10-20 01:43Z) requested changes, and there has been no new head since.

🤖 Generated with Claude Code

MaxGhenis and others added 3 commits October 20, 2026 18:32
… penny

From the GPT-6.1 Sol review of a7d8988:

- A recorded benunit_reported_capital total holds its owner's reg. 77
  holding at full value, while the pool counts that holding at 90%. The
  report now consumes its known holding part at the counted 90%
  (household_uc_reported_financial_capital), so it can no longer eat another
  unit's savings; the owner's own assessment still takes the full holding off
  the report. The company-override conservation oracle consumes the same way,
  and a YAML regression pins the reviewer's case: £16,500 of savings and a
  £10,000 report that is all holding give [0, 16,500].
- valued_capital rounds the 90% value down to the penny in the claimant's
  favour (ADM H1665), in float64 with a guard against binary noise. A YAML
  regression puts £1,000 of savings and £16,666.67 of shares at exactly
  £16,000.00, which passes the limit (WRA 2012 s. 5(1)(a), UC reg. 18(1)).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inputs are float32, so 90% of £16,001 came to £14,400.8994 and the
penny floor gave £14,400.89 (the HB £16,001 case failed). less_sale_expenses
takes the value to the penny in float64 before the 10% deduction and
rounds down to the penny (ADM H1665). The UC property path now uses it
too, so every reg. 49(1)(a) valuation rounds the same way.

Adds a Hypothesis property checking 90% of every whole-penny float32
input up to £131,071.99 against integer pence, and a UC YAML case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sol review v4b found that rounding the input to the penny first could
round up (the ADM H1665 example gave 930.94, not 930.93), that nothing
was rounded at the last stage, and that UC rounds to the nearest penny.

- capital.round_down per programme: false for UC (UC Regs 2013 reg 6(1);
  NI reg 7(1)) and HB (HB Regs 2006 reg 80; HB (SPC) reg 61(6); NI 78(8),
  59(7)); true for IS (reg 27), JSA (reg 92), ESA (reg 3(a)) and Pension
  Credit (reg 24A), which count a fraction only to the claimant's
  advantage.
- exact_amount removes float32 storage error only where a value is the
  stored form of a whole penny; other values are kept exactly.
- Each valuation, each UC holding and property share, and each
  programme's assessed capital (the last stage) is rounded by that rule.
- A recorded financial total below its owner's holding now consumes the
  holding at its counted value, as an override does, so no part of the
  holding is left for another unit.

Tests: the H1665 example (930.93 / 930.94), whole-penny properties for
both rules, a never-wrong-way bound for any float32 input, UC half-penny,
16,000.00 after a holding, a three-way split, IS shared-capital rounding,
and reports below the holding in the reg 77 conservation grid.

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

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