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>
|
Reviewed commit Confirmed integration conflict. A prospective merge of this exact head onto main Preserve the component contract. test_wealth_components.py:26 verifies the share components use the same entity, stock semantics and uprating as Repair and acceptance steps:
Suggested checks after resolving the dependency and conflict: uv run --no-sync pytest policyengine_uk/tests/test_wealth_components.py -q
uv run --no-sync ruff format --check .
uv run --no-sync ruff check .Validation already completed on this exact PR snapshot: 3 tests passed in Live posting check (2026-10-02T15:12:45.036767+00:00): same reviewed commit; GitHub reports |
# 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>
|
VERDICT: APPROVE WITH NITS I couldn't run any commands here: there was no shell, so I didn't run What checks out
Findings
Out of scope, but worth a look: |
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Thanks Max. Reviewed at head 21412b6a.
What I checked:
- Tests.
test_capital_share_components.pyandtest_wealth_components.py: 16 passed. DWP, family and household YAML: 1,040 passed. CI is green and it merges cleanly into current main. - The residual.
unitemised_corporate_wealth= max(0, cw − Σ components). On an unsplit household the capital totals match main exactly, so "no impact on any dataset" is true for this PR on its own. - Uprating. Each component is listed once under per-capita GDP, the same as
corporate_wealth, and thegrowthfactors.mdsentence now matches the YAML list. - No other readers. Nothing else reads
corporate_wealthin a means test: onlytotal_wealth,corporate_sector_wealthand the residual do.
Findings:
-
Blocking (merge order). This PR must not end up on main alongside #1969 without #2131.
- #1969's
sale_expenses.sourcesnamescorporate_wealth, which this PR removes from everycapital.sourceslist, so the 10% on shares silently disappears. - On a #1969 + #1974 tree, savings £5,000 +
corporate_wealth£12,000 gives £17,000 again for UC, HB and IS, instead of £15,800. Seven of #1969's YAML cases fail. - The "Impact: none on any dataset" claim holds only while #1969 is unmerged. Please say so in the body.
- Safe orders: merge this PR before #1969 and then land #1969 and #2131 back to back; or skip merging it separately and let #2131 (which contains it) carry it after #1969. If you take the second route, close this PR as superseded.
- #1969's
-
Nit.
variables/input/directly_held_shares.py:8-9. "listed or not" includes unquoted shares. They need an expert valuation (ADM H1679-H1681), not the H1665 price formula. The docstring already says so. It's worth a line noting that, under #2131, they still take the 10%, because selling them needs a solicitor or accountant (H1605). That reading is also the one #1965's owner-manager holding relies on.
# 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>
#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>
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>
|
UK hub (2026-10-13 16:16Z): #1965 (UC reg 77) merged as 🤖 Generated with Claude Code |
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Needs changes before merge. The share_like_capital block is sound, but this head fails a main test in CI, and it needs main's #1965 company-holding cap moved onto the new block.
@MaxGhenis, this is a delta review. My last review was at 21412b6a; this one is at 365f642d. Since then the floored residual has become the share_like_capital block with a 1e-6 relative tolerance, and main has been merged in up to 976ec91fe. Main has since moved on to 714b1438c, which includes #1965.
Status of my earlier findings:
- F1 (merge order with #1969): still applies. See finding 3.
- F2 (unquoted shares): addressed. The docstring now sends unquoted shares to an expert valuation (H1679-H1681).
Findings:
-
must. CI
Test / Python 1/4fails at this head, and this PR causes it. The failing test istests/test_projection_horizon.py:181,test_unmapped_inputs_preserve_legacy_years_and_grow_after_2030. Main's #2376 asserts thatcash_isaandstocks_and_shares_isaare unmapped inputs, which keep their legacy values through 2030. This PR lists both indata/uprating_indices.yaml(lines 96 and 123), so they are no longer inFALLBACK_INDEX_OF. I reproduced it locally on this head and on this head merged into current main. Main alone passes (13 passed).- Fix: drop the two names from that set. The per-index test above it then covers them.
- Say in the body that a dataset storing ISA balances now grows them with per-capita GDP every year, where before it held them flat to 2030. Nothing on main reads either variable in a formula, so no result on main changes. "Impact: none" is still true for outputs, but not for the projected data.
-
should.
household_uc_pool_company_holdings(from #1965, now on main) caps the holdings it takes out of the pool atmax_(0, corporate_wealth). That cap is invariables/gov/dwp/universal_credit/household_uc_pool_company_holdings.py:37. Once the capital lists countshare_like_capital, a household that enters only components keeps the holding in its capital.- Probe on this head merged into main: a single UC claimant who stands as sole owner of a trading company, with a £10,000 holding, £5,000 savings and
directly_held_sharesof £10,000 (nocorporate_wealth). UC capital is £15,000, and the pool holding is 0. The holding is disregarded under reg 77(2), so the capital should be £5,000. - The same household with
corporate_wealthof £10,000 gives £5,000, as expected. - Fix: cap at
share_like_capital, as the hub note says is planned. Add this case as a test, so the cap can't drift from the list again.
- Probe on this head merged into main: a single UC claimant who stands as sole owner of a trading company, with a £10,000 holding, £5,000 savings and
-
should (merge order, still live). At these heads, #1969 + #1974 without #2131 drops the 10% on shares, as before.
- On a #1969 + #1974 tree, £5,000 savings +
corporate_wealth£12,000 gives £17,000 for UC and IS, at ages 40 and 70, and for PC at 70. #1969 alone gives £15,800. - The reason: #1969's
sale_expenses.sourcesstill namescorporate_wealth, which this PR takes out of everycapital.sources. - The hub's order (#1974, then #1969, then #2131) is safe only if the three land back to back, with #2131 retargeted to main and green before the first merge. Landing #2131 alone, after retargeting (it contains both), leaves no gap.
- On a #1969 + #1974 tree, £5,000 savings +
-
nit.
share_like_capitalmakes components count whencorporate_wealthis absent, buttotal_wealthstill adds onlycorporate_wealth. A household given only components has share-like means-test capital and zero corporate wealth intotal_wealth. That's fine for now, since no dataset does this. A line in theshare_like_capitaldocstring would stop someone relying ontotal_wealthfor it.
What I checked:
- Guidance. ADM H1656, H1665 and H1673-H1674 against the docstrings, from the current ADM H1 PDF. I also confirmed that HB (SPC) Regs 2006 Sch 6 para 26 is the home disregard ("The dwelling occupied as the home"), so the citation fix is right.
- Block definition. Read
share_like_capital.pyagainst the body. The comparison is on the difference, so it doesn't round the aggregate up. - Probes. The ones above. I also ran an unsplit
corporate_wealthhousehold on this head merged into main, and it matches main exactly. - Tests. I ran the capital, share-component, wealth-component, stock-period, projection-horizon, PC reported-capital and savings-credit-only property files,
code_health, and #1965's two company-owner files. I also ran the YAML undergov/dwp,finance/benefit,household/wealthandgov/local_authorities.- At this head: 1 failed (finding 1) and 2,333 passed; YAML 1,865 passed.
- Merged into current main: 1 failed (the same) and 2,381 passed; YAML 1,964 passed. Current main
714b1438con the same set: all 2,363 pass, and YAML 1,964 pass.
- Lint.
ruff checkandruff format --checkon the 7 changed Python files: clean. - CI at
365f642d. Every check passes exceptTest / Python 1/4, which fails on finding 1 only (1 failed, 1,417 passed). - Overlap with other open PRs. #2382 also edits
uprating_indices.yamlandgrowthfactors.md, but in other sections (the CPI list). - Not checked. Any dataset run. The WAS mapping in uk-data #501.
🤖 Generated with Claude Code
What
corporate_wealthbundles assets the means tests value differently:This PR prepares the model for datasets that itemise it, without changing any existing result:
directly_held_shares(UK shares, listed or not, plus employee shares and options, held outside ISAs and pooled funds) andunit_and_investment_trusts(the Wealth and Assets Survey asks about both in one question).cash_isaandstocks_and_shares_isafrom Add cash ISA and stocks and shares ISA holdings variables #1791, joincorporate_wealthunder per-capita GDP inuprating_indices.yaml, so a dataset's identitycorporate_wealth == Σ componentssurvives projection. Thegrowthfactors.mdsentence now matches the YAML list exactly.share_like_capital(formula, STOCK). It iscorporate_wealthas supplied, unless the itemised components are positive and add up to more than that by more than 1e-6 of the larger of the two, in which case it is exactly their sum. The tolerance covers float32 rounding: a dataset that buildscorporate_wealthas the float64 sum of three components and stores all four rounds each once, and the engine re-adds them in float32, so the two totals differ by at most about 3e-7 of the value. With nocorporate_wealth, any positive components count in full, down to a penny.share_like_capitalinstead ofcorporate_wealth.Impact
None on any dataset; this is tested exactly.
corporate_wealth(every released uk-data build) counts exactlycorporate_wealth, any sign.corporate_wealthas the components' sum: Split pensions, shares, trusts, ISAs and secured debt out of WAS wealth policyengine-uk-data#501, and Microcosm's currentstocks_and_shares_isaexport.#2131, stacked on #1969, then splits
share_like_capitalinto the part with sale expenses (directly held shares, plus anything not itemised) and the rest. Unit trusts and ISAs therefore escape the 10%, and unsplit inputs keep #1969's treatment exactly.Invariants (tests)
The block's definition (Hypothesis, float32 inputs including negative aggregates): it equals
corporate_wealthexactly unless the components exceed it beyond the tolerance, and then equals their sum.Differential against the original lists. On identical inputs, unsplit (negative and fractional aggregates) or split consistently, every programme's assessable capital and UC eligibility equal those under a reform restoring
corporate_wealthto the six lists. The check is exact equality, annual and monthly.Boundary cases, specified independently of the formula:
corporate_wealthof −£2 gives main's £5,999.corporate_wealth, on top of £16,000 of savings, gives the same capital and UC eligibility as the penny entered ascorporate_wealth, annually and monthly.The earlier floored-residual design, and the earlier tolerance, each fail one of these.
Uprating. Components share
corporate_wealth's entity, quantity type, uprating and YAML index;total_wealthdoesn't add them on top ofcorporate_wealth.Local runs (head f64da77, main 976ec91 merged)
test_capital_share_components.py,test_wealth_components.py,test_stock_capital_periods.py,code_healthgov/dwp,household/wealth,gov/local_authorities(incl. CTR routes)ruff format --checkReviews
A round-4 review has been requested at f64da77.
axiom: n/a: model plumbing that changes no rule; the sale-expense rule itself is #1969's (rulespec-uk#395)
🤖 Generated with Claude Code