Repository navigation
Conversation
corporate_wealth bundles three assets the means tests value differently: quoted shares less 10% for the expenses of sale (ADM H1665), unit trusts at the manager's withdrawal price with no deduction (H1673-H1674), and stocks and shares ISAs at their withdrawal value (H1656). Add household inputs for the two parts not yet modelled, directly_held_shares (UK shares and employee shares and options) and unit_and_investment_trusts, and uprate them, cash_isa and stocks_and_shares_isa with per-capita GDP like corporate_wealth, so a dataset's identity corporate_wealth == sum of components survives projection. The inputs have no consumers yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # docs/book/assumptions/growthfactors.md
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>
…le-expense-share-split
…eduction corporate_wealth is now itemised in the capital sources lists (#1974), so the sale-expense lists name directly_held_shares, which DWP values less 10% for costs of sale (ADM H1665; DMG 29671/52671/84763; HBGM W1.513), and unitemised_corporate_wealth, which keeps the previous flat treatment on datasets that do not itemise. Unit trusts have no costs of sale even when sold through a broker (ADM H1673-H1674; DMG 29680-29681, 52680-52681, 84771-84772; HBGM W1.530-W1.531), and an ISA is valued at what the holder would get by withdrawing it, with no paragraph applying the 10% (ADM H1656-H1658; DMG 29662-29664, 52662, 84748-84750). The survey bucket of unit and investment trusts is treated as having no costs of sale; investment trusts were 8.2% and 6.9% of the pooled funds in stocks and shares ISAs at 5 April 2022 and 2025 (HMRC ISA Table 9.6). The property tests keep their own legal asset set and assert each programme's list against it, and gain the split invariants: the residual, backward compatibility with every component nil, conservation at a zero rate, split effect = rate x (trusts + ISA), and no double counting. #1974's itemising-invariance test no longer holds by design and is replaced by them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Thanks Max. Reviewed at head b1a66004.
The "+10.5k lines" is the main merge carried in through #1974. The real diff over #1969 + #1974 (merged at their heads) is 11 files, +710/−125:
- the six
sale_expenses.sourceslists; - the new
capital_valuation_share_split.yaml; - the property tests;
test_stock_capital_periods.py;- a changelog fragment.
That's what I reviewed.
Guidance:
- ADM H1674 is explicit: unit trusts have "no costs of sale. This applies even if persons use an agent, such as a stockbroker."
- H1665 applies the 10% to quoted shares.
- H1656 values an ISA at "what people would get if they withdrew their investment".
The lists now apply the 10% to directly_held_shares and the unitemised residual only, which is right for shares and unit trusts. SPC Sch V and UC Sch 10 have no disregard that changes this.
Probes:
- savings £5,000, shares £4,000, trusts £3,000, ISA £5,000 (
corporate_wealth£12,000) gives £16,600 for UC, HB and IS. The same holdings with only the components entered also give £16,600. - An unsplit
corporate_wealthof £12,000 still gives #1969's £15,800. - Your monthly case (£12,300) matches.
Tests:
- At the head: the capital-valuation, share-component, wealth-component, stock-period and PC reported-capital property tests, 26 passed. DWP, family and household YAML, 1,064 passed.
- Merged into current main (the merge is clean): YAML, 1,389 passed. Properties: 19 passed and 2 failed. The two failures are the same stale
applies()oracle as on #1969 (here attests/test_capital_valuation_properties.py:240), not the new share-split properties. They'll clear when #1969's fix is carried through. - There's no CI on this PR, because its base isn't main, so these local runs are the only test evidence so far.
Findings:
-
Should fix (merge order). Retarget to main as soon as #1969 merges, and get CI green on the retargeted head before merging. Until then, nothing hosted has run on these lists. This PR must land in the same window as #1969 and #1974 (see my notes there): with those two in and this one out, the 10% on shares disappears.
-
Should fix.
parameters/gov/dwp/universal_credit/means_test/capital/sale_expenses/sources.yaml:20-22, and the same sentence in the other five lists. The ISA exemption is weaker than the unit-trust one:- H1674 says outright that unit trusts have no costs of sale. For ISAs, the guidance is silent and you're inferring from H1656 ("withdrawal value").
- A stocks-and-shares ISA holding quoted shares has to sell them through the ISA manager. That is a broker sale under H1605, and reg 49(1)(a) applies wherever "there would be expenses attributable to sale".
I'd keep ISAs exempt, since most ISA money is in funds, but describe it as a judgement, like the investment-trust one, rather than "no ISA paragraph applies the 10%". Give the directly-held-shares share of stocks-and-shares ISAs from HMRC Table 9.6 alongside it.
-
Nit.
variables/input/directly_held_shares.py:9. Employee share options now take the 10% as "shares". An unexercised option isn't sold through a broker and is often not realisable at all. It's fine as an approximation, but say so in the list descriptions next to the investment-trust caveat. -
Nit.
tests/test_capital_share_components.py. You removed the itemising-invariance test on purpose. A one-line comment pointing to the replacement differential intest_capital_valuation_properties.pywould stop someone restoring it.
|
Independent review (GPT-6.1 Sol, UK hub sweep, 2026-10-12): REQUEST_CHANGES at VERDICT: REQUEST_CHANGES
The stated mechanisms and invariants match the code under the chosen methodology. Property tests cover the load-bearing logic; replacing the old itemising-invariance test is justified. The exemption commit adds no skips or xfails. Ruff formatting and lint passed for its three changed Python files. Shell testing was attempted below the 30 GB compressor threshold. GB regulations and programme manuals were reviewed; several NI references and the HMRC percentages remain unverified. Requested |
# Conflicts: # docs/book/assumptions/growthfactors.md # policyengine_uk/data/uprating_indices.yaml
The independent review of 21412b6 found that reconstructing corporate_wealth as components plus a floored residual could tip a household across the capital limit on float32 rounding (components summing to 16,000.0009 against a supplied 16,000), and changed unsplit inputs with a negative corporate_wealth. The six capital lists now name one formula variable, share_like_capital: corporate_wealth as supplied, unless the itemised components add up to more than it beyond a rounding tolerance (1e-5 relative plus a penny), in which case their sum. Every dataset that carries only corporate_wealth, or builds it as the components' sum, counts exactly corporate_wealth; a household given only components has them counted. Tests: a differential against the original corporate_wealth lists on identical inputs (unsplit with negative and fractional aggregates, and consistent splits), annual and monthly, asserting capital and UC eligibility equal exactly; the float32 limit case and the negative aggregate; and the block's definition as a property. Replacing the formula with the old floored residual fails the float32 test. Also: the pension-age HB home disregard is Schedule 6 paragraph 26 of SI 2006/214, not paragraph 1 (review finding, predates this PR). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ale-expense-share-split # Conflicts: # docs/book/assumptions/growthfactors.md # policyengine_uk/data/uprating_indices.yaml # policyengine_uk/tests/test_capital_valuation_properties.py
…le-expense-share-split # Conflicts: # docs/book/assumptions/growthfactors.md # policyengine_uk/data/uprating_indices.yaml # policyengine_uk/tests/test_capital_share_components.py
#1974 replaced the components-plus-residual design with one share_like_capital block that keeps the supplied corporate_wealth exactly. This follow-up now splits that block in two: - share_like_capital_with_sale_expenses: share_like_capital less the itemised holdings in gov.dwp.share_like_capital.without_sale_expenses (unit and investment trusts, stocks and shares ISAs), floored at nil; all of it when nothing is itemised. - share_like_capital_without_sale_expenses: the rest, exactly nil when nothing is itemised. The six capital lists name both parts and the six sale-expense lists the first, so datasets that carry only corporate_wealth keep #1969's flat 10% exactly (differential test, annual and monthly, against the lists before itemising, including negative aggregates and a value whose 90% is a fraction of a penny over the limit). The exempt holdings are a parameter list, so the methodology call on investment trusts and ISAs (d1396) is a one-line change. Merged the refreshed #1969 (54ed063) and #1974 (7784193). #1969's property oracle reads the share-like sources from an unitemised household's corporate_wealth; the split invariants (definition of both parts, the unitemised differential, split effect = 10% of trusts plus ISAs on UC capital, components-only valuation) are in test_capital_share_components. Dropping the nothing-itemised branch from the formula fails the differential. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round-3 review of 7784193: with no corporate_wealth, a penny of shares fell inside the absolute tolerance and vanished, so UC eligibility at the £16,000 limit differed from the same penny entered as corporate_wealth; and 1e-5 relative plus a penny (£0.17 at £16,000, about 174 units in the last place) hid genuine excesses. The tolerance is now 1e-6 of the larger of corporate_wealth and the components' sum, against a float32 bound of about 3e-7 for a float64 sum of three components rounded and re-added in float32, and it is relative only, so a household with no corporate_wealth has any positive components counted in full. Tests: the component-winning case compares exactly against the engine's float32 sum; new independently specified cases for a component penny at the limit (same capital and UC eligibility as the penny entered as corporate_wealth, annually and monthly) and a genuine £0.10 excess; both fail under the old tolerance. The six list descriptions and the changelog state the qualified rule; the module claims are narrowed to UC eligibility and uprating metadata. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uping Review of b7e6bc5: - gov.dwp.share_like_capital.without_sale_expenses started in 2013, after the legacy capital lists (JSA from 1996-10-07), so a 2012 simulation with unitemised corporate_wealth raised ParameterNotFoundError; it now starts at 1996-10-07, and Income Support and JSA capital in 2012 are tested (both fail with the 2013 start). - An empty exempt list made add() return None; the exempt holdings are now summed from a numeric zero, and an empty-list reform (every share-like holding less 10%) is tested. - The property oracle now follows the engine's float32 grouping (components left to right, exempt holdings summed before subtraction) and compares exactly; the reviewer's example (trusts £200,000, ISA £199,999.984375) is pinned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ale-expense-share-split
…le-expense-share-split # Conflicts: # policyengine_uk/tests/test_capital_share_components.py
Round-4 review of f64da77 (APPROVE WITH NITS): - N2: the tests pinned the tolerance from above only. A new case uses pence-valued components (£16,015.98 + £286.98 + £81.05) whose float32 sum lands one unit in the last place (£0.00195) above £16,384.01: more than 1e-7 of the value, less than 1e-6. It exposed that comparing itemised > corporate_wealth + tolerance in float32 rounds the tolerance up to a whole unit; the formula now compares the difference, which float32 computes exactly for close values, and a 1e-7 tolerance now fails the case. Property-test components are drawn as pounds and pence. - N4: the differential now also runs a projected year (2027, annual and monthly), with the original lists restored for 2026-2030. - N1: the error-bound comment states it assumes float64 dataset tables. - N5: stocks_and_shares_isa says it is part of corporate_wealth. N3 (a microcosm export where corporate_wealth falls below the ISA) does not apply to microcosm main: corporate_wealth there is corporate_wealth_excl_isa plus the ISA, both drawn non-negative. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ale-expense-share-split
…le-expense-share-split # Conflicts: # policyengine_uk/tests/test_capital_share_components.py
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Needs changes before merge. The split itself is right and well tested. But this branch carries both CI failures from #1969 and #1974, it inherits the #1965 holding problem in both forms, and nothing hosted has run on it.
@MaxGhenis, this is a delta review. My last review was at b1a66004; this one is at 6a5a13a4. Since then the split has been rebuilt on #1974's block: share_like_capital_with_sale_expenses and _without_sale_expenses, with an exempt-holdings parameter. I reviewed the diff against #1969 + #1974 merged at their current heads: 21 files, +678/−186.
Status of my earlier findings:
- F1 (retarget to main, CI): still open. See finding 2.
- F2 (ISA exemption described as a judgement): partly addressed. The body now has a methodology note, but the parameter text still states it as settled. See finding 3.
- F3 (employee share options caveat): not taken. See finding 5.
- F4 (invariance test comment): addressed. The module docstring now points to the differential.
Findings:
-
must (carried in from #1969 and #1974). This head contains both CI failures and the #1965 interaction:
test_projection_horizon.py:181fails, as on #1974. The ISA variables are now mapped inuprating_indices.yaml.test_savings_credit_only_receipt_properties.py:508fails, as on #1969. It expects 601.02 where reg 19 gives 621.82.- Company holdings, probed on this head merged into current main. Take a single UC claimant who is sole owner of a trading company, with a £10,000 holding and £5,000 savings:
- with
corporate_wealth£10,000, UC capital is £4,000 (expected £5,000); - with only
directly_held_shares£10,000, UC capital is £14,000 (expected £5,000), because the pool cap still readscorporate_wealth.
- with
- On this head merged into main, the same 8
uc_company_analogous_business.yamlcases and 3test_uc_company_owner_properties.pyproperties fail as on #1969. Five of those YAML cases are the holding problem; three are the plain 90% revaluation and only need new expectations (see my #1969 review). - Here the holding should come off
share_like_capital_with_sale_expensesat 90%, since an owner's shares are directly held shares.
Once the fixes land on #1969 and #1974, merge them through and re-run.
-
must. No CI has run on this PR, because its base is
uc-capital-sale-expenses. The body's "Local runs" line is still "pending". Retarget to main and get the full suite green before anything in the stack merges. If this PR lands alone, it carries #1969 and #1974 with no window where the 10% on shares disappears. On a #1969 + #1974 tree without this PR, £5,000 savings +corporate_wealth£12,000 gives £17,000 for UC; this head gives £15,800. -
should.
parameters/gov/dwp/share_like_capital/without_sale_expenses.yaml:10-12still says "no ISA paragraph applies the deduction", as if that settled it. The changelog fragment states the ISA exemption flatly too.- ADM H1656 and DMG 29662 value an ISA at "what people would get if they withdrew their investment" and say nothing about costs of sale.
- The body parks this, and the investment-trust question, for a decision (d1396).
- Until it's decided, the parameter text should call the exemption a judgement, as it already does for investment trusts. I'm not suggesting which way to decide.
-
nit. The six
capital/sources.yamldescriptions each have an unwrapped line of about 175 characters. An example isuniversal_credit/means_test/capital/sources.yaml:8("share_like_capital_with_sale_expensesandshare_like_capital_without_sale_expensessplit ..."). Rewrap to match the rest. -
nit.
share_like_capital_with_sale_expenses.py:3hardcodesCOMPONENTSfor the "is anything itemised" gate, while the exempt list is a parameter. A reform adding a new exempt variable outsideCOMPONENTSwould be ignored for a household that itemises only that variable. Consider a comment saying the exempt list must be a subset of the components, or a test asserting it. Separately, employee share options still take the 10% as "directly held shares" with no caveat (my F3). One clause inwithout_sale_expenses.yamlwould cover it.
What I checked:
-
Guidance, re-read from the current PDFs:
- ADM H1656-H1658 (ISA at withdrawal value; nothing on costs of sale);
- ADM H1673-H1674 and DMG 29680-29681 (unit trusts: "no costs of sale ... even if persons use an agent");
- DMG 29662 and 29664 (ISA), and DMG 29671 (10% on shares).
The paragraph numbers in the parameters match. I didn't check the NI or HBGM numbers, or the HMRC Table 9.6 percentages.
-
Probes on this head:
- unsplit £5,000 +
corporate_wealth£12,000 gives £15,800 (UC, IS; PC at 70), as on #1969; - split shares £4,000, trusts £3,000, ISA £5,000 gives £16,600;
- components only with £1,000 savings gives £1,000 + 90% × £4,000 + £3,000 + £5,000 = £12,600, matching the body.
- unsplit £5,000 +
-
Revert check.
capital_valuation_share_split.yamlrun on #1969 + #1974 without this PR's source changes: 10 failed. At this head: 10 passed. -
Tests. I ran the same property files and YAML directories as on #1969 and #1974, plus #1965's company-owner tests.
- At this head: 2 failed (the two carried CI failures) and 2,360 passed; YAML 1,889 passed.
- Merged into current main (clean): 5 failed (those two plus the three #1965 properties) and 2,405 passed; YAML 8 failed (company cases) and 1,980 passed. Current main
714b1438con the same set: all 2,363 pass, and YAML 1,964 pass.
-
Lint.
ruff checkandruff format --checkon the 11 changed Python files: clean. -
Repo process. The
.fixedfragment and the Reproducer and axiom lines are present, and the named reproducer case exists. The branch also carries #1974's.addedfragment, which is fine if it lands as one unit. -
Overlap with my open PRs. #2382 merges cleanly with it.
-
Not checked. Any dataset run, or uk-data #501.
🤖 Generated with Claude Code
Stacked on #1969 (sale-expense deduction, head 1efee62). It also merges #1974 (
share_like_capital, head 365f642). Once both are on main, this diff is the split below plus its tests.What
#1969 deducts 10% for the expenses of sale from
corporate_wealthas a whole. The guidance treats its parts differently:#1974's
share_like_capitaliscorporate_wealthas supplied, or the itemised components' sum where that is larger. This PR splits it in two:share_like_capital_with_sale_expenses:share_like_capitalless the itemised holdings listed ingov.dwp.share_like_capital.without_sale_expenses(unit and investment trusts, stocks and shares ISAs), floored at nil. When nothing is itemised, it is all ofshare_like_capital.share_like_capital_without_sale_expenses: the rest. It is exactly nil when nothing is itemised.All six capital lists (UC, HB, IS, JSA, ESA, PC) name both parts, and all six sale-expense lists name the first.
Methodology note (d1396, parked). The UK hub's review asked whether the trust bucket and stocks and shares ISAs should escape the 10%:
The exempt holdings are therefore one parameter list of whole variables. A ruling that moves a whole component, for example applying the 10% to stocks and shares ISAs, or to the whole trust bucket, is a one-line parameter change and needs no code change. A ruling that applies the 10% to only part of a component (say, the investment-trust share of the trust bucket) cannot be expressed in this list; it would need a weighting parameter and a formula change.
Impact
None on any dataset that does not itemise
corporate_wealth, which covers every released uk-data build. The differential test below checks this exactly.On a split dataset (PolicyEngine/policyengine-uk-data#501), WAS round 8 puts directly held shares at 32.9% of the share-like wealth (£217bn of £659bn). The average deduction on
corporate_wealththerefore falls from 10% to about 3.3%. Measured caseload effects will come from #501's release-equivalent builds, which are queued behind the shared heavy-run lock.Invariants (tests)
test_capital_share_components.py, Hypothesis with pounds-and-pence inputs (as a dataset stores them; the engine rounds each to float32), checked exactly against the engine's float32 arithmetic:share_like_capitalkeeps the supplied aggregate unless the components exceed it beyond the tolerance.share_like_capitaland the other part is exactly 0. Otherwise the part with sale expenses equalsshare_like_capitalless trusts and ISAs, floored at nil, and the two parts sum toshare_like_capital.Fixed cases:
#1969's property tests keep their own legal asset set, now naming
share_like_capital_with_sale_expenses, and assert each programme's list against it. Their oracle values an unitemised household's share-like capital from itscorporate_wealth.Mutation check. Dropping the nothing-itemised branch from the formula fails the differential.
Round-2 review fixes (head 10c54a5):
ParameterNotFoundError. It now starts on 1996-10-07, with a 2012 regression for IS and JSA.add()returnsNonefor an empty list. The exempt holdings are now summed from a numeric zero, and an empty-list reform is tested.Local runs (head 6a5a13a): pending; results to follow.
Reproducer:
policyengine_uk/tests/policy/baseline/gov/dwp/capital_valuation_share_split.yaml::UC split of the same total exempts trusts and ISAs and crosses the limitaxiom: TheAxiomFoundation/rulespec-uk#395 queued (extended with the asset-type distinction: TheAxiomFoundation/rulespec-uk#395 (comment))
🤖 Generated with Claude Code