Skip to content

UK landlords: bind the SPI net property amounts, leave property_wealth to the engine, add receipts and finance costs - #1145

Merged
juaristi22 merged 19 commits into
mainfrom
uk-property-income-1106
Oct 12, 2026
Merged

juaristi22 merged 19 commits into
mainfrom
uk-property-income-1106

Conversation

@juaristi22

@juaristi22 juaristi22 commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1106. Rebased onto main d1fda720b on 2026-10-12 (five commits since the previous base: #1202's NZ weight-quality gates, #1178 and #1186's PUF Social Security fixes, and two pool-checkpoint commits; none touches the UK build). The only overlap was the UK concept-coverage golden, regenerated on the merged mapping so main's source_household_id entry sits beside the landlord inputs. The earlier rebase the same day, onto d648c976c after #1167 merged, had picked up main's Chronicle feed re-pin to 1ee7dfe: the PRIS resource is re-vendored from it and the references, membership and compile-parity receipts are regenerated against it in one commit near the head; the intermediate commits keep the surfaces they were measured with. Active national references: 1,245 (main's 1,232 plus the 13 SPI property amounts). The spec mirror, the coverage manifest, the H2 fixture and the gate-battery digests are re-derived on the merged spec. Marked ready for review on 2026-10-10; Max's ruling on the add-back is still open.

What changes

A. The SPI net property-income amounts bind unscaled (e34412b15). SPI Table 3.7 "net income from property" is landlords' profit after allowable expenses and before residential finance costs, the concept property_income holds (PolicyEngine/policyengine-uk#2172 documents it). The incumbent's x1.9 scaling set the Property Rental Income Statistics (PRIS) receipts, which are before expenses, against it. The signed exclusion is retired and the 13 rows bind at £33.60bn for 2025 (the incumbent fixture holds £55.78bn), uprated by the band index the count rows' growth belongs with, hmrc.itl_2026.total_income_growth_by_total_income_band (María's ruling of 2026-10-11 on the review's item 4; the flat per-capita GDP index measured in the arms before the final one gave £31.78bn and implied per-band mean profit moves of −13% to +11% against the count rows).

B. property_wealth stays with the engine (0f86d6a54). The WAS stage saved the survey's own total, which overrides policyengine-uk's sum of the three property components and is never uprated (the uk-data#543 defect). The release export drops it, and so does the measure resolver's scratch H5, inside #1115's _engine_scratch_frame. It is a reviewed export exclusion and an engine-derived coverage exclusion; the three components stay required. On the A9 candidate at 2025, measured with policyengine-uk 2.100.0, this moves property_wealth from £7,469.5bn to £7,633.4bn and household land value from £4,595.0bn to £4,697.3bn (target £4,559.8bn). The scratch H5 applies the whole release drop list; of the other dropped columns only incapacity_benefit_reported is an engine variable, and it is all zero on the spine.

C0. policyengine-uk 2.123.0 (e80030848), the release with PolicyEngine/policyengine-uk#2172's landlord inputs. Only policyengine-uk moves in the lock; policyengine-core stays 3.32.19 on the UK side and 3.32.5 on the US side.

C1. Finance costs on the SPI channel (dbbf83fe9). The tape's LLIR_RESTRICT_AMT_TOT (restricted land property finance cost) becomes property_finance_costs, drawn last in the first-stage SPI model so every earlier draw is unchanged, and uprated by obr.mortgage_interest. The donor is refused unless TAX_CRED carries the 20% relief (median ratio between 0.195 and 0.205).

C3. A property_components stage (abb061af0, walk rule 2efba5388) after regional_property_uprating.

  • FRS landlords report ROYYR1 after show card K6, which nets mortgage interest and capital. Their finance costs are drawn from the tape among its landlords with a profit after finance costs: person-keyed incidence by decile, then the amount from a regime-gated QRF on profit and age. The draws are added back to property_income (see the question for Max below).
  • Every landlord gets property_rental_income (receipts) from PRIS 2024-25 Table 13. Landlords are ranked by profit and walked into its eleven bands with the CGT Table 3 walk's rule, each band filled to its PRIS share of the spine's own landlords. Receipts are linear within a band and never below profit. The open top band takes max(100,000, k × profit) with k ≥ 1 solved toward the individuals' receipts total, or held at 1 with the overshoot recorded where the floor at profit already exceeds that share; on this head's spine k is 1.166.
  • A release-blocking stage gate, uk_stage_property_components: no receipts below profit, no landlord banded below their profit, every bounded band within its recorded rounding allowance (the weight of the person straddling each boundary), and the top band's multiplier below its cap. The vendored PRIS resource has 44 rows (Tables 1, 2, 6, 8 and 13).
  • The band donors' pools are cut on the income leaves alone, so a record's restricted finance costs never move it between total-income bands (the review's item 1).

PRIS stays a diagnostic. bb900bba1 bound three PRIS targets (individuals' receipts, the eleven landlord counts by size of receipts, residential finance costs); fc7a9484d reverts it, so active national references stay at 1,244. In every PRIS arm the SPI property amounts missed the 10% fit band, two arms breached the 25% release bound, and uk_weight_ratio failed (below). The stage receipt reports the spine against PRIS instead.

Measurement

Licensed arms with a national calibration and local staging only. The scripts and run outputs live outside the repo, in data/ukds/acceptance/1106-property-income/ (licensed inputs); the receipts are in experiments/1106-uk-property-income-receipts.md. The arms up to c7 ran on the pre-rebase commits, kept as local tags, from the 825406f feed artifact (rebuilt on 2026-10-08 with both digests matching the pin); arms c8 and c9 run on this head from the 1ee7dfe artifact main now pins.

  • Control (UK: port the remaining uk-data fixes tracked in #1095, engine items included #1121's head, policyengine-uk 2.122.2): loss 0.00712, 98.3% of 1,167 targets within 10%, 7 of 7 calibrated-seam gates pass.
  • C3 (A, B, C0, C1 and the stage; no PRIS targets): loss 0.00714, 98.1% of 1,180 within 10%. The 13 SPI property amounts fit to 0.1% (48% off at design weights).
  • Engine quantities at 2025 (policyengine-uk 2.123.0, control against c3x):
    • The finance-cost relief is £1.90bn for 1.03m landlords, with £2.21bn of costs carried forward for 0.31m.
    • property_finance_costs total £11.71bn (PRIS: £12.13bn).
    • The property allowance deduction falls from £0.117bn to £0.062bn.
    • Housing Benefit −£21m, Council Tax Reduction −£26m, Pension Credit +£10m, Universal Credit +£3m.
    • income_tax takes 0.33 s against 0.23 s to compute.
  • The share-filled walk on the spine: every band, the lowest included, holds its share within rounding. At the C3 calibrated weights it gives £46.11bn of receipts against PRIS's £49.81bn (2024 values), and 1.19m, 0.90m and 0.35m landlords in the lowest three bands against 1.29m, 0.85m and 0.33m.
  • PRIS as targets, four arms. Each fits the PRIS rows it binds to within 3.3%, but each breaks the SPI fit:
    • All 13 rows with the count-filled walk: £20,000 to £30,000 of total income −17.8%.
    • The same without PRIS's lowest band: still −17.7%.
    • All 13 with the share-filled walk: £70,000 to £100,000 +48.2%.
    • The two totals alone: still +30.9%.
    • uk_weight_ratio fails in all four.
  • The pre-review head (arm c6x, measured as 7b20ef73c, now 700e2e901): loss 0.00710, 98.1% of 1,180 within 10%; all gates pass once the stale West Midlands deferral is dropped. Receipts £45.72bn against PRIS's £49.81bn.
  • The review-fixed head before the rebase (arm c7, f478063d5, no measurement-only edit): loss 0.00700, 98.6% of 1,180 within 10%, the SPI property amounts within 0.4% of their £33.60bn and the counts within 0.3%. All 7 calibrated-seam gates and all 33 spine gates pass; the West Midlands cell sits at +20.6%.
    • Against PRIS at the calibrated weights: receipts £49.57bn against £49.81bn (−0.5%), finance costs £9.29bn against £11.08bn (−16%), and 2.78m landlords against 2.85m. The band counts sit within 14% of PRIS up to £40,000 and scatter above it (−39% to +36%).
    • The band-donor fix (review item 1) moves the SPI channel's finance costs from £7.78bn to £5.31bn at design weights: the old pools had lifted cost-heavy tape records into higher total-income bands.
    • The open top band's multiplier is 1.166 on this spine, so landlords with £100,000 or more of receipts carry expenses of 14% of receipts; on the pre-review head it sat at its floor of 1.
    • At 2025: property_rental_income £52.11bn (PRIS uprated £51.99bn); property_finance_costs £10.25bn (PRIS £12.13bn); relief £1.76bn for 0.99m landlords, £1.46bn carried forward; the property allowance deduction £0.144bn for 0.32m (the control: £0.117bn for 0.22m); Housing Benefit −£38m, Council Tax Reduction −£69m, Pension Credit −£7m, Universal Credit +£82m against the control, which is calibrated separately.
  • This head after the rebase (arms c8 and c9, a33079775): the spine rebuilt afresh in 359 s; 37 of 37 spine gates pass (the four new ones are Set UK road-fuel and energy levels in the LCFS stage on a calendar-2024 basis (#1113) #1167's LCFS gates). The national step then stopped on main's native-evidence reader (microcosm#1187: run manifests are charged the schema's per-item budget), which fails every national build after a spine built on current main; this PR does not touch it, and Bound native evidence by bytes, not by the schema item budget #1188 (open) is the fix. Arm c9 calibrated the c8 spine with Bound native evidence by bytes, not by the schema item budget #1188's commit overlaid for measurement only: loss 0.00644, 98.5% of 1,177 targets within 10% (the set moved with Set UK road-fuel and energy levels in the LCFS stage on a calendar-2024 basis (#1113) #1167's measure exclusions), ESS 5,203, 7 of 7 calibrated-seam gates.
    • The SPI property amounts fit to 0.3% and the counts to 0.2%; the West Midlands cell sits at +24.2%, inside the 25% bound.
    • The stage's design-weight receipt is identical to c7's: the rebase moved nothing in the stage's output.
    • Against PRIS at the calibrated weights: receipts £49.54bn (−0.5%), finance costs £9.28bn (−16%), 2.78m landlords.
    • At 2025: property_finance_costs £10.24bn; relief £1.77bn for 1.03m landlords, £1.38bn carried forward; the allowance deduction £0.143bn for 0.33m; income tax £326.01bn against the control's £327.05bn; Housing Benefit +£4m, Council Tax Reduction −£41m, Universal Credit +£44m.
    • A national build or the certifier on this head needs Bound native evidence by bytes, not by the schema item budget #1188 merged first.

Signed and retired (2026-10-11)

  • UK: port the remaining uk-data fixes tracked in #1095, engine items included #1121's West Midlands deferral is retired: the cell sits inside the 25% bound (+23.1% on C3, +23.4% on the pre-review head, +20.6% on the review-fixed head, +24.2% on this head after the rebase), so the gate failed it as stale. target_fit_reviewed_exclusions.json is empty again.
  • María signed the property_wealth entry in input_mass_reviewed_exclusions.json (expires 2027-04-11) and the landlord-inputs-person-net-new-columns entry in spine_swap_signed_differences.json; the engine-derived coverage exclusion carries her signature in the ledger.

Decisions held

  • For Max, the FRS add-back (decision 6). C3 adds FRS landlords' imputed finance costs back to property_income, so the engine relieves the interest once, as the s. 274A reduction; the capital part stays netted. This departs from the uk-data#509 rule of using FRS amounts as reported. The alternative is to leave FRS profit as reported and give FRS landlords no finance costs. We will change it before merge if Max disagrees.
    • Size at 2025 (Vahid's review item 1). In policyengine-uk 2.123.0 hbai_household_net_income, market_income and household_market_income add property_income, profit before finance costs, while HBAI's published basis is FRS property income after show card K6. On the c9 candidate at its calibrated weights, reversing the add-back (FRS landlords' profit as reported, no finance costs) lowers HBAI household net income by £2.95bn (0.20% of £1,508.5bn) and household market income by £3.29bn, lowers income tax by £0.34bn, cuts the finance-cost relief from £1.77bn to £1.11bn, and raises the before-housing-costs poverty rate by 0.02 points and the after-housing-costs rate by 0.04 points; the Gini of equivalised HBAI income moves by 0.0002. The SPI channel sits on the same before-interest basis. A policyengine-uk follow-up for HBAI and market income to read property_income_after_finance_costs, as the means tests do, makes the add-back neutral for HBAI and fixes the SPI side too (issue draft held for María).
  • Optional signature (María): retiring the two stale property count exclusions, which expire on 26 November 2026.

#1167's re-review items, taken here

At María's direction the minor items of #1167's re-review (its comment of 2026-10-11) are addressed in this PR, since #1167 has merged.

  • Hardening: the evidence sanitiser drops donor_min and donor_max beside donor_floor, keeps only the spine file's name, and the committed outputs and scripts carry no machine-local paths (the scripts read the inputs root from PE).
  • Minor 1: an ambiguous or duplicate vendored row is a data fault and fails closed as a plain ValueError; only a missing year is recorded as a report-only diagnostic being unavailable.
  • Minor 2: the recomposition receipt records each parent's uprating residue (the parent's factor against each netted part's), not corrected: about £1bn in housing and £1.7bn in transport on the committed totals. Rescaling the parts before netting is a modelling change for a ruling.
  • Minor 3: UK consumption-tax levels: open items after #1167 #1207's acceptance sentence is the issue's text; a corrected sentence is proposed in the reply.
  • Nits: the consumption-basis gate checks each bridged column's recorded factor against after / base; the uncarried_spend and donor_uprating docstrings and the obr.fuel_duties_cars and 04.5 binding notes are current; the evidence README says the per-division movement between runs is redraw noise around the uniform bridge.

Known residuals

  • PRIS counts UK property only, while the SPI profit and the engine's receipts include overseas property. The open top band's multiplier can sit at its floor of 1 on another spine; the receipt reports it.
  • Finance costs run 16% under PRIS at the calibrated weights.
  • Landlords without profit carry no receipts. PRIS counts them; in the FRS 2024-25, 67 adults (0.11m weighted) report a loss from other property.
  • Sub-letting rent keeps equal receipts until Rent a Room is released (Model rent from boarders, lodgers and sub-tenants for the householder policyengine-uk#2002).
  • Furnished holiday lettings (Vahid's item 2): the tape is 2022-23, when FHL landlords still deducted finance costs in full, so their SPI profit is after interest and LLIR_RESTRICT_AMT_TOT excludes their costs. The FHL regime ended on 6 April 2025, so at 2025 their interest falls under s. 272A: their property_income and property_finance_costs are both understated by it. Small, with the direction known.
  • FRS mortgage capital stays netted, and it also shifts the finance-cost draw (Vahid's item 3): the model is fitted on the tape's profit after interest, but FRS landlords are queried with ROYYR1, which is after interest and capital repayments, so a repayment-mortgage landlord takes the incidence and amounts of tape landlords with a lower profit after interest. Most buy-to-let lending is interest-only, so this is small.
  • property_finance_costs_brought_forward is unset.

Testing

  • The engine-free and engine-UK lanes, the coverage manifest --check, and the vendoring and pin checks: run on C3.
  • After the revert: the target, calibration, country-spec and stage tests (293 pass).
  • After the rebase onto UK: port the remaining uk-data fixes tracked in #1095, engine items included #1121's merge: H2 spine parity, the stage and FRS spine tests (120 pass), tools/ci_test_plan.py verify, and ruff on every changed file.
  • After the rebase onto a469ebe3d: the engine-UK lane with H2 parity (159 pass), 17 engine-free files across the touched surfaces (817 pass, 1 skipped), the coverage manifest --check, a fresh H2 fixture regeneration with no diff, the lock pin, tools/ci_test_plan.py verify and ruff.
  • After the review fixes: the engine-UK lane with H2 parity (159 pass), 27 engine-free files across the touched surfaces, the manifest --check, the uprating-pins check, tools/ci_test_plan.py verify and ruff; the H2 fixture regenerated after the spec edit; arm c7 on the committed tree.
  • After the rebase onto d648c976c: the engine-UK lane with H2 parity (159 pass), 29 engine-free files across the touched surfaces (1,055 pass), the references test (30 pass), the LCFS, stage-health and donor-uprating tests (124 pass), the coverage manifest --check, the PRIS vendoring --check, the lock pin, the uprating-pins check, tools/ci_test_plan.py verify and ruff; arms c8 and c9 on the committed tree.
  • After the rebase onto d1fda720b: the engine-UK lane with H2 parity (159 pass), 20 engine-free paths across the touched surfaces plus the frame and spec-loader tests (2,192 pass, 54 skipped), the references test against the pinned feed (30 pass), the concept-coverage --check, the coverage manifest --check, the PRIS vendoring --check, the lock pin, the uprating-pins check, tools/ci_test_plan.py verify and ruff. The rebase is content-exact: the new head differs from the old one by main's own diff and the merged concept-coverage files alone.

🤖 Generated with Claude Code

@juaristi22

Copy link
Copy Markdown
Collaborator Author

@MaxGhenis, we need two rulings on the FRS property rule from #1081 and uk-data#509 before this merges.

1. Adding imputed finance costs back to FRS landlords' profit. ROYYR1 is the rent after the items on show card K6, which include mortgage payments and loan interest. So FRS landlords' profit sits below the SPI concept that property_income now holds: profit before residential finance costs (PolicyEngine/policyengine-uk#2172). The property_components stage draws each FRS landlord's finance costs from the SPI 2022-23 tape and adds them back to property_income. The draw has two steps: whether a landlord has costs follows the tape's share in their decile of profit after finance costs, and the amount comes from a QRF on that profit and age. The capital part of the mortgage payments stays netted.

  • Size. On the licensed spine at design weights (2024 values), 32% of the 1.03m FRS landlords draw costs, £2.37bn in all. That raises the FRS channel's profit from £9.5bn to £11.9bn.
  • Income tax. As reported, the interest is in effect relieved at the landlord's marginal rate. With the add-back, income tax charges it and gives the 20% reduction of ITTOIA 2005 s. 274A instead.
  • Means tests. Housing Benefit, Income Support, tax credits, Council Tax Reduction and Pension Credit read property_income_after_finance_costs, so they still see the reported amount.
  • HBAI. HBAI net income reads property_income, so it rises by the interest, less the extra tax.
  • Fit. With the add-back, the 13 SPI property amounts calibrate to within 0.2%, and every gate passes once a stale deferral inherited from UK: port the remaining uk-data fixes tracked in #1095, engine items included #1121 is removed (experiments/1106-uk-property-income-receipts.md, section C8).
  • The alternative is to leave FRS profit as reported and give FRS landlords no finance costs. All finance costs would then come from the SPI channel: £7.78bn of the £10.15bn now drawn at design weights, against PRIS's £11.08bn for individuals. The SPI amounts bound on property_income would then lean on reweighting, as frs_property_income's docstring notes.

2. If the add-back stands, what about landlords who report a loss? Because K6 nets mortgage capital and interest, an FRS loss is often not a tax loss. Take £12,000 of rent, £2,000 of repairs and £11,000 of mortgage payments, £9,000 of them interest. That is reported as a £1,000 loss, but for tax the profit is £10,000, with £9,000 of finance costs. The stage draws finance costs only for FRS landlords reporting a profit, so landlords with a mortgage who report a loss stay at zero. The consistent extension would draw their interest the same way, add it to the reported loss, and keep the result if it is positive. That turns some reported losses into profits, against "a loss counts as zero". It affects at most 67 adults in the FRS 2024-25 (0.11m weighted). Should we extend the add-back to them, or keep their losses at zero?

If you disagree with either, we'll change the stage before merge and re-measure.

@juaristi22
juaristi22 force-pushed the uk-property-income-1106 branch from 668d916 to 4cb36ac Compare October 9, 2026 12:28
@juaristi22
juaristi22 marked this pull request as ready for review October 10, 2026 22:41
juaristi22 added a commit that referenced this pull request Oct 11, 2026
- Band donors: the pools are cut on the income leaves alone, so a record's
  restricted finance costs never move it between total-income bands; the
  costs travel with the drawn record (review item 1, with a test).
- property_components: each bounded band records its rounding allowance,
  the weight of the person straddling each of its boundaries; the top band
  records whether its multiplier sits at the floor of 1, the floor mean and
  the overshoot; the stage refuses a frame off the declared build period;
  the unused nonnegative-columns constant is gone (items 7, 8, 14).
- uk_stage_property_components holds each band to its own allowance, fails
  any landlord banded below their profit and reports a multiplier at the
  floor; five gate tests join (item 8). The gate-battery and spine part
  digests move with gates.json.
- The input-mass gate compares the release surface, the export frame, on
  both paths (item 5).
- Wording: the top band is solved where the target mean is reachable and
  otherwise held at 1; the PRIS arms missed the 10% fit band, two breached
  the release bound, and uk_weight_ratio failed; the scratch H5's drop list
  is spelled out (items 6, 7, 9).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Oct 11, 2026
The 13 SPI Table 3.7 net property-income amount rows move from the flat
per-capita GDP index to hmrc.itl_2026.total_income_growth_by_total_income_band,
the index the paired count rows' growth belongs with and the employment,
self-employment and private pension amount rows already use (María's ruling
of 2026-10-11 on the #1145 review, item 4): GBP 33.60bn for 2025 instead of
31.78bn, with factors from 0.977 to 1.236 by band. The flat index implied
per-band mean profit moves of -13% to +11% against the count rows. The
generator's rationale, the docs and the receipts note say PRIS stays a
diagnostic and describe the FRS-side add-back.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@juaristi22

juaristi22 commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. Every item is addressed in the five commits above 4cb36acf2 (head adafe6b8c), and arm c7 re-measures the committed tree at f478063d5 with no measurement-only edit (receipts note, section C9). Point by point:

Must fix

  1. Band-donor pools counted finance costs as income. Fixed in spi_income._resample_band_donor_leaves: the pools are cut on the income leaves alone, and a drawn record's finance costs travel with its leaves at their own factor. test_resample_pools_ignore_restricted_finance_costs puts costs on every sub-200k record large enough to lift it into the 200k pool and checks the pool is unchanged and the costs are copied. The receipt's pool_basis says so. The C1 claim holds again. The fix matters: it moves the SPI channel's finance costs from £7.78bn to £5.31bn at design weights, since the old pools had lifted cost-heavy records into higher bands, and it lifts the top band's multiplier off its floor (item 7).
  2. The stale West Midlands deferral. Retired: target_fit_reviewed_exclusions.json is empty again, with María's ruling of 2026-10-11, and test_committed_target_fit_register_retains_only_live_deferrals pins that. You marked the PR ready for review on 2026-10-10, so the body's "Draft" line was stale; it now says the PR is in review with Max's ruling open.
  3. The measured tree was not the merged tree. Arm c7 builds the spine and calibrates on f478063d5: loss 0.00700, 98.6% of 1,180 targets within 10%, 7 of 7 calibrated-seam and 33 of 33 spine gates with nothing dropped, the West Midlands cell at +20.6%. Against PRIS at the calibrated weights: receipts £49.57bn against £49.81bn, finance costs £9.29bn against £11.08bn, 2.78m landlords against 2.85m. The body says where the scripts live (data/ukds/acceptance/1106-property-income/, licensed inputs, outside the repo) and that the earlier SHAs are the pre-rebase commits, kept as local tags.

Should fix

  1. Flat index on the 13 amount rows. Switched, on María's ruling: the rows now uprate by hmrc.itl_2026.total_income_growth_by_total_income_band, the index their count rows belong with, and bind £33.60bn (factors 0.977 to 1.236 by band). The target's notes record the −13% to +11% per-band mean moves the flat index implied. Dividends keep the flat index; that pairing is a separate question.
  2. Signatures. Signed by María on 2026-10-11: the property_wealth entry in input_mass_reviewed_exclusions.json (its reason says which path is authoritative: the release surface, so the comparison is skipped on both the raw-frame and export paths while the three components stay compared) and the landlord-inputs-person-net-new-columns entry in spine_swap_signed_differences.json, with the figures measured on the final head's spine. The engine-derived coverage exclusion now carries approved_by and approved_on in the ledger and in the manifest tool's constant. The two stale property count exclusions stay for María's optional call; they expire on 2026-11-26. They are not why the fit runs on 1,180 targets: the measure-exclusion register holds many more cells than those two.
  3. The scratch H5 drops more than property_wealth. True, and now in the changelog. Of the dropped columns only incapacity_benefit_reported and property_wealth are policyengine-uk variables, and incapacity_benefit_reported is all zero on the spine; the five reported disability amounts and the salary-sacrifice carrier are not engine variables, so the engine ignores them whether present or not. Nothing the solve measures changes beyond property_wealth.
  4. The top band is not "solved". The code, the spec, the gate notes, the changelog and the body now say: k ≥ 1 solved by bisection where the target mean is reachable; otherwise k = 1 with the overshoot recorded. The receipt carries multiplier_at_floor, mean_receipts_at_floor_gbp, mean_receipts_shortfall_gbp and target_mean_nonpositive; the gate reports the floor without failing it. test_the_top_band_holds_its_floor_and_records_the_overshoot covers the case. On the c7 spine the multiplier is 1.166 (floor mean £133,270 against a target mean of £145,858), so the top band's landlords now carry expenses.
  5. The gate could barely fail. The stage now records each bounded band's rounding allowance, the weight of the person straddling each of its two boundaries, and the gate holds the band's error to that allowance (maximum_band_error_in_boundary_weights, 1.0) instead of twice the largest landlord weight. Your empty-band case fails: test_property_components_gate_holds_a_band_to_its_own_rounding_allowance. The gate also fails any landlord banded below their profit (maximum_rows_with_profit_above_band, 0) and reports top_band_multiplier_at_floor. Five gate tests join test_uk_stage_health.py; the receipts-below-profit check stays as a second line behind the stage's own raise. The gate-battery digests moved with gates.json.
  6. "Out of their bound." Reworded in the changelog, the body and the receipts note: the SPI property amounts missed the 10% fit band in all four PRIS arms, two arms breached the 25% release bound, and uk_weight_ratio failed in every one. The revert commit's message keeps the looser phrase; the decision stands on the corrected reason.

Minor

  1. uv.lock revision 3. uv is updated to 0.13.0 and the lock re-derived from main's: revision 5, policyengine-uk 2.123.0, and the pin moved. The only other line that differs from main is a ptyprocess marker the newer uv writes.
  2. Stale text. The generator rationale, its test pin and the emitted membership report now say PRIS stays a diagnostic; the receipts note's line on C4 says C8 reverts it; the docs bullet describes the add-back instead of "not done".
  3. Figures from other runs. The body's engine figures are now c7's, means tests included (Housing Benefit −£38m, Council Tax Reduction −£69m, Pension Credit −£7m, Universal Credit +£82m against the separately calibrated control), and it says Part B was measured on policyengine-uk 2.100.0.
  4. Receipts ≥ profit at 2025. It holds: policyengine-uk 2.123.0 uprates property_income and property_rental_income with the same index, obr.per_capita.gdp (data/uprating_indices.yaml), so the floor survives uprating.
  5. Nits. mapped_build_period is now checked against the frame's period (test_the_stage_refuses_a_frame_off_the_declared_build_period); the unused nonnegative-columns constant is gone (the manifest's nonnegative_outputs is the declaration). The TAX_CRED check keeps None for a tape with no restricted costs on purpose: the synthetic test tapes carry none, and the build fails closed where the costs are consumed, in property_components.

#1198 and #1167. Noted. Whichever lands first, the other rebases; if #1198 lands first, PRIS is re-vendored from 942a1fa and the West Midlands cell is measured again on the bridged head.

@juaristi22

Copy link
Copy Markdown
Collaborator Author

Re-review (Claude Code) at adafe6b8c

Static review of 4cb36acf2..adafe6b8c against the previous review and the reply. I ran nothing. f478063d5, the tree arm c7 measured, is on the branch, and adafe6b8c changes only the receipts note. CI for this head was still queued.

Verdict.

  • The code fixes hold, and there's no major finding.
  • Three replies claim more than the tests show: items 1, 5 and 8.
  • Text: a few spots are newly stale.
  • Disclosure: a few disclosure-control items went to the author separately.

Previous items

# Status Note
1 Band-donor pools Code fixed; test doesn't catch it (M1) spi_income.py:1155-1169 cuts the pools and the outside guard on income leaves only, and :1258 copies the costs at their own factor. The pools are again the pre-#1106 pools, so "earlier draws unchanged" holds. gift_aid and charitable_investment_gifts still count toward the pool total; that predates this PR.
2 West Midlands Fixed The register is {}, and test_uk_terminal_gates.py:570 fails any re-added entry.
3 Measured tree Fixed c7 ran on f478063d5; only receipts follow. Its base is still a469ebe3d; see CI below.
4 Amount index Fixed All 13 rows use total_income_growth_by_total_income_band: £33.596bn, factors 0.977–1.236. Implied per-band means now run 0.993–1.025, as employment's do. The £1m+ band is +2.51%, a hair over the note's "within 2.5%".
5 Signatures Partly; see M4 and M5 The input-mass property_wealth entry is fully signed, with an expiry, and the gate now compares uk_release_export_frame(...) on both paths (battery_bindings.py:1487). The spine-swap entry is signed.
6 Scratch drops Fixed Only incapacity_benefit_reported and property_wealth are policyengine-uk variables in 2.122.2, and in 2.123.0 at its tag. The changelog says so.
7 Top band Fixed; text partly stale (M6) The bisection and floor are recorded, and the gate reports the floor without failing. The floor test covers only a non-positive target, not a positive target with overshoot.
8 Gate Partly (M2) Per-boundary allowances and rows_with_profit_above_their_band are now gated, with five new tests.
9 Wording Fixed
10 uv.lock Fixed Revision 5. Against origin/main only the pe-uk 2.123.0 lines and the ptyprocess marker differ, and APPROVED_UV_LOCK_SHA256 matches the committed bytes.
11 Stale text Fixed, with new spots (M3, M6)
12 Figures Fixed They're c7's, and Part B states policyengine-uk 2.100.0.
13 Receipts ≥ profit Verified 2.123.0's uprating_indices.yaml puts property_income and property_rental_income on obr.per_capita.gdp, and property_finance_costs on obr.mortgage_interest.
14 Nits Fixed mapped_build_period is enforced and tested (property_components.py:912-918).

New

Minor

  • M1. The item-1 test passes on the old code. test_resample_pools_ignore_restricted_finance_costs (test_uk_spi_band_donors.py:623-668) sets the finance-cost factor to 2.0. Under the old pool total, the sub-200k records with 300k of costs land in the 500k pool, not the 200k one. So the 200k pool count, carriers_outside_band, the drawn pay and the copied cost all still pass; only the pool_basis string check would fail. Fix: use factor 1.0 with costs sized for the 200k band, or also assert that the 500k pool is unchanged.
  • M2. The empty-band gate case isn't shown through the stage.
    • The gate test hand-sets the empty band's boundary_allowance_weight to 7,000. But for an empty band both boundaries fall on the same person, so property_components.py:691-700 would report twice that person's weight, while the walk can legitimately miss by at most once.
    • The gate trusts whatever allowance the stage reports, and never bounds it, though it reads largest_landlord_weight.
    • Fix: count one boundary weight for an empty band, drive the test through allocate_property_rental_income, and cap the allowance at twice the largest weight.
  • M3. The parity reasons are stale. _SPI_PROPERTY_AMOUNT_DRIFT_RATIONALE (tools/build_uk_ledger_compile_parity_signed_differences.py:72-80) still says the 13 rows use "the pinned engine's per-capita GDP index". It's regenerated into all 13 rows of ledger_compile_parity_incumbent_2025_signed_differences.json, whose values are now on the band index.
  • M4. The input-mass switch to the export frame is untested. battery_bindings.py:1480-1487 changed in d904b5ded with no test, so reverting it would leave CI green.
  • M5. The engine-derived exclusion's new signature has no effect. parity_reference._ledger_entries (:211-237) reads only reason and tracking_note. Nothing requires approved_by or approved_on there, and there's no expiry.
  • M6. "k stays 1 on the licensed spine" is still in changelog.d/1106-uk-property-components-stage.added.md:5 and the body's C3 bullet. On c7 the multiplier is 1.166.

Nits

  • spine_swap_signed_differences.json:915 and the input-mass adjudication cite c6x as "the final head". The band-donor fix then changed the landlord set (2,594 against 2,635 records), so the signed figures are pre-fix.
  • d904b5ded, 70f7bfdba and 824ce68ad carry main's uv.lock (pe-uk 2.122.2) against >=2.123.0 in the pyprojects, until f478063d5 re-derives it. So a bisect through them won't build.
  • docs/uk-income-anchors-280.md:57-63 still says the employment, self-employment and pension amounts follow OBR indices; they use the band index.
  • The body labels c6x as 700e2e901; the receipts say 7b20ef73c.

CI and main

@juaristi22

Copy link
Copy Markdown
Collaborator Author

Independent adversarial review (Claude Code) at 4cb36acf2

Originally posted 2026-10-10; re-posted with minor edits.

Static review only. I read the code, the committed registers and experiments/1106-uk-property-income-receipts.md; I ran no tests, builds or calibrations. CI is green on this head.

Verdict: not ready to merge. One code defect contradicts a stated invariant and moves the top-income SPI draws. The head as committed fails uk_target_fit by the PR's own receipts. The final-arm fit was measured on a tree that differs from this one. The decisions held for Max and María are separate from these.

Must fix

  1. The band-donor resample now counts finance costs as income, so "every earlier draw is unchanged" is false.

    • Where: uk_runtime/spi_income.py:1149-1158. _resample_band_donor_leaves cuts its pools on leaf_values @ factors + max(published_total - leaf_values.sum(axis=1), 0) over SPI_INCOME_QRF_OUTPUT_COLUMNS, and that list now ends in property_finance_costs (spi_support.py:116-124).
    • Effect: a donor with restricted finance costs gains about 0.94 × LLIR of "total income" when its leaves are below the published total, and f × LLIR when they are above it.
    • Example: a record whose income leaves come to £185k, with £10k of finance costs, moves into the £200k pool.
      • Each carrier draws by inverse CDF over its pool's cumulative FACT in tape order. So one record entering or leaving a pool changes the donor for every carrier positioned after it, and each of those carriers copies the new donor's whole leaf vector.
      • The outside guard (:1255) checks the same inflated total, so it can't catch this.
    • What is unaffected: the stage-1 QRF draws themselves.
    • Test coverage: the only test of the "drawn last" claim asserts the column order (SPI_INCOME_QRF_OUTPUT_COLUMNS[-1] == "property_finance_costs").
    • Fix: cut the pools on the income leaves only, and still copy the drawn record's finance costs. Then re-measure, which item 3 needs anyway.
  2. The head fails a release-blocking gate by its own evidence.

  3. The final-arm numbers are not of this tree, and the method can't be reproduced from the PR.

    • c6x ran on 7b20ef73c, before the rebase onto a469ebe3d. That rebase brought in Weight UK BRMA assignment by census private-rented households #1087's BRMA change to private renters' LHA and Housing Benefit inputs. "The same content" holds for the PR's own diff, not for the tree that merges.
    • None of 7b20ef73c, 48a277264, 82c744ccc or 692e8b7b3 is in the repo.
    • data/ukds/acceptance/1106-property-income/ is not committed.
    • After item 1, re-run the final arm on the merged tree and say in the body that the scripts live outside the repo.

Should fix

  1. The 13 newly bound amount rows use a flat index, while their count rows move by band.

    • Where: uk_population_targets.json:3658.
    • Every amount band is uprated by ×1.0826 (obr.per_capita.gdp). The paired count rows use taxpayer_count_growth_by_total_income_band, from ×0.979 to ×1.244.
    • Effect: the implied mean profit per landlord between 2023 and 2025 moves from −13.0% (£100k–150k) to +10.6% (£15k–20k), purely from the index pairing.
    • Why it matters: the UK spine follow-ups after #1045: SPI band donors as a mass-conserving support channel, WAS tenure coherence, the residential draw's bound and Table 7 composition, then a national build #1063 rule already applied to employment, self-employment and private pension amounts says a fixed nominal band's amount moves with the band's membership. On employment, the same pairing stays within ±2.5%.
    • c6x fits both the amounts and the counts to 0.2%, so the calibration enforces these per-band means.
    • Size: under total_income_growth_by_total_income_band the 13 rows total £33.60bn, against £31.78bn now. Dividends have the same flat-index pattern but aren't bound against counts the same way.
    • Either switch the index or record why property is the exception.
  2. The pending signatures are not wired so the gates wait for them.

    • property_wealth: the exclusion is in force through gates.json and efrs_parity_known_gaps.json:634-639, but neither carries approved_by, approved_on or expires_on. The input-mass, target-fit and calibration-measure registers all do.
    • The input-mass gate: it reads the raw frame (battery_bindings.py:1481, uk_input_mass_totals(context.frame)), where the WAS total is still present. Only the coverage gate moved to uk_release_export_frame (:259). So input-mass parity checks a column the release no longer ships. Say which path is authoritative.
    • The two stale property count exclusions are still at calibration_measure_exclusions.json:59 and :347. They are why the fit runs on 1,180 targets rather than 1,244.
    • Net-new columns: spine_swap_signed_differences.json has no net_new_column entry for property_rental_income or property_finance_costs. The precedent is other-investment-income-net-new-column. The signed difference the body lists as pending isn't in the diff yet.
  3. The scratch H5 now drops more than property_wealth.

    • Where: measure_simulation.py:388-394. _engine_scratch_frame now drops all of UK_RELEASE_EXPORT_DROPPED_COLUMNS from the calibration engine's input.
    • That includes incapacity_benefit_reported, attendance_allowance_reported, dla_sc_reported, dla_m_reported, pip_m_reported, pip_dl_reported and salary_sacrifice_pre_conversion_pay.
    • Lining calibration up with the release is defensible. But it changes what the solve measures for incapacity and disability benefits, and neither the body nor the changelog says so.
  4. The open top band is not "solved to the individuals' receipts total".

    • Where: property_components.py:743-745 (if mean_at(low) >= top_target_mean: multiplier = low).
    • On the spine, the band mean at k = 1 is already above the target, so the band overshoots its total rather than meeting it. The body says both "solved" (C3) and "pins at 1" (decisions held), and the gate notes repeat "reaches the individuals' Table 2 receipts".
    • Nothing reports a negative top_target_mean, which happens when the bounded bands already exceed the total.
  5. uk_stage_property_components can barely fail, and nothing tests it. test_uk_stage_health.py has no test for _property_components_gate.

    • Receipts below profit: can never be counted, because the stage raises first (property_components.py:973).
    • Band tolerance: twice the largest landlord weight. That scales with one record's weight rather than with the band, so for the smallest PRIS bands (£70k–£100k) it can exceed the band's whole target, and an empty band there would pass.
    • Multiplier: only the cap is checked, not the floor of 1 the top band actually sits on.
    • Not gated: rows_with_profit_above_their_band is recorded but no check reads it.
  6. The reason given for keeping PRIS a diagnostic is stronger than the receipts.

    • The body, the fc7a9484d message and the changelog say every PRIS arm pushed the SPI amounts "out of their bound".
    • C4 (−17.8%) and c4b (−17.7%) stay inside the 25% release bound. They miss the 10% fit band and fail uk_weight_ratio. Only the share-filled arms (+48.2%, +30.9%) breach 25%.
    • The decision may still be right; the stated reason should match the receipts.

Minor

  1. uv.lock:2 drops from revision = 5 to revision = 3 and changes pexpect's ptyprocess marker, so it was written by an older uv than main's. APPROVED_UV_LOCK_SHA256 in us_runtime/worker_identity.py pins those exact bytes. Re-lock with main's uv so "only policyengine-uk moves" is literally true.
  2. Stale text after the revert.
    • "the receipts bind on their own variable" is still in tools/generate_uk_target_references.py:815, is pinned by test_uk_target_references.py:812, and is emitted into target_reference_membership.json:29508. It came from e34412b15, which is why the exact revert missed it.
    • experiments/…:37 still says PRIS targets hold the fit.
    • docs/uk-income-anchors-280.md:366-370 says the FRS side "is not done", but the stage adds the imputed finance costs back.
  3. Some body figures are from runs the head doesn't ship.
    • The allowance-deduction and means-test deltas (−£21m Housing Benefit, −£26m Council Tax Reduction, +£10m Pension Credit, +£3m Universal Credit) are from c3x's count-filled walk. On c6x the deduction is £0.142bn, and the means-test changes weren't re-measured.
    • Part B was measured on policyengine-uk 2.100.0 (experiments/…:17), which the body doesn't say.
  4. Question: does receipts ≥ profit still hold at 2025? The stage enforces it in 2024 money, and the engine then uprates property_rental_income and property_income by their own indices. If 2.123.0 gives receipts the smaller index, every row floored at profit (including the whole open band at k = 1) goes below profit at 2025. I couldn't check this: only 2.122.2 is in the local env.
  5. Nits.
    • The TAX_CRED check accepts a tape with no restricted costs: the median is None (spi_income.py:1549). It fails closed later, in property_components.
    • mapped_build_period: 2024 is declared but never checked against the frame's period.
    • UK_PROPERTY_COMPONENTS_NONNEGATIVE_OUTPUT_COLUMNS is unused and disagrees with the manifest's nonnegative_outputs.

Interaction with #1198 and #1167

Checked and fine:

  • The 13 rows: they bind at £31.78bn (×1.0826 on £29.36bn), the incumbent's £55.78bn is exactly 1.9×, and active references go from 1,231 to 1,244.
  • The PRIS revert: fc7a9484d is an exact revert, and the vendored PRIS has 44 rows.
  • The fixture is synthetic: spi_donor.csv changes only by two formula-filled columns.
  • The stage's FRS draw: person-keyed incidence by decile, a regime-gated QRF, a non-negative add-back to the same person, losses left at zero, and sub-letting excluded.
  • The walk: ranks by profit with person_id breaking ties, is monotone, and floors receipts at profit.
  • property_wealth: dropped at both engine boundaries, with the three components still required.

@juaristi22
juaristi22 force-pushed the uk-property-income-1106 branch from adafe6b to 59bf19a Compare October 12, 2026 03:05
juaristi22 added a commit that referenced this pull request Oct 12, 2026
- Band donors: the pools are cut on the income leaves alone, so a record's
  restricted finance costs never move it between total-income bands; the
  costs travel with the drawn record (review item 1, with a test).
- property_components: each bounded band records its rounding allowance,
  the weight of the person straddling each of its boundaries; the top band
  records whether its multiplier sits at the floor of 1, the floor mean and
  the overshoot; the stage refuses a frame off the declared build period;
  the unused nonnegative-columns constant is gone (items 7, 8, 14).
- uk_stage_property_components holds each band to its own allowance, fails
  any landlord banded below their profit and reports a multiplier at the
  floor; five gate tests join (item 8). The gate-battery and spine part
  digests move with gates.json.
- The input-mass gate compares the release surface, the export frame, on
  both paths (item 5).
- Wording: the top band is solved where the target mean is reachable and
  otherwise held at 1; the PRIS arms missed the 10% fit band, two breached
  the release bound, and uk_weight_ratio failed; the scratch H5's drop list
  is spelled out (items 6, 7, 9).
- The lock is re-derived from main's with uv 0.13.0 (revision 5) and
  policyengine-uk 2.123.0, and the approved lock pin moves (item 10).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Oct 12, 2026
The 13 SPI Table 3.7 net property-income amount rows move from the flat
per-capita GDP index to hmrc.itl_2026.total_income_growth_by_total_income_band,
the index the paired count rows' growth belongs with and the employment,
self-employment and private pension amount rows already use (María's ruling
of 2026-10-11 on the #1145 review, item 4): GBP 33.60bn for 2025 instead of
31.78bn, with factors from 0.977 to 1.236 by band. The flat index implied
per-band mean profit moves of -13% to +11% against the count rows. The
generator's rationale, the docs and the receipts note say PRIS stays a
diagnostic and describe the FRS-side add-back.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Oct 12, 2026
- M1: the band-donor test uses the stage's factor and costs that the old
  pools would have counted as income, and asserts the 200k and 500k pools
  both; it fails on the old code.
- M2: an empty band records one straddler's weight as its allowance, since
  both boundaries fall between the same two people; the gate caps any
  reported allowance at twice the largest landlord weight; a stage-level
  empty-band test and a cap test join, with a positive-target floor test.
- M3: the compile-parity rationale names the band index; the 2025 receipt
  is regenerated.
- M4: a test holds the input-mass gate to the release surface: a frame
  carrying the spine's property_wealth against a reference that stores it
  passes with no stale exclusion.
- M5: engine-derived exclusions carry approved_by and approved_on, which
  the loader requires for that section; a test covers it.
- M6 and nits: the top band's multiplier is 1.166 on the licensed spine
  once the pools are cut on income leaves; the signed registers cite arm
  c7's figures (the input-mass digest moves); the docs list the four amount
  families on the band index; the notes read "within about 2.5%".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Oct 12, 2026
Taken in #1145 at María's direction, #1167 having merged.

- Hardening: the evidence sanitiser drops donor_min and donor_max beside
  donor_floor and keeps only the spine file's name; the committed outputs
  and scripts carry no machine-local paths (the scripts read the inputs
  root from PE).
- Minor 1: an ambiguous or duplicate vendored row is a data fault and
  fails closed as a plain ValueError; only a missing year is recorded as a
  report-only diagnostic being unavailable.
- Minor 2: the recomposition receipt records each parent's uprating
  residue, the parent's factor against each netted part's, weighted; not
  corrected.
- Nits: the consumption-basis gate holds each bridged column's recorded
  factor to after / base; the uncarried_spend and donor_uprating docstrings
  and the obr.fuel_duties_cars and 04.5 binding notes say what the rows
  are now (measured, not bound); the evidence README says the per-division
  movement between runs is redraw noise around the uniform bridge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@juaristi22

Copy link
Copy Markdown
Collaborator Author

Thanks for the re-review. All six minor items and the nits are addressed at 59bf19a1e, rebased onto main after #1167 merged. Arm c9 re-measures the rebased head (below); the stage's design-weight receipt is identical to arm c7's, so the registers' c7 figures stand (the one code change, M2's single straddler for an empty band, touches only a receipt field, and no band on this spine is empty).

Minor

  • M1. The test now uses the stage's factor of 1.0 and costs of 200,000, which the old pools would have counted as income and lifted every sub-200k record into the 200k pool. It asserts the 200k pool holds the 260k records alone, the 500k pool the 620k records alone, and the drawn record's cost is copied. On the old code it fails on the pool counts, not only on the pool_basis string.
  • M2. The stage counts one straddler for an empty band: both boundaries fall between the same two people, and the walk can miss the band by at most one of them. The gate caps any reported allowance at twice the largest landlord weight before judging the error. Two tests: an empty-band case driven through allocate_property_rental_income (a band whose share is under one landlord's weight, missed whole, with an allowance of one straddler that covers the miss and a passing gate), and a gate case where a reported allowance of 30,000 is capped at 19,000 and the band fails.
  • M3. _SPI_PROPERTY_AMOUNT_DRIFT_RATIONALE names the band index, and the 13 rows of the 2025 incumbent receipt are regenerated with it.
  • M4. test_input_mass_parity_compares_the_release_surface: a frame carrying the spine's property_wealth and a reference storing it within tolerance. On the raw frame the signed exclusion would be stale and the gate would fail; on the export frame the column is absent, the exclusion is in force and the gate passes with no stale entry. Reverting the switch fails the test.
  • M5. The engine-derived section is a permanent adjudication, so EfrsParityKnownGap now carries approved_by and approved_on, the loader requires both for that section (ISO date checked) and refuses an unsigned entry, and known gaps carry neither; test_engine_derived_exclusions_require_a_signature covers it. No expiry, by design: the engine owns the column.
  • M6. The changelog and the body now say the multiplier is 1.166 on the licensed spine once the pools are cut on income leaves, and sat at the floor before that fix.

Nits

  • The spine-swap entry and the input-mass adjudication cite arm c7: 9,541 of 145,292 person rows carry receipts and 3,442 carry finance costs (shares 0.0657 and 0.0237). The input-mass digest and its mirrors moved with the text.
  • The lock: the git checkout origin/main -- uv.lock before the re-lock had left main's lock staged, so three commits carried policyengine-uk 2.122.2 against a 2.123.0 floor. The branch is rewritten so every commit carries the re-derived lock (revision 5, 2.123.0); the tree is unchanged.
  • docs/uk-income-anchors-280.md now lists the four amount families on the band index and the two on engine indices.
  • The body labels c6x as 7b20ef73c, now 700e2e901.
  • Item 4's "within 2.5%" now reads "within about 2.5%, the £1,000,000 and over band at +2.51%".

Main. Rebased onto main d648c976c after #1167 merged. Main had re-pinned the Chronicle feed to 1ee7dfe, so the PRIS resource is re-vendored and the references, membership and compile-parity receipts regenerated against it in one commit at the head (1,245 active references: main's 1,232 plus the 13 property amounts); the intermediate commits keep the surfaces they were measured with. The expiring measure exclusion of 2026-10-15 is main's and is noted for the next build. gift_aid and charitable_investment_gifts in the pool total predate this PR and stay as they are.

Measurement on the rebased head (experiments/1106-uk-property-income-receipts.md, section C10). Arm c8 rebuilt the spine on a33079775: 37 of 37 spine gates pass (the four new ones are #1167's LCFS gates). The national step then stopped on main's native-evidence reader, which charges run manifests the schema's per-item budget (microcosm#1187, fixed by #1188, open); every national build after a spine built on current main fails the same way, and this PR does not touch it. Arm c9 calibrated that spine with #1188's commit overlaid for measurement only: loss 0.00644, 98.5% of 1,177 targets within 10% (the set moved with #1167's measure exclusions), ESS 5,203, 7 of 7 calibrated-seam gates; the SPI property amounts fit to 0.3% and the counts to 0.2%; the West Midlands cell sits at +24.2%. Against PRIS: receipts £49.54bn (−0.5%), finance costs £9.28bn (−16%), 2.78m landlords. At 2025: relief £1.77bn for 1.03m landlords, £1.38bn carried forward, income tax £326.01bn against the control's £327.05bn. A national build or the certifier on this head needs #1188 merged first.

#1167's re-review items, taken here at María's direction.

  • Hardening: the sanitiser drops donor_min and donor_max beside donor_floor and keeps only the spine file's name; the committed outputs and scripts carry no machine-local paths (the scripts read the inputs root from PE).
  • Minor 1: an ambiguous or duplicate vendored row now fails closed as a plain ValueError; only a missing year is recorded as unavailable. The missing-year fallback for the litres audit stays report-only, as the block is, and the receipt names it.
  • Minor 2: the recomposition receipt records each parent's uprating residue (parent factor against each netted part's factor, weighted), not corrected; rescaling the parts before netting changes the totals and is a ruling for María.
  • Minor 3: UK consumption-tax levels: open items after #1167 #1207's acceptance sentence is the issue's text. Proposed wording: "R1 is accepted when the bound row sits within 3% of design at the final weights (it reads +2.3% at design, so a fit moves levelled road fuel by about 2.2%), and the levelled fuel moves no further than that."
  • Nits: the consumption-basis gate holds each bridged column's recorded factor to after / base; the uncarried_spend and donor_uprating docstrings and the obr.fuel_duties_cars and 04.5 binding notes are current; the evidence README says the per-division movement between runs is redraw noise around the uniform −0.62% bridge.

juaristi22 and others added 7 commits October 12, 2026 09:54
HMRC's Property Rental Income Statistics are receipts before allowable
expenses; SPI Table 3.7 net income from property is landlords' profit
after allowable expenses and before residential finance costs, the
concept property_income holds (policyengine-uk#2172 now documents it so).
The incumbent's x1.9 scaling (uk-data#311) set the first against the
second, so the 13 amount rows were signed out pending a ruling. María
ruled on 2026-10-05 to bind each concept on its own variable.

- The signed exclusion property_income_undercount_adjustment_missing is
  retired, the hmrc_spi family outcome becomes active_strict_band_pins
  with a rationale for the net concept, and the contract note replaces
  the x1.9 sentence. The uprating index stays obr.per_capita.gdp.
- Regenerated from feed 825406f: 1,244 active references on #1121's base (13 added,
  none changed), 15 signed out; membership candidates 1,253 -> 1,265.
- Both national compile-parity receipts gain the 13 rows. A property-
  specific reason precedes the generic SPI drift reason, which blamed
  the fixture's one-year projection: the 2025 values are £31.78bn
  against the fixture's £55.78bn (1.9 x £29.36bn).
- Tests: the reference counts, the inverted property test (13 rows on
  the per-capita GDP hold, no value operation), the membership outcome
  and signed-rationale lists; the income-anchors note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine-uk defines property_wealth as residential_property_value
(main_residence_value + other_residential_property_value) plus
non_residential_property_value. The WAS stage saved the survey's own
total, which also counts owned land and the overseas and other-property
remainder; a persisted copy overrides the engine's sum, and the variable
has no uprating index, so the published aa31bdf6 year files hold it flat
from 2026 to 2030 while the sum grows 13% (the uk-data#543 defect).

- UK_RELEASE_EXPORT_DROPPED_COLUMNS gains household.property_wealth, and
  the measure resolver's scratch H5 applies the same drops in #1115's
  _engine_scratch_frame, beside the alias codes it already leaves out;
  engine_scratch_dropped_columns in the receipt names them. The spine
  keeps the column for the WAS stage's identity gate and donor checks.
- household.property_wealth is a reviewed export exclusion (constant and
  gates.json), and the coverage gate's two coverage halves read the frame
  the release writes, where the raw calibrated frame would report it as a
  stale exclusion; the build-state half keeps reading the spine (#1115).
- The coverage ledger gets a declared engine_derived_exclusions section,
  separate from the evidence-derived known gaps; the builder refuses
  drift, overlap with the gaps, and any column that is not a formula-
  owned override the reference persists. Manifest: 144 required, 1
  reviewed exclusion; the three components stay required.
- Tests: the scratch H5 and receipt, the export columns, the export-
  frame coverage binding, the household spine read for nonnegativity,
  the ledger and manifest, and an engine test that the engine's total is
  the component sum in 2024 and 2025 and that a persisted total would
  override it.

The gates.json change moves the release contract's UK gate-battery pins
(the full-manifest policy, manifest and fingerprint digests and the
release_cut part's two digests; microcosm-data contract.py and its test
mirror), re-derived from the live spec as the pin tests do.

The input-mass parity register entry (efrs-post-calibration) waits for
María's signature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Aggregates only. A: the 13 bound SPI amount rows compile to GBP 31.78bn
for 2025 against the fixture's 55.78bn. B: on the A9 candidate at 2025
the engine-derived property_wealth is GBP 7,633.4bn against 7,469.5bn
persisted (+2.2%), household land value 4,697.3bn against 4,595.0bn
(target 4,559.8bn), land value 6,755.9bn against 6,653.6bn (target
7,117.8bn); the saved total sat below the uprated component sum for
45,232 of 63,941 households.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2.123.0 carries PolicyEngine/policyengine-uk#2172: property_rental_income,
property_finance_costs and property_finance_costs_brought_forward, the
s. 274A finance-cost reduction, and a property allowance that no longer
stacks on expenses already netted from profit. The lock's UK side moves
exactly to 2.123.0 (uv lock --upgrade-package policyengine-uk==2.123.0)
and nothing else moves: policyengine-core stays 3.32.19 on the UK side,
which 2.123.0 accepts, and 3.32.5 on the US side (#1086's per-country
pins). The UK extras of microcosm-build, microcosm-data and
microcosm-frame require >=2.123.0. The step from #1121's 2.122.2 also
brings #2160, #2096, #2097 and #2144.

Re-derived with no data change: the uprating pin stamp (values
unchanged), the concept coverage (288 inputs, the three property inputs
uncovered) and the mapping reviewed against 2.123.0 (the property_income
note quotes its new documentation, alignment still approximate; the
liquid-asset reason holds unchanged), the spec's declared dependency and
the H2 fixture's, the coverage manifest's source digests, and
APPROVED_UV_LOCK_SHA256.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SPI tape's LLIR_RESTRICT_AMT_TOT is the "restricted land property
finance cost (tax relief is included in TAX_CRED)" (SPI Public Use Tape
2022-23 documentation): the dwelling-related finance costs ITTOIA 2005
s. 272A no longer deducts and s. 274A relieves as a tax reduction. It
becomes the first-stage output property_finance_costs, drawn last so it
conditions on every income leaf and leaves every earlier draw where it
was; the band donors copy it with the record. It is uprated from 2022 by
the engine's own index for the variable, obr.mortgage_interest.

The donor is refused unless the median TAX_CRED / LLIR_RESTRICT_AMT_TOT
on rows with finance costs lies in [0.195, 0.205] (the 20% relief carried
in TAX_CRED); the stage receipt records the check. FRS-channel rows start
at zero until property_components imputes their landlords.

Lockstep: SPI required columns and source columns in the spec and its
projection, the stage-1 outputs, the stage outputs and non-negativity
list, the graph cells (after the first-stage outputs, the legacy column
order), the export allow-list and gates.json with the gate-battery
digests, the H2 fixture (synthetic LLIR and TAX_CRED on every third
landlord) and the coverage manifest's fixture digests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine-uk 2.123.0 reads two landlord inputs beside property_income:
property_rental_income (receipts before expenses, which decide between
the property allowance and the expenses route) and property_finance_costs
(the year's dwelling-related finance costs, relieved as the s. 274A tax
reduction). A person-grain property_components stage after
regional_property_uprating fills both.

Finance costs on the FRS concept. ROYYR1 is rent after show card K6,
which nets mortgage payments and loan interest. The stage prepares and
rebases the SPI 2022-23 tape exactly as hmrc_spi_income_spine does and
keeps its landlords with profit after restricted finance costs above
zero, the analog of an FRS landlord reporting a profit. Incidence is a
person-keyed uniform against the tape's share in the landlord's decile of
profit after finance costs; the amount is the regime-gated QRF fitted on
the analog landlords with costs, given that profit and age, drawn at a
second person-keyed quantile. The costs are added back to the FRS
landlord's property_income so the engine relieves the interest once; the
mortgage capital the card also nets stays netted. The add-back is
decision 6's recommendation, to be confirmed with Max before merge. The
SPI channel keeps the costs the income stage drew.

Receipts for every landlord, from PRIS 2024-25 vendored from the pinned
feed (hmrc_property_rental_income_facts.json: Tables 1, 2, 6, 8 and 13,
44 rows, typed by uk_runtime.hmrc_property_rental). Landlords on both
channels are ranked by profit and walked top band first into Table 13's
eleven bands at their individuals-basis counts with the CGT Table 3
walk's rule; receipts are linear within a bounded band, never below
profit, and the open top band takes max(100,000, k x profit) with k
solved to the individuals' Table 2 receipts. Receipts less profit are the
deductible expenses. Sub-letting rent keeps equal receipts and no
expenses until Rent a Room is released; landlords without profit carry no
receipts (a recorded residual).

Gate uk_stage_property_components (stage health, release-blocking, in
the spine scope): no receipts below profit, every band above the lowest
within twice the largest landlord weight of its target, and the top
band's multiplier below its cap.

Lockstep: the sources manifest and its projection, two schema branches
and the operation-kind allow-list; the graph roster, kernel registry and
spine tool; the H2 fixture (40 stages, synthetic PRIS facts sized to the
frame the stage reads) and the coverage manifest with its family
coverage contract; the vendoring register and the country package; the
export allow-list, gates.json, the stage-health parameter keys, the
release contract's gate mirrors and its gate-battery digests; the
stage-roster tests; the build-graph doc's stage count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1106)

Three PRIS 2026 targets (tax year 2024-25), per María's ruling of
2026-10-05 that the receipts bind on their own variable:

- hmrc.pris.rental_income.individuals: Table 2's individuals' rental
  income before expenses on property_rental_income, uprated by the
  engine's obr.per_capita.gdp (GBP 49.81bn -> 51.99bn for 2025).
- hmrc.pris.landlord_count_by_rental_income_band: Table 13's eleven
  landlord counts by size of rental income, restated from all tax
  entities to individuals by Table 1's share (2.85m / 2.88m) through
  scaled_by_ratio and held at their 2024-25 levels; banded on
  property_rental_income.
- hmrc.pris.residential_finance_costs: Table 8's residential finance
  costs on property_finance_costs, restated by Table 6's individuals'
  share of allowable expenses (30.03 / 34.75) and uprated by the engine's
  obr.mortgage_interest (GBP 12.82bn -> 12.13bn for 2025).

Table 13's lowest band publishes only its upper edge (up to GBP 10,000),
so the binding declares band_floor 0 and the shared band reader takes a
declared floor for a band that states only an upper edge; a band without
one is still skipped as unreadable.

obr.mortgage_interest joins UK_ENGINE_INDEX_PARAMETERS and the pins are
re-derived at 2.123.0. The contract records the three as new
declarations with no incumbent registry row and adds the PRIS category.
Regenerated: the national references and membership (1,244 -> 1,257
active; hmrc_pris family, 11 fan-out rows), the local references (the
category only) and the 2025 compile-parity receipt (13 ledger-only
rows; the 2023 receipt does not move). Pins moved: active reference
counts, contract target counts (392 -> 395), registry scope (341 ->
344), unmapped declarations (149 -> 152), the engine index count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
juaristi22 and others added 12 commits October 12, 2026 09:55
)

The walk filled each Table 13 band to PRIS's individuals-basis count.
Before calibration the spine carries 2.21m weighted landlords against
PRIS's 2.85m, so the ten bands from GBP 10,000 took 71% of the spine's
landlords where PRIS has 55%, and every profit rank landed in a higher
receipts band than PRIS's distribution implies. The national calibration
then scaled that up: on the C3 arm's calibrated weights, which bind no
receipts, the stage's receipts came to GBP 55.39bn against PRIS's
GBP 49.81bn, with 1.16m landlords at GBP 10,000 to 20,000 against 0.85m
and 0.71m in the lowest band against 1.29m. Binding the PRIS targets (the
C4 arm) then pulled weight across total-income bands: the SPI property
amount at GBP 20,000 to 30,000 of total income went to -17.8% and the
weight-ratio gate failed.

Each band now takes its share of the individuals-basis landlords times
the landlords' weight in the frame. The open top band's target mean is
unchanged (the scale cancels), so k is still solved to the individuals'
receipts per landlord. Evaluated offline on the same calibrated weights
(scripts/walk_variants.py in the measurement folder): receipts
GBP 46.11bn and band counts of 1.19m, 0.90m, 0.35m, 0.16m from the
lowest up, against PRIS's 1.29m, 0.85m, 0.33m, 0.15m; the misallocation
summed over the bands falls from about 1.12m landlords to 0.25m.

The receipt records each band's published count beside its target mass
and the share scale. Declared text moves with it (the operation's walk
and the stage notes in both manifests, the gate notes, the stage's
changelog), and so do the gate-battery digests, the H2 fixture's stage
payload and the coverage manifest's digests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This reverts commit 82c744c. Receipts are a fixed function of profit
rank, so every PRIS count or receipts target measured (all 13 rows, the
13 rows without the lowest band, and the two totals alone, under both
walk rules) moved weight across the SPI's total-income bands: the SPI
property amounts left their bound and `uk_weight_ratio` failed. The
`property_components` stage still vendors PRIS 2024-25 and its receipt
reports the spine against it; the stage gate keeps the band shares.

Active national references return to 1,244. The arms are in
experiments/1106-uk-property-income-receipts.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C3, C6, C7 and C8 of the receipts note: the stage on the licensed spine,
the calibration arms (control, C3, C3 without the stale West Midlands
deferral, C4, C4 without PRIS's lowest band, the share-filled walk with
all 13 PRIS rows and with the two totals only), the engine quantities at
2025, and the decision to keep PRIS a diagnostic. The stage's changelog
fragment says why PRIS binds no target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Arm c6x on 7b20ef7: the share-filled walk with PRIS a diagnostic,
spine and calibration built afresh. All 7 calibrated-seam gates and all
33 spine gates pass with #1121's stale West Midlands deferral dropped at
gate time; receipts at the calibrated weights against PRIS; the engine's
landlord quantities at 2025.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Band donors: the pools are cut on the income leaves alone, so a record's
  restricted finance costs never move it between total-income bands; the
  costs travel with the drawn record (review item 1, with a test).
- property_components: each bounded band records its rounding allowance,
  the weight of the person straddling each of its boundaries; the top band
  records whether its multiplier sits at the floor of 1, the floor mean and
  the overshoot; the stage refuses a frame off the declared build period;
  the unused nonnegative-columns constant is gone (items 7, 8, 14).
- uk_stage_property_components holds each band to its own allowance, fails
  any landlord banded below their profit and reports a multiplier at the
  floor; five gate tests join (item 8). The gate-battery and spine part
  digests move with gates.json.
- The input-mass gate compares the release surface, the export frame, on
  both paths (item 5).
- Wording: the top band is solved where the target mean is reachable and
  otherwise held at 1; the PRIS arms missed the 10% fit band, two breached
  the release bound, and uk_weight_ratio failed; the scratch H5's drop list
  is spelled out (items 6, 7, 9).
- The lock is re-derived from main's with uv 0.13.0 (revision 5) and
  policyengine-uk 2.123.0, and the approved lock pin moves (item 10).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rs (#1106)

María's rulings of 2026-10-11. The microcosm#1095 deferral of the West
Midlands GBP 12,570 to 15,000 income-tax cell is retired: on this head the
cell sits inside the 25% bound (+23.1% on C3, +23.4% on the final arm) and
the gate failed it as stale. Signed: the property_wealth entry in the
input-mass parity register (the release leaves the column to the engine;
expires 2027-04-11), the two net-new landlord columns in the spine-swap
register, and the engine-derived coverage exclusion's approval. The
input-mass evidence digest and its mirrors move; the battery test clock
moves to 2026-10-11.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The 13 SPI Table 3.7 net property-income amount rows move from the flat
per-capita GDP index to hmrc.itl_2026.total_income_growth_by_total_income_band,
the index the paired count rows' growth belongs with and the employment,
self-employment and private pension amount rows already use (María's ruling
of 2026-10-11 on the #1145 review, item 4): GBP 33.60bn for 2025 instead of
31.78bn, with factors from 0.977 to 1.236 by band. The flat index implied
per-band mean profit moves of -13% to +11% against the count rows. The
generator's rationale, the docs and the receipts note say PRIS stays a
diagnostic and describe the FRS-side add-back.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Arm c7 on f478063, spine and calibration with no measurement-only edit:
7 of 7 calibrated-seam and 33 of 33 spine gates; the SPI property amounts
on the band index fit to 0.4%; receipts against PRIS at the calibrated
weights within 0.5%; the band-donor fix moves the SPI channel's finance
costs and lifts the top band's multiplier off its floor; the engine's
landlord quantities at 2025.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- M1: the band-donor test uses the stage's factor and costs that the old
  pools would have counted as income, and asserts the 200k and 500k pools
  both; it fails on the old code.
- M2: an empty band records one straddler's weight as its allowance, since
  both boundaries fall between the same two people; the gate caps any
  reported allowance at twice the largest landlord weight; a stage-level
  empty-band test and a cap test join, with a positive-target floor test.
- M3: the compile-parity rationale names the band index; the 2025 receipt
  is regenerated.
- M4: a test holds the input-mass gate to the release surface: a frame
  carrying the spine's property_wealth against a reference that stores it
  passes with no stale exclusion.
- M5: engine-derived exclusions carry approved_by and approved_on, which
  the loader requires for that section; a test covers it.
- M6 and nits: the top band's multiplier is 1.166 on the licensed spine
  once the pools are cut on income leaves; the signed registers cite arm
  c7's figures (the input-mass digest moves); the docs list the four amount
  families on the band index; the notes read "within about 2.5%".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Taken in #1145 at María's direction, #1167 having merged.

- Hardening: the evidence sanitiser drops donor_min and donor_max beside
  donor_floor and keeps only the spine file's name; the committed outputs
  and scripts carry no machine-local paths (the scripts read the inputs
  root from PE).
- Minor 1: an ambiguous or duplicate vendored row is a data fault and
  fails closed as a plain ValueError; only a missing year is recorded as a
  report-only diagnostic being unavailable.
- Minor 2: the recomposition receipt records each parent's uprating
  residue, the parent's factor against each netted part's, weighted; not
  corrected.
- Nits: the consumption-basis gate holds each bridged column's recorded
  factor to after / base; the uncarried_spend and donor_uprating docstrings
  and the obr.fuel_duties_cars and 04.5 binding notes say what the rows
  are now (measured, not bound); the evidence README says the per-division
  movement between runs is redraw noise around the uniform bridge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Main re-pinned the Chronicle feed to 1ee7dfe (352,549 rows) and merged
#1167, #1087 and #1099 among 87 commits. On the merged tree: the PRIS
resource is re-vendored from the new pin (44 rows unchanged), the target
references, membership and both national compile-parity receipts are
regenerated against it (1,245 active references: main's 1,232 plus the 13
SPI property amounts), the release input coverage manifest and the H2
fixture are re-derived, the gate-battery and certification part digests
move with the merged gates.json, and the count pins follow. The commits
below this one keep the surfaces they were measured with.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Arm c8 rebuilt the spine on the rebased head: 37 of 37 spine gates pass, and
the national step then stopped on main's native-evidence reader defect
(microcosm#1187). Arm c9 calibrated that spine with #1188's commit overlaid
for measurement only: loss 0.00644, 98.5% of 1,177 targets within 10%, 7 of 7
calibrated-seam gates, the SPI property amounts within 0.3%, and the stage's
design-weight receipt identical to arm c7's.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@juaristi22

Copy link
Copy Markdown
Collaborator Author

Re-review (Claude Code) at 59bf19a1e

Static review of the rebase onto d648c976c (#1167's merge) and the four commits after it, against the previous re-review and the reply. I ran nothing. CI is green on this head, but that run used d648c976c, not current main.

Verdict.

Before merge

  1. It conflicts with main (GitHub: CONFLICTING).
  2. The UK domestic electricity and gas: record why ONS 04.5.1/04.5.2 is preferred over DESNZ × QEP, reconcile the two, and declare the price basis PE-UK should project from #1113 evidence digests are stale.
    • docs/evidence/uk-consumption-taxes-1113/receipts.json hashes 15 files in files_sha256.
    • 472222d50 edited 11 of them (the README, four outputs, six scripts) without updating the hashes, so 11 of 15 no longer match.
    • Nothing tests the file. Regenerate the hashes, and a small test would keep them honest.
  3. The West Midlands register now collides with UK: port the #1095 fixes that need no new policyengine-uk release #1200.

Minor

  1. UK domestic electricity and gas: record why ONS 04.5.1/04.5.2 is preferred over DESNZ × QEP, reconcile the two, and declare the price basis PE-UK should project from #1113 changes riding here. 472222d50 is scoped and tagged, and the body has a section for it. But there's no changelog.d fragment for what it changes in main's stage: a duplicate vendored row now hard-fails, there's a new residue receipt field, and there's a new gate factor check. Add a fragment and cross-link on Set UK road-fuel and energy levels in the LCFS stage on a calendar-2024 basis (#1113) #1167 or UK domestic electricity and gas: record why ONS 04.5.1/04.5.2 is preferred over DESNZ × QEP, reconcile the two, and declare the price basis PE-UK should project from #1113 so those reviewers see it.
  2. The intermediate commits don't match their own test pins.
    • About 17 rebased commits carry surfaces that disagree with their own pins. For example, target_references has 1,242 against pins of 1,244/1,245, and the membership sits on feed 825406f against the 1ee7dfe pin.
    • If this merges with a merge commit, as Set UK road-fuel and energy levels in the LCFS stage on a calendar-2024 basis (#1113) #1167 did, they land in first-parent history and break bisect. Squash-merging avoids that.
    • On the lock: "revision 5 / 2.123.0 in every commit" holds from a06e539b4 on. Earlier commits carry consistent older locks (floor, lock and approved hash agree in each), so the old nit is fixed, but the claim isn't literal.
  3. The basis-gate factor check is close to tautological.
    • stage_health.py:2718-2741 checks factor == after / base, which donor_uprating.py:139-142 writes by construction.
    • It never checks base == 0.75 · before + 0.25 · following, which is the bridge itself.
    • It silently skips entries without numbers.
  4. Nits.
    • M1: with every factor at 1.0, the test no longer pins "costs copied at their own factor". A non-unit cost factor with costs sized for the 200k band would keep both properties.
    • M2: the empty-band assertion is conditional (test_uk_property_components.py:386-388); assert walk_records == 0 would make it unconditional.
    • lcfs_consumption.py:771: a total ≤ 0 with a cars row present is still recorded as "unavailable".
    • Stale SHAs: spine_swap_signed_differences.json:916 and the note's C9 cite f478063d5, which is no longer on the branch. The body's "now 700e2e901" is now 5400506c7.

Previous items

Item Status
Rebase, #1145 logic Faithful
Rebase, #1167 logic Faithful
Surfaces at head Fixed
M1 Fixed: on the old pool formula the sub-200k records land in the 200k pool and the count assertion fails
M2 Fixed: the stage counts one straddler for an empty band, the gate caps at twice the largest weight, and reverting fails the stage-driven test
M3 Fixed: all 13 reasons name the band index
M4 Fixed: on the raw frame the exclusion is stale and the gate fails, so reverting fails the test
M5 Fixed: non-empty approved_by and an ISO approved_on are required (parity_reference.py:220-262)
M6 Fixed
#1167 items Fixed: the sanitiser drops donor_min and donor_max, scripts read PE with no machine paths, ambiguous rows fail closed, and the residue is recorded. The basis check is only partly fixed (item 6).

Sequencing

@juaristi22
juaristi22 force-pushed the uk-property-income-1106 branch from 59bf19a to 63943a7 Compare October 12, 2026 09:45
@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Thanks María, reviewed at 63943a76b. Verdict: the tax concepts are right and the code and tests hold up, so nothing in the code blocks this from my side. Two things should be on the record before Max rules on the add-back, and one is a small new residual.

  1. The HBAI effect of the add-back needs a size and a fix, not just a mention (uk_runtime/property_components.py:1004).

    • Your note to Max says HBAI net income rises by the interest less the extra tax. What's missing is how much at 2025, and that isn't in the description's "Decisions held" or in the receipts note.
    • In policyengine-uk 2.123.0, hbai_household_net_income, market_income and household_market_income all add property_income, which is profit before finance costs.
    • HBAI takes FRS property income as reported, after the show card K6 items. With the add-back, FRS landlords' HBAI income rises by the drawn interest (£2.37bn on the FRS channel at design weights in 2024 prices, receipts note C10) less the extra tax. Poverty and inequality runs on this data then drift from the published HBAI basis.
    • The SPI channel has the same gap, now that property_finance_costs is set there.
    • Suggest: (a) add the HBAI effect at 2025 to "Decisions held" and the receipts note (the c9 engine run can report hbai_household_net_income against the control); (b) a policyengine-uk follow-up so HBAI and market income read property_income_after_finance_costs, as the means tests already do. That makes the add-back neutral for HBAI and fixes the SPI side too.
    • On the ruling itself I'd keep the add-back: relieving the interest at 20% rather than at the landlord's marginal rate is what s. 274A does, and (b) removes its main side effect.
  2. Furnished holiday lettings from 2025-26 (uk_runtime/spi_support.py:110).

    • The tape is 2022-23, when furnished holiday lettings could still deduct finance costs in full. Their SPI profit is after interest, and LLIR_RESTRICT_AMT_TOT doesn't include their costs.
    • The FHL regime was abolished from 6 April 2025, so at 2025 those landlords' interest falls under s. 272A: their property_income should be higher and the interest should sit in property_finance_costs.
    • Probably small, but the direction is known (both the 2025 profit and the costs are understated). Worth a line under "Known residuals" and in experiments/1106-uk-property-income-receipts.md.
  3. Minor: capital netting also shifts the finance-cost draw, not only the profit level (uk_runtime/property_components.py:994-997).

    • The model is fitted on the tape's profit after interest, but FRS landlords are queried with ROYYR1, which is after interest and capital repayments.
    • So a repayment-mortgage landlord is matched to tape landlords with a lower profit after interest than their own, and takes that cell's incidence and amounts.
    • Most buy-to-let lending is interest-only, so this is likely small. One sentence beside the existing "FRS mortgage capital stays netted" residual would cover it.

What I checked

  • The diff d1fda720b..63943a76b, mainly property_components.py, hmrc_property_rental.py, the SPI income and support changes, the uk_stage_property_components gate, and the target references and retired exclusion. The one US file (us_runtime/worker_identity.py) only re-pins the approved uv.lock hash for the engine bump.
  • The concepts against the law and the engine: SPI Table 3.7 property income is profit before residential finance costs (s. 272A, in full from 2020-21), so binding the 13 rows unscaled and retiring the x1.9 receipts scaling is right. LLIR_RESTRICT_AMT_TOT as property_finance_costs, with the s. 274A 20% reduction in TAX_CRED, matches the median-ratio guard (spi_income.py:1540).
  • The policyengine-uk 2.123.0 wheel: taxable_property_income no longer deducts the costs, property_finance_cost_relief limits the reduction as s. 274AA does, the property allowance and the reduction are exclusive, and the legacy means tests read property_income_after_finance_costs. For an FRS landlord that is reported ROYYR1 again after the add-back, which test_the_stage_adds_frs_costs_back_and_gives_every_landlord_receipts pins.
  • Engine-free tests in a fresh uv venv at 63943a76b, on the touched packages rather than the full CI job:
    • microcosm-build UK and shared country-spec, plus microcosm-data: 5,366 passed, 31 skipped;
    • microcosm-frame and microcosm-graph: 1,985 passed, 60 skipped. Two Orrery tests fail only because Node isn't installed here; engine-free is green on CI on both Pythons.
  • CI on this head: everything green except engine-us (3.14), still pending.

Good to merge from my side once Max has ruled, #1188 is in, and 1 and 2 are recorded.

@juaristi22
juaristi22 merged commit 51a2637 into main Oct 12, 2026
10 checks passed
@juaristi22

Copy link
Copy Markdown
Collaborator Author

Thanks Vahid. Nothing here changes code, so no new CI round; items 1 and 2 are recorded in the description now and go into the receipts note with the follow-up PR that also carries the control arm on the current base.

  1. HBAI size at 2025, measured on the c9 candidate at its calibrated weights by reversing the add-back (FRS landlords' profit as reported, no finance costs): HBAI household net income −£2.95bn (0.20% of £1,508.5bn), household market income −£3.29bn, income tax −£0.34bn, the finance-cost relief £1.77bn to £1.11bn, the BHC poverty rate +0.02 points and the AHC rate +0.04 points, the Gini of equivalised HBAI income −0.0002. Recorded under "Decisions held". The policyengine-uk follow-up (HBAI and the two market-income variables read property_income_after_finance_costs) is filed as HBAI and market income should read property income after finance costs policyengine-uk#2390; the SPI channel's £6.8bn of finance costs sit on the same basis, so it fixes both sides.
  2. Furnished holiday lettings from 2025-26: added under "Known residuals" with the direction (both the 2025 profit and the costs understated by their interest).
  3. Capital netting and the draw: one sentence added beside the mortgage-capital residual.

The receipts note gets the same three items and the control arm on the current base in the follow-up.

juaristi22 added a commit that referenced this pull request Oct 12, 2026
The branch now sits on 51a2637. main brought the landlord work of
microcosm#1145: the 13 SPI Table 3.7 property-income amounts bound unscaled
and uprated by the band index, the property components stage, the PRIS
resource, and policyengine-uk 2.123.0; and #1188 and #1211. Where the two
sides touched one hunk the rebase kept the branch's side; this commit puts
both sides in and regenerates what depends on them, on the 942a1fa feed.

- The SPI property-income amount target takes main's note and band index
  and keeps this branch's taxpayer filter (income_tax > 0), as every other
  SPI row does here (decision "taxpayer basis everywhere").
- National references: 1,281 active, the branch's 1,268 plus the 13 property
  amounts.
- The vendor selections keep both sides' resources;
  hmrc_property_rental_income_facts.json is vendored again under the 942a1fa
  feed identity, and the UK spine parity fixture follows it.
- The two national parity receipts and the reference membership are
  regenerated; the local references, the local census and the local parity
  receipt come out byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Oct 12, 2026
…note

#1145 merged with these items in its description only; its author's reply
said the receipts note would take them in a follow-up. They are: the HBAI
size of the FRS add-back at 2025 with the engine follow-up
(PolicyEngine/policyengine-uk#2390), furnished holiday lettings from
2025-26, and the effect of capital netting on the finance-cost draw. The
figures are the description's. No code changes; the control arm on the
current base is still owed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Oct 12, 2026
The branch is rebased again, on main after #1145, so both receipts are run
again. Before is 51a2637 with only the receipt tool, on the 1ee7dfe feed.
After is cd39d03 on the 942a1fa feed, on a clean tree.

Bound national rows go from 1,177 to 1,213: 36 added, none removed, 552
moved. main's 13 SPI property-income amounts are on both sides at the same
values, so both counts are 13 higher than before the rebase; the rows this
branch adds and moves are the same. Local cells go from 20,885 to 21,804:
919 added, none removed, 18,811 moved.

The before receipt's overlay is now in the tree as before-overlay.diff, and
each implied cell carries its household share and share ratio.

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

UK: model landlord property income on the taxable (net of expenses) concept, not rent received

2 participants