Repository navigation
One consistent UK target surface: enforced cross-grain reconciliation, declared uprating holds, nation household controls and the 942a1fa re-pin (#1123 PR A) - #1198
Conversation
Independent adversarial review (Claude Code) at
|
…s hold (#1123) Engine CI on #1198: the US pool tool's run config pins the US spec digest, which #104's solve.py edit moved (as us-f0-coverage.json already records), and the diesel per-head donor factor moves 0.036% with ONS's revised mid-2024 population in the 942a1fa feed, past its 5e-4 pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…must-fix 4) Nothing enforced the signatures #1198 asks for: all 15 holds and the small-cell rule were approved_by null and proposed, and the compile honoured them anyway. A hold is now in force only once it carries approved_by and approved_on and is no longer proposed; the holds check refuses the rows of an unsigned entry as it refuses undeclared ones, and the loader requires tracking and expires_on on every entry. A small-cell rule nobody signed defers nothing (its cells stay bound), and the runtime refuses a membership that defers under an unsigned rule. test_every_committed_hold_is_signed fails until the register is signed, so the open decision cannot be merged around. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Must-fix 2. The 2% fan-out gate read the reconciliation receipt, where the
single-winner rule had already put the local-authority cells on the
constituency total, so that grain's leg always read one. The gate now reads
the cells before any reconciliation, at every grain, and tests source
agreement: the bands' total over the cells' total before either side's
uprating index. The applied factor and the index gap are receipted beside
it. On the pinned feed the source factors are 0.999 to 1.007 at
constituency grain and 1.006 to 1.014 at local-authority grain; the applied
employment-count factors of 1.0192 and 1.0211 are 1.44% index choice
(band-by-band growth on the bands, all-ranges growth on the cells).
Must-fix 3. The 24 national band bridges had no bound and one shared reason
("about 0.5 percent short"). Each now declares max_factor_shift (its
measured gap plus half a point) and its own gap: the shortfall is the
taxpayers Table 3.11's UK row carries in no region, concentrated in the
lowest band (2.7% of taxpayers, 5.7% of income tax at 12,570 to 15,000).
Should-fix 6. A declared subset relation names its part and whole and is
tested on the compiled values in both loaders (a local part at each grain
on its own). The national-only loader runs the coverage check. An applier
that moved nothing stamps uprating_hold and stays a hold: the Welsh,
Scottish and Northern Irish tenure cells are now declared. A row with no
readable fact period is refused instead of passing as not held.
Minor 9. A roll-forward base resolves exactly one cell, or one for each
geography it names, and its numerator and denominator must be one series at
two periods.
Minor 10. A SPREE tenure share that moves by more than 10% in two years is
a stock reclassification (Rushmoor's social stock falls 22%): that
authority's census shares are held. The docstring says what the drift
carries: social renting against everything else.
The receipt tool writes the fan-out legs per grain, the sum bridges and the
subset relations, and names its inputs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…review) Must-fix 1, the part this repository can do: the UC area extract carries no Payment Indicator, so its cells count every household on UC (6.73m across Great Britain) while the national rows count paid claims (6.20m). The selector notes said the bridge "reconciles like with like"; they, the bridge and the partition now state the gap, the single GB factor (0.921) and the child bands' resulting spread against the national paid rows. The fix is a paid-claims area series from Chronicle and a re-pin. Should-fix 7: a proposed rule (unsigned, so not in force) would defer the 192 higher rate area cells of 5,000 or fewer, where HMRC's rounding to the thousand is 10% or more of the cell. Stale text: the #104 fragment (the census reports a zero target's weight, it does not enforce one), the DfI count (seventeen values, not eleven), the A15 and household-bridge paragraphs of the cross-grain doc, and the census notes that said every council-tax cell binds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…review) An unsigned rule defers nothing, so the 16 council-tax band cells below the floor are bound again: 21,804 active local references, 342 signed deferrals. Rushmoor's four tenure cells hold their census shares. National references, the release manifest and the uk-data parity register regenerate unchanged. The council-tax and surface pins now follow the rule's signature state, so signing it moves them with the regenerated references rather than breaking them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… its commit (#1198 review) A receipt of a draft surface may need a local overlay (a hold register signed for the run, since an unsigned hold refuses the compile). It now records code_modified_paths beside code_head rather than passing as the commit's own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w, question 11) The tax_band bindings were tested against fakes only. An engine test now pins the classification the national Table 2.2 rows and the higher rate area cells rely on, for rest-of-UK and Scottish taxpayers, and that the area metric counts higher rate taxpayers only. It also pins the gap the review asked about: a Scottish taxpayer with 40,000 of earnings and 5,000 of dividends is HIGHER on the engine, which compares total income with the Scottish thresholds, where HMRC applies Scottish rates to non-savings, non-dividend income and counts no higher rate taxpayer. The same person in England is BASIC. Only Scotland's two national rows carry it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…review, should-fix 5) The receipts behind the PR's value moves were not in the tree, and the "before" one postdated this branch's own national reconciliation, so the band-bridge and CGT regional moves were missing from the count. The before receipt is now the merge base aca40a6 with only the receipt tool, on the feed it pins; the after receipt is 5ad4816. Bound national rows go from 1,167 to 1,203: 36 added, none removed, 552 moved (360 SPI regional and 24 CGT regional cells by reconciliation; 168 by the re-pin and the household controls). Local cells go from 20,885 to 21,804: 919 added, none removed, 18,374 moved. docs/evidence/uk-target-surface-1123 holds the row-level national diff, the local diff by target and grain, and the after surface's reconciliation (the band bridges with their bounds, each fan-out bridge at both grains, every group's factors, the subset relations and the holds), with the script that extracts them. The after receipt was run over a register signed for the run only, which it records. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reply to the adversarial review (Claude Code), at
|
Re-review (Claude Code) at
|
| # | Status | Note |
|---|---|---|
| 1 UC by area | Text fixed; substance open | No "like with like" remains. The paid-claims extract is pending in Chronicle. |
| 2 Fan-out gate | Partly fixed; see A | It now reads the pre-reconciliation cells at each grain (uk_runtime/ledger_targets.py:2621-2694). The source factors reproduce: 0.9992–1.0066 constituency, 1.0058–1.0144 LA. Only the source factor is gated. |
| 3 Band bridges | Fixed | Bounds are required and enforced (national_reconciliation.py:82, :247), and all 24 factors sit inside them. Reasons match the receipt to 0.1 pt. The West Midlands re-measure is still owed. |
| 4 Signatures | Fixed | The loader requires tracking and expires_on. An unsigned hold or small-cell rule is out of force, and the 16 cells are bound again. The head can't compile a UK surface until the holds are signed, which is intended. See C and D. |
| 5 Receipts | Fixed | The numbers above verify, and the extraction script is correct. See G for provenance. |
| 6 Escape paths | Fixed; see F | Subset relations are checked in both loaders, and the national loader runs coverage. The tenure applier now stamps uprating_hold (264 = 65 × 4 + Rushmoor × 4), and a period that can't be read is refused. |
| 7 Higher-rate small cells | Inert, as stated | 192 cells (155 + 37); the rule is unsigned so it defers nothing. |
| 8 Land | Open, acknowledged | |
| 9 Roll-forward | Fixed | One base cell per geography; operands one series at two periods. |
| 10 SPREE | Fixed | Rushmoor (0.763) is the only share past 10% in two years; the next widest is 6.9%. |
11 Scottish tax_band |
Pinned | test_uk_tax_band_measure.py pins the Scotland-vs-England case. The decision is still yours. |
| 12 Scotland and Wales households | Open (question to María) | |
| 13 Stale text | Fixed, with new spots | See H. |
68a0025d8 is legitimate:
- The diesel factor: it follows this PR's own re-pin of UK mid-2024 population (69,281,437 → 69,256,274). The tolerance is unchanged; only the centre moved.
- The US digest: it comes from this PR's
solve.pyedit, whichus-f0-coverage.jsonalready records.
New
A. The fan-out gate no longer bounds what the cells take.
_assert_uk_fanout_sum_factorsrefuses onlysource_factor.applied_factorand the index gap are receipt-only.- So LA employment counts take ×1.0211 with nothing checking it, and the 1.44-point index gap passes.
- The test that refused an applied 1.03 is gone.
test_uk_fanout_sum_bridges.py:151now asserts that a 5.4% applied factor passes. - Gating source agreement is the right addition, but it replaces the bound instead of joining it. A future ITL re-pin that widens the band-vs-all-ranges gap would move the area targets silently.
- Fix: also bound the index (or applied) factor per grain, measured gap + 0.5 pt as the band bridges do.
B. LA cells take per-region factors up to 1.043 that nothing gates.
- Factors (
reconciliation.json,constituency_over_lagroups):- employment count, South West: 1.0430 (every other region 1.018–1.021);
- employment amount, South West: 1.0294;
- self-employment amount, North East: 1.0294.
- Cause: the single-winner rule scales LA cells to the constituency total region by region. Torbay and the Isles of Scilly have no SPI LA cell, so their share of the South West lands on the other South West authorities.
- Consequence: South West LA targets ask about 2.4% more than the constituency targets for the same households.
- The body's "0.996 to 1.021" and the gate's
applied_factorare UK-level, so this doesn't show. It predates the fix commits. - Fix: drop the absent authorities' constituencies from that leg's parent, or gate per-leg factors.
C. The deliberately failing test stops the engine-free job.
tools/run_engine_free_tests.shruns--maxfail=1 -n 2. Whentest_every_committed_hold_is_signedfails, pytest stops scheduling.- So "red on that one test" means the rest of the engine-free suite goes unverified in CI on every push until the holds are signed.
- Fix: mark it
xfail(strict=True)with the reason, so CI goes red only once it unexpectedly passes. Or move the check into a merge gate.
Minor
- D. "Signed" means "not proposed".
uk_hold_is_signed(uprating_holds.py:162-172) anduk_small_cell_rule_is_signed(ledger_targets.py:882) acceptrejected,withdrawnordraftonce an approver and date are set. Use an allowlist (status == "approved"). - E. The source factor falls back silently to the uprated value when
ledger_value_before_alignmentis missing (_spec_value_before_alignment,ledger_targets.py:2617; alsonational_reconciliation.py:407). That would mix periods on one side of the gate. It should refuse instead. - F. The subset checks are thin.
- They compare aggregate totals only.
age_0_9_by_region ≤ uk_totalis trivially true (ratio 0.106), while the stated constraint (all bands ≤ total less 90+) isn't tested.max_excessis unbounded at runtime; only the test caps it.- A relation with an unbound side is skipped silently (
national_reconciliation.py:460).
- G. Receipt provenance.
code_modified_pathsrecords paths only, with no hash or diff, and ignores untracked files (tools/uk_target_surface_receipt.py:75). So "the signed-for-the-run overlay moved no value" can't be checked from the receipt.- The "before" receipt names
code_head: aca40a692, butcompile_uk_full_target_surfacedoesn't exist there. The README says so in words; the receipt doesn't.
- H. Stale text.
docs/uk-local-cross-grain-reconciliation-802.md:98("a factor more than 2% from one is refused") andchangelog.d/1123-hmrc-area-taxpayer-basis.changed.md("within 2%") still describe the applied-factor gate. - I. The band-bridge bounds are drift tripwires (measured + 0.5 pt by construction), not plausibility bounds. If the feed carries Table 3.11's UK row, bounding each bridge by UK row ÷ Σ regions would check the stated explanation in code.
- Nits.
- Eight taxpayer band-bridge reasons read "the taxpayers of taxpayers Table 3.11's UK row carries".
- The higher-rate rule says "rounding error of at least 10%"; it means up to 10% (25% at 2,000).
assert_uk_uprating_holds_declared(holds=...)skips the loader's validation, and a missingexpires_onnow raises aKeyError.- A deferral under a rule id later deleted from the register still passes
assert_uk_local_deferrals_in_force.
Still open, as the reply says:
- the paid-claims UC extract;
- the West Midlands re-measure against its moved target;
- land at 2023;
- the Scotland and Wales household estimates;
- the Scottish
tax_banddecision; - signing the holds, the tenure entry and the two small-cell rules.
No tolerance was loosened. The count edits (21,788 → 21,804, 1,598 → 1,582) follow from the 16 cells being bound again. The only test weakened is the fan-out applied-factor refusal (A).
40a69a9 to
b8509c7
Compare
…s hold (#1123) Engine CI on #1198: the US pool tool's run config pins the US spec digest, which #104's solve.py edit moved (as us-f0-coverage.json already records), and the diesel per-head donor factor moves 0.036% with ONS's revised mid-2024 population in the 942a1fa feed, past its 5e-4 pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…must-fix 4) Nothing enforced the signatures #1198 asks for: all 15 holds and the small-cell rule were approved_by null and proposed, and the compile honoured them anyway. A hold is now in force only once it carries approved_by and approved_on and is no longer proposed; the holds check refuses the rows of an unsigned entry as it refuses undeclared ones, and the loader requires tracking and expires_on on every entry. A small-cell rule nobody signed defers nothing (its cells stay bound), and the runtime refuses a membership that defers under an unsigned rule. test_every_committed_hold_is_signed fails until the register is signed, so the open decision cannot be merged around. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Must-fix 2. The 2% fan-out gate read the reconciliation receipt, where the
single-winner rule had already put the local-authority cells on the
constituency total, so that grain's leg always read one. The gate now reads
the cells before any reconciliation, at every grain, and tests source
agreement: the bands' total over the cells' total before either side's
uprating index. The applied factor and the index gap are receipted beside
it. On the pinned feed the source factors are 0.999 to 1.007 at
constituency grain and 1.006 to 1.014 at local-authority grain; the applied
employment-count factors of 1.0192 and 1.0211 are 1.44% index choice
(band-by-band growth on the bands, all-ranges growth on the cells).
Must-fix 3. The 24 national band bridges had no bound and one shared reason
("about 0.5 percent short"). Each now declares max_factor_shift (its
measured gap plus half a point) and its own gap: the shortfall is the
taxpayers Table 3.11's UK row carries in no region, concentrated in the
lowest band (2.7% of taxpayers, 5.7% of income tax at 12,570 to 15,000).
Should-fix 6. A declared subset relation names its part and whole and is
tested on the compiled values in both loaders (a local part at each grain
on its own). The national-only loader runs the coverage check. An applier
that moved nothing stamps uprating_hold and stays a hold: the Welsh,
Scottish and Northern Irish tenure cells are now declared. A row with no
readable fact period is refused instead of passing as not held.
Minor 9. A roll-forward base resolves exactly one cell, or one for each
geography it names, and its numerator and denominator must be one series at
two periods.
Minor 10. A SPREE tenure share that moves by more than 10% in two years is
a stock reclassification (Rushmoor's social stock falls 22%): that
authority's census shares are held. The docstring says what the drift
carries: social renting against everything else.
The receipt tool writes the fan-out legs per grain, the sum bridges and the
subset relations, and names its inputs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…review) Must-fix 1, the part this repository can do: the UC area extract carries no Payment Indicator, so its cells count every household on UC (6.73m across Great Britain) while the national rows count paid claims (6.20m). The selector notes said the bridge "reconciles like with like"; they, the bridge and the partition now state the gap, the single GB factor (0.921) and the child bands' resulting spread against the national paid rows. The fix is a paid-claims area series from Chronicle and a re-pin. Should-fix 7: a proposed rule (unsigned, so not in force) would defer the 192 higher rate area cells of 5,000 or fewer, where HMRC's rounding to the thousand is 10% or more of the cell. Stale text: the #104 fragment (the census reports a zero target's weight, it does not enforce one), the DfI count (seventeen values, not eleven), the A15 and household-bridge paragraphs of the cross-grain doc, and the census notes that said every council-tax cell binds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…review) An unsigned rule defers nothing, so the 16 council-tax band cells below the floor are bound again: 21,804 active local references, 342 signed deferrals. Rushmoor's four tenure cells hold their census shares. National references, the release manifest and the uk-data parity register regenerate unchanged. The council-tax and surface pins now follow the rule's signature state, so signing it moves them with the regenerated references rather than breaking them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… its commit (#1198 review) A receipt of a draft surface may need a local overlay (a hold register signed for the run, since an unsigned hold refuses the compile). It now records code_modified_paths beside code_head rather than passing as the commit's own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w, question 11) The tax_band bindings were tested against fakes only. An engine test now pins the classification the national Table 2.2 rows and the higher rate area cells rely on, for rest-of-UK and Scottish taxpayers, and that the area metric counts higher rate taxpayers only. It also pins the gap the review asked about: a Scottish taxpayer with 40,000 of earnings and 5,000 of dividends is HIGHER on the engine, which compares total income with the Scottish thresholds, where HMRC applies Scottish rates to non-savings, non-dividend income and counts no higher rate taxpayer. The same person in England is BASIC. Only Scotland's two national rows carry it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…review, should-fix 5) The receipts behind the PR's value moves were not in the tree, and the "before" one postdated this branch's own national reconciliation, so the band-bridge and CGT regional moves were missing from the count. The before receipt is now the merge base aca40a6 with only the receipt tool, on the feed it pins; the after receipt is 5ad4816. Bound national rows go from 1,167 to 1,203: 36 added, none removed, 552 moved (360 SPI regional and 24 CGT regional cells by reconciliation; 168 by the re-pin and the household controls). Local cells go from 20,885 to 21,804: 919 added, none removed, 18,374 moved. docs/evidence/uk-target-surface-1123 holds the row-level national diff, the local diff by target and grain, and the after surface's reconciliation (the band bridges with their bounds, each fan-out bridge at both grains, every group's factors, the subset relations and the holds), with the script that extracts them. The after receipt was run over a register signed for the run only, which it records. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w, D) uk_hold_is_signed and uk_small_cell_rule_is_signed accepted any status but proposed, so a rejected or withdrawn entry with an approver and a date was in force. Both now share one allowlist: an approver, an approval date and the status approved. Also from the re-review's nits: - assert_uk_uprating_holds_declared validates a register passed in as the loader validates the committed one, so a missing expires_on is a named refusal and not a KeyError. - A rule deferral whose rule is no longer in uk/local_small_cell_rule.json is refused. Only a rule deferral carries expires_on, which is how one is told from the other signed deferrals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… their neighbours (#1198 re-review, A, B, E, H) A. The fan-out gate bounded only the source factor, so the factor the cells take was unbounded: a re-pinned ITL index could move the area targets unseen. Each fan-out sum bridge now also declares max_index_shift (its measured index gap plus half a point: 0.010, 0.020, 0.007, 0.009) and the gate refuses an index factor past it, at each grain. Both parts of the applied factor are bounded. B. South West local authorities took up to 1.043 against 1.018 to 1.021 elsewhere, because HMRC's local-authority tables carry no Torbay or Isles of Scilly row and the leg's bound cells took their share of the constituency total. The same happened wherever a signed cell is off the surface: the City of London (deferred with its value: 1.8% of London's self-employment income) and one constituency with no self-employment mean. uk_runtime/off_surface_cells.py puts those cells in the reconciliation frame, where a control moves their leg. A cell with a value joins with it, moved as its leg was moved. A cell with none is implied from the target's other local tier over the legs where both tiers are whole. An absent cell no tier can imply is refused. The cells never reach the solve. The fan-out gate counts them, so a grain that lacks a few authorities no longer reads as a source disagreement. On the pinned feed: 44 frame rows (38 with a value, 6 implied). Torbay and the Isles of Scilly are implied at 2.3% of the South West's employees (their household share is 2.6%). Local-authority leg factors against the constituencies: employment count 1.018 to 1.021 (was up to 1.043), employment amount 1.009 to 1.011 (was up to 1.029). E. The source factor fell back to the uprated value when ledger_value_before_alignment was missing. uk_value_before_uprating now refuses a spec that carries an uprating_factor and no source value, on both sides of the gate. H. The cross-grain doc and the changelog fragments describe the two gates, the signature rule and the off-surface cells. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The subset checks compared one part's total with its whole, so they were thin in four ways: - The age relation tested the 0-9 band alone against the UK total (ratio 0.106), where its reason is about all the bands. A relation can now name other_parts, disjoint targets tested on their sum: the nine age bands and the two 90-and-over rows together equal the UK total on the pinned feed. - Only totals were compared. Where a part and its whole both carry cells at one geography, the part is now tested there too. - max_excess was capped only by a test. The check refuses an allowance above UK_SUBSET_MAX_EXCESS (2%). - A relation with an unbound side was skipped silently. It is now refused, because it would stand untested. Two skips remain, each with its reason in the receipt: a narrowed register that binds neither side, and a local part in the national-only loader, which has no local surface. On the pinned feed the joint surface tests all 20 and skips none; the national-only loader tests 14 and skips the 3 local parts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ew, I) A band bridge's max_factor_shift is its measured gap plus half a point: a tripwire for drift, not a test of the reason the declaration gives. The reason is that Table 3.11's United Kingdom row counts taxpayers HMRC places in no region, so the twelve regional areas sum short of it. The feed carries that UK row, so the compile now tests the reason. Each of the 24 bridges declares explained_by: hmrc_spi_table_3_11_uk_row, and its factor must sit within half a point of the UK row over the regional cells' sum at the table's own year. The UK rows are vendored (uk/hmrc_spi_table_3_11_uk_rows.json, 120 rows from the pinned feed). On the pinned feed 21 of the 24 factors equal the explained factor to five decimals. The other three span two SPI bands under one ITL band and differ by the two publications' rounding: 0.19, 0.05 and 0.04 points. Also drops a doubled word from eight bridge reasons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The joint local surface refused its own fan-out sum control: the synthetic control's signature carried filters at top level, and the reconciliation canonicalizes only a 'measurement' mapping. The test now runs the synthetic signature through that canonicalization. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s hold (#1123) Engine CI on #1198: the US pool tool's run config pins the US spec digest, which #104's solve.py edit moved (as us-f0-coverage.json already records), and the diesel per-head donor factor moves 0.036% with ONS's revised mid-2024 population in the 942a1fa feed, past its 5e-4 pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…must-fix 4) Nothing enforced the signatures #1198 asks for: all 15 holds and the small-cell rule were approved_by null and proposed, and the compile honoured them anyway. A hold is now in force only once it carries approved_by and approved_on and is no longer proposed; the holds check refuses the rows of an unsigned entry as it refuses undeclared ones, and the loader requires tracking and expires_on on every entry. A small-cell rule nobody signed defers nothing (its cells stay bound), and the runtime refuses a membership that defers under an unsigned rule. test_every_committed_hold_is_signed fails until the register is signed, so the open decision cannot be merged around. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Must-fix 2. The 2% fan-out gate read the reconciliation receipt, where the
single-winner rule had already put the local-authority cells on the
constituency total, so that grain's leg always read one. The gate now reads
the cells before any reconciliation, at every grain, and tests source
agreement: the bands' total over the cells' total before either side's
uprating index. The applied factor and the index gap are receipted beside
it. On the pinned feed the source factors are 0.999 to 1.007 at
constituency grain and 1.006 to 1.014 at local-authority grain; the applied
employment-count factors of 1.0192 and 1.0211 are 1.44% index choice
(band-by-band growth on the bands, all-ranges growth on the cells).
Must-fix 3. The 24 national band bridges had no bound and one shared reason
("about 0.5 percent short"). Each now declares max_factor_shift (its
measured gap plus half a point) and its own gap: the shortfall is the
taxpayers Table 3.11's UK row carries in no region, concentrated in the
lowest band (2.7% of taxpayers, 5.7% of income tax at 12,570 to 15,000).
Should-fix 6. A declared subset relation names its part and whole and is
tested on the compiled values in both loaders (a local part at each grain
on its own). The national-only loader runs the coverage check. An applier
that moved nothing stamps uprating_hold and stays a hold: the Welsh,
Scottish and Northern Irish tenure cells are now declared. A row with no
readable fact period is refused instead of passing as not held.
Minor 9. A roll-forward base resolves exactly one cell, or one for each
geography it names, and its numerator and denominator must be one series at
two periods.
Minor 10. A SPREE tenure share that moves by more than 10% in two years is
a stock reclassification (Rushmoor's social stock falls 22%): that
authority's census shares are held. The docstring says what the drift
carries: social renting against everything else.
The receipt tool writes the fan-out legs per grain, the sum bridges and the
subset relations, and names its inputs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…review) Must-fix 1, the part this repository can do: the UC area extract carries no Payment Indicator, so its cells count every household on UC (6.73m across Great Britain) while the national rows count paid claims (6.20m). The selector notes said the bridge "reconciles like with like"; they, the bridge and the partition now state the gap, the single GB factor (0.921) and the child bands' resulting spread against the national paid rows. The fix is a paid-claims area series from Chronicle and a re-pin. Should-fix 7: a proposed rule (unsigned, so not in force) would defer the 192 higher rate area cells of 5,000 or fewer, where HMRC's rounding to the thousand is 10% or more of the cell. Stale text: the #104 fragment (the census reports a zero target's weight, it does not enforce one), the DfI count (seventeen values, not eleven), the A15 and household-bridge paragraphs of the cross-grain doc, and the census notes that said every council-tax cell binds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…review) An unsigned rule defers nothing, so the 16 council-tax band cells below the floor are bound again: 21,804 active local references, 342 signed deferrals. Rushmoor's four tenure cells hold their census shares. National references, the release manifest and the uk-data parity register regenerate unchanged. The council-tax and surface pins now follow the rule's signature state, so signing it moves them with the regenerated references rather than breaking them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… its commit (#1198 review) A receipt of a draft surface may need a local overlay (a hold register signed for the run, since an unsigned hold refuses the compile). It now records code_modified_paths beside code_head rather than passing as the commit's own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w, question 11) The tax_band bindings were tested against fakes only. An engine test now pins the classification the national Table 2.2 rows and the higher rate area cells rely on, for rest-of-UK and Scottish taxpayers, and that the area metric counts higher rate taxpayers only. It also pins the gap the review asked about: a Scottish taxpayer with 40,000 of earnings and 5,000 of dividends is HIGHER on the engine, which compares total income with the Scottish thresholds, where HMRC applies Scottish rates to non-savings, non-dividend income and counts no higher rate taxpayer. The same person in England is BASIC. Only Scotland's two national rows carry it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…review, should-fix 5) The receipts behind the PR's value moves were not in the tree, and the "before" one postdated this branch's own national reconciliation, so the band-bridge and CGT regional moves were missing from the count. The before receipt is now the merge base aca40a6 with only the receipt tool, on the feed it pins; the after receipt is 5ad4816. Bound national rows go from 1,167 to 1,203: 36 added, none removed, 552 moved (360 SPI regional and 24 CGT regional cells by reconciliation; 168 by the re-pin and the household controls). Local cells go from 20,885 to 21,804: 919 added, none removed, 18,374 moved. docs/evidence/uk-target-surface-1123 holds the row-level national diff, the local diff by target and grain, and the after surface's reconciliation (the band bridges with their bounds, each fan-out bridge at both grains, every group's factors, the subset relations and the holds), with the script that extracts them. The after receipt was run over a register signed for the run only, which it records. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
María signed every entry of uk/uprating_holds.json on 2026-10-11. Each expires on 2026-12-11, two months after the signature, in place of the proposed 2027 dates, so every hold is reviewed again by then. Status moves from proposed to approved. The compile no longer refuses the committed register, and test_every_committed_hold_is_signed passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w, D) uk_hold_is_signed and uk_small_cell_rule_is_signed accepted any status but proposed, so a rejected or withdrawn entry with an approver and a date was in force. Both now share one allowlist: an approver, an approval date and the status approved. Also from the re-review's nits: - assert_uk_uprating_holds_declared validates a register passed in as the loader validates the committed one, so a missing expires_on is a named refusal and not a KeyError. - A rule deferral whose rule is no longer in uk/local_small_cell_rule.json is refused. Only a rule deferral carries expires_on, which is how one is told from the other signed deferrals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… their neighbours (#1198 re-review, A, B, E, H) A. The fan-out gate bounded only the source factor, so the factor the cells take was unbounded: a re-pinned ITL index could move the area targets unseen. Each fan-out sum bridge now also declares max_index_shift (its measured index gap plus half a point: 0.010, 0.020, 0.007, 0.009) and the gate refuses an index factor past it, at each grain. Both parts of the applied factor are bounded. B. South West local authorities took up to 1.043 against 1.018 to 1.021 elsewhere, because HMRC's local-authority tables carry no Torbay or Isles of Scilly row and the leg's bound cells took their share of the constituency total. The same happened wherever a signed cell is off the surface: the City of London (deferred with its value: 1.8% of London's self-employment income) and one constituency with no self-employment mean. uk_runtime/off_surface_cells.py puts those cells in the reconciliation frame, where a control moves their leg. A cell with a value joins with it, moved as its leg was moved. A cell with none is implied from the target's other local tier over the legs where both tiers are whole. An absent cell no tier can imply is refused. The cells never reach the solve. The fan-out gate counts them, so a grain that lacks a few authorities no longer reads as a source disagreement. On the pinned feed: 44 frame rows (38 with a value, 6 implied). Torbay and the Isles of Scilly are implied at 2.3% of the South West's employees (their household share is 2.6%). Local-authority leg factors against the constituencies: employment count 1.018 to 1.021 (was up to 1.043), employment amount 1.009 to 1.011 (was up to 1.029). E. The source factor fell back to the uprated value when ledger_value_before_alignment was missing. uk_value_before_uprating now refuses a spec that carries an uprating_factor and no source value, on both sides of the gate. H. The cross-grain doc and the changelog fragments describe the two gates, the signature rule and the off-surface cells. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The subset checks compared one part's total with its whole, so they were thin in four ways: - The age relation tested the 0-9 band alone against the UK total (ratio 0.106), where its reason is about all the bands. A relation can now name other_parts, disjoint targets tested on their sum: the nine age bands and the two 90-and-over rows together equal the UK total on the pinned feed. - Only totals were compared. Where a part and its whole both carry cells at one geography, the part is now tested there too. - max_excess was capped only by a test. The check refuses an allowance above UK_SUBSET_MAX_EXCESS (2%). - A relation with an unbound side was skipped silently. It is now refused, because it would stand untested. Two skips remain, each with its reason in the receipt: a narrowed register that binds neither side, and a local part in the national-only loader, which has no local surface. On the pinned feed the joint surface tests all 20 and skips none; the national-only loader tests 14 and skips the 3 local parts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ew, I) A band bridge's max_factor_shift is its measured gap plus half a point: a tripwire for drift, not a test of the reason the declaration gives. The reason is that Table 3.11's United Kingdom row counts taxpayers HMRC places in no region, so the twelve regional areas sum short of it. The feed carries that UK row, so the compile now tests the reason. Each of the 24 bridges declares explained_by: hmrc_spi_table_3_11_uk_row, and its factor must sit within half a point of the UK row over the regional cells' sum at the table's own year. The UK rows are vendored (uk/hmrc_spi_table_3_11_uk_rows.json, 120 rows from the pinned feed). On the pinned feed 21 of the 24 factors equal the explained factor to five decimals. The other three span two SPI bands under one ITL band and differ by the two publications' rounding: 0.19, 0.05 and 0.04 points. Also drops a doubled word from eight bridge reasons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iew, G) A receipt named its commit and the tracked paths that differed from it, with no digest and no untracked files. So a receipt run over an overlay could not be checked, and one run with the tool laid over an older commit named that commit as if it carried the tool. The receipt now records every file under packages and tools that differs from code_head, tracked or not, with the SHA-256 of its content, and the diff of the tracked ones (its digest always, its text up to 256 kB). It also carries the off-surface cells' receipt. The evidence extracts keep the file list and the diff's digest for both receipts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ew, nit) A cell rounded to the thousand can be up to 500 from its true count: up to 10% of a cell of 5,000 and up to 25% of the smallest cell. The rule's reason said "at least 10%". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The full-graph engine tests run on a small synthetic ladder, and the committed membership signs areas that ladder does not hold (the Isles of Scilly), so the off-surface cells refused the compile there. The rule is the leg licences' own: under the committed crosswalk an area that cannot be placed is a register defect and refuses; under a run's own ladder it is an area the run does not carry, so it is left out and named in the receipt (areas_outside_the_ladder). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The after receipt is now bc32a49c1, on a clean tree: the hold register is signed, so the compile needs no overlay. The before receipt is re-run with the provenance record and names the six files laid over aca40a6; its rows are identical to the earlier one. National rows are unchanged from the last extract: 1,167 to 1,203, 36 added, none removed, 552 moved. Local cells go from 20,885 to 21,804: 919 added, none removed, 18,811 moved. 437 more cells move than at 40a69a9: the council-tax band cells of London, the South West and Scotland's band H, by up to 0.5%, because a deferred authority no longer leaves its share on its neighbours. reconciliation.json adds the off-surface cells (44 frame rows), each band bridge's explained factor, and both gated parts of each fan-out factor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The branch now sits on d1fda72. main brought the 1ee7dfe feed re-pin, the ONS 07.2.2 household road-fuel target and the measure exclusions of VAT, the two ONS energy rows and the OBR cars receipts (microcosm#1113), the US state AGI bands (#940) and the PUF-support change (#1178). 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 (which holds every 1ee7dfe row). - National references: 1,268 active, the branch's 1,267 plus the road-fuel target. The contract has 400 targets (348 registry, 52 profile; 156 unmapped declarations). - The two national parity receipts and the reference membership are regenerated; the local references, the local census, the local parity receipt and uk_data_target_parity come out byte-identical. - desnz_road_fuel_by_vehicle.json is vendored again under the 942a1fa feed identity; the UK spine parity fixture follows it. - Pins that both sides moved are recomputed on the merged code: the seed protocol and seed map digests (#1178's puf_support and #104's solve edit), the loader golden vector, the US spec digest and the US coverage evidence. - The re-pin doc keeps both re-pins in order; 942a1fa is on top of 1ee7dfe. The compiled surface on the pinned feed: 1,200 bound national rows (four rows are now measured diagnostics, one is new) and 21,804 local cells, every value equal to the branch's before the rebase. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The branch is rebased on main at d1fda72, so both receipts are run again. Before is that commit with only the receipt tool, on the 1ee7dfe feed main pins. After is 67e7af0 on the 942a1fa feed, on a clean tree. Bound national rows go from 1,164 to 1,200: 36 added, none removed, 552 moved. main now measures VAT, the two ONS energy rows and the OBR cars receipts as diagnostics and binds ONS 07.2.2 road fuel (microcosm#1113), so both counts are three lower 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. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
…indow (#1198 re-review 2, item 1) All 16 holds expire on 2026-12-11, and the evaluation date defaults to today, so from the next day every national and joint UK build refuses 117 target ids. Nothing warned: the feed-backed compiles are skipped in CI, so CI would stay green while the builds failed. - The compile warns when a held target's declaration is within UK_HOLD_EXPIRY_NOTICE_DAYS (14) of its expiry, and the receipt carries the earliest expiry. - test_no_committed_hold_is_about_to_lapse fails in the same window, while the builds still pass. It is a clock on purpose: from 2026-11-27 it is red until the holds are reviewed and signed again, or replaced by a newer fact or an index. - A hold is in force from approved_on through expires_on, as a measure exclusion is: evaluated before its signature it is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… items 2 and 3, nits) 2. An implied row is what a leg's bound cells do not hold, so an undercount anywhere in the leg passed into it and through every gate that counts it. Its share of the leg is now bounded by its areas' share of the leg's census households: at most UK_IMPLIED_SHARE_MAX_RATIO (1.5) times that share, or the compile refuses. On the pinned feed the ratios are 0.32 (higher rate taxpayers) to 0.89 (employees) for Torbay and the Isles of Scilly, and 0.37 for the one North West constituency. An implied area with no census households to set it against is refused; a surface that binds no census households says in the receipt that the row is unbounded. 3. An off-surface area outside a run's own ladder was left out with a receipt line whatever the run was. It is now left out only when the membership binds some cell for the area: a whole run then binds that cell, and the frame refuses a bound cell its ladder cannot place, so a run that gets that far is a narrowed one (a sample or a fixture). An area the membership binds nothing for has no such guard and refuses. A test pins that every committed off-surface area is one a whole run binds. Nits: - full_problem's own surface passes the membership's deferred and off-surface cells, as the graph's does. - A deferred cell with a value counts in its leg's total in every leg, so a leg no control moves reads whole when the two tiers' ratio is taken; it adds a frame row where a control moves either tier of its leg. - The evidence extraction writes the before receipt's overlay diff beside the extracts, so it can be read in the tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
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>
b8509c7 to
a4df360
Compare
Reply to the re-review at
|
The taxpayer filter on the 13 SPI property-income rows: measurement and decision (Claude Code)Decision (María, 2026-10-12): keep the filter. The 13 SPI Table 3.7 property-income amounts that #1145 binds are measured over taxpayers only on this branch ( Why the two measures differ
What was measured
What it means
The script"""Property income by SPI total-income band on the #1145 c9 candidate: all people against taxpayers."""
import json, os, sys, warnings
os.environ.setdefault("HDF5_USE_FILE_LOCKING", "FALSE")
warnings.simplefilter("ignore")
import numpy as np
import importlib.metadata as metadata
from policyengine_uk import Microsimulation
YEAR = 2025
dataset = sys.argv[3] # the calibrated c9 candidate, microcosm_uk_2024_25.h5
sim = Microsimulation(dataset=dataset)
def col(name):
v = sim.calculate(name, YEAR)
return np.asarray(v.values, dtype=float), np.asarray(v.weights, dtype=float)
prop, w = col("property_income")
total, _ = col("total_income")
tax, _ = col("income_tax")
relief, _ = col("property_finance_cost_relief")
costs, _ = col("property_finance_costs")
edges = [12570, 15000, 20000, 30000, 40000, 50000, 70000, 100000, 150000, 200000, 300000, 500000, 1000000, np.inf]
receipt = json.load(open(sys.argv[1])) # a target-surface receipt, for the 13 target values
targets = {r["name"]: r["value"] for r in receipt["national"] if r["name"].startswith("hmrc/property_income_income_band_")}
landlord = prop > 0
payer = tax > 0
rows = []
for lo, hi in zip(edges[:-1], edges[1:]):
band = (total >= lo) & (total < hi)
all_amt = float((prop * w)[band].sum()); pay_amt = float((prop * w)[band & payer].sum())
all_n = float(w[band & landlord].sum()); pay_n = float(w[band & landlord & payer].sum())
nonpay = band & landlord & ~payer
rows.append({"band": lo, "all_gbp": all_amt, "taxpayers_gbp": pay_amt, "all_landlords": all_n, "taxpayer_landlords": pay_n,
"nonpayer_with_relief": float(w[nonpay & (relief > 0)].sum()), "nonpayer_records": int(nonpay.sum())})
out = {"engine": metadata.version("policyengine-uk"), "year": YEAR, "dataset": dataset, "bands": rows,
"below_12570": {"all_gbp": float((prop*w)[total < 12570].sum()), "landlords": float(w[(total < 12570) & landlord].sum())},
"all_property_income_gbp": float((prop*w).sum()), "landlords": float(w[landlord].sum()),
"targets": targets}
json.dump(out, open(sys.argv[2], "w"), indent=1)
ta = sum(r["all_gbp"] for r in rows); tp = sum(r["taxpayers_gbp"] for r in rows)
print("engine", out["engine"], "| all property income £bn", round(out["all_property_income_gbp"]/1e9,2), "landlords m", round(out["landlords"]/1e6,3))
print("in the 13 bands: all people £bn", round(ta/1e9,2), "| taxpayers £bn", round(tp/1e9,2), "| non-taxpayers £bn", round((ta-tp)/1e9,2), f"({(ta-tp)/ta:.1%})", "| targets £bn", round(sum(targets.values())/1e9,2), len(targets))
print("landlords in bands: all m", round(sum(r['all_landlords'] for r in rows)/1e6,3), "taxpayers m", round(sum(r['taxpayer_landlords'] for r in rows)/1e6,3), "non-taxpayers with relief m", round(sum(r['nonpayer_with_relief'] for r in rows)/1e6,3), "records", sum(r['nonpayer_records'] for r in rows))
for r in rows:
print(f" {r['band']:>8}: all £{r['all_gbp']/1e9:6.2f}bn taxpayers £{r['taxpayers_gbp']/1e9:6.2f}bn gap {1-r['taxpayers_gbp']/r['all_gbp'] if r['all_gbp'] else 0:6.1%} | landlords {r['all_landlords']/1e3:7.1f}k taxpayers {r['taxpayer_landlords']/1e3:7.1f}k | non-payers with relief {r['nonpayer_with_relief']/1e3:6.1f}k ({r['nonpayer_records']} records)")
print("below 12,570: £bn", round(out["below_12570"]["all_gbp"]/1e9,2), "landlords m", round(out["below_12570"]["landlords"]/1e6,3))The output{
"engine": "2.123.0",
"year": 2025,
"bands": [
{
"band": 12570,
"all_gbp": 739209114.2499585,
"taxpayers_gbp": 673939177.727847,
"all_landlords": 106072.8392278701,
"taxpayer_landlords": 98167.14690719545,
"nonpayer_with_relief": 164.04813385009766,
"nonpayer_records": 103
},
{
"band": 15000,
"all_gbp": 1734569916.506525,
"taxpayers_gbp": 1576771007.521718,
"all_landlords": 228460.7329572402,
"taxpayer_landlords": 215265.92346588895,
"nonpayer_with_relief": 7034.7943078279495,
"nonpayer_records": 30
},
{
"band": 20000,
"all_gbp": 4016857379.8852468,
"taxpayers_gbp": 3632881784.709773,
"all_landlords": 434235.018026039,
"taxpayer_landlords": 412633.3857693523,
"nonpayer_with_relief": 19504.546014398336,
"nonpayer_records": 42
},
{
"band": 30000,
"all_gbp": 4467844392.178325,
"taxpayers_gbp": 4374841463.2569685,
"all_landlords": 411677.84457619116,
"taxpayer_landlords": 408056.6843499057,
"nonpayer_with_relief": 3161.4855183959007,
"nonpayer_records": 16
},
{
"band": 40000,
"all_gbp": 4440908596.628444,
"taxpayers_gbp": 4401200624.90477,
"all_landlords": 367039.04541874677,
"taxpayer_landlords": 366080.90053547174,
"nonpayer_with_relief": 958.144883275032,
"nonpayer_records": 2
},
{
"band": 50000,
"all_gbp": 6896780804.749105,
"taxpayers_gbp": 6896780804.749105,
"all_landlords": 486628.99004568905,
"taxpayer_landlords": 486628.99004568905,
"nonpayer_with_relief": 0.0,
"nonpayer_records": 0
},
{
"band": 70000,
"all_gbp": 4811365242.8906,
"taxpayers_gbp": 4811365242.8906,
"all_landlords": 276958.4430429302,
"taxpayer_landlords": 276958.4430429302,
"nonpayer_with_relief": 0.0,
"nonpayer_records": 0
},
{
"band": 100000,
"all_gbp": 3301603916.086019,
"taxpayers_gbp": 3301603916.086019,
"all_landlords": 171501.82781397924,
"taxpayer_landlords": 171501.82781397924,
"nonpayer_with_relief": 0.0,
"nonpayer_records": 0
},
{
"band": 150000,
"all_gbp": 1401580825.345178,
"taxpayers_gbp": 1401580825.345178,
"all_landlords": 63391.11676461995,
"taxpayer_landlords": 63391.11676461995,
"nonpayer_with_relief": 0.0,
"nonpayer_records": 0
},
{
"band": 200000,
"all_gbp": 1337064761.5412693,
"taxpayers_gbp": 1337064761.5412693,
"all_landlords": 49441.01511074789,
"taxpayer_landlords": 49441.01511074789,
"nonpayer_with_relief": 0.0,
"nonpayer_records": 0
},
{
"band": 300000,
"all_gbp": 976727442.7887601,
"taxpayers_gbp": 976727442.7887601,
"all_landlords": 27743.166271453723,
"taxpayer_landlords": 27743.166271453723,
"nonpayer_with_relief": 0.0,
"nonpayer_records": 0
},
{
"band": 500000,
"all_gbp": 610383423.6390421,
"taxpayers_gbp": 610383423.6390421,
"all_landlords": 12970.121472515166,
"taxpayer_landlords": 12970.121472515166,
"nonpayer_with_relief": 0.0,
"nonpayer_records": 0
},
{
"band": 1000000,
"all_gbp": 552367083.8276165,
"taxpayers_gbp": 552367083.8276165,
"all_landlords": 23303.13035662286,
"taxpayer_landlords": 23303.13035662286,
"nonpayer_with_relief": 0.0,
"nonpayer_records": 0
}
],
"below_12570": {
"all_gbp": 575829853.843927,
"landlords": 137855.0947300978
},
"all_property_income_gbp": 35863092754.16002,
"landlords": 2797278.385814743,
"targets": {
"hmrc/property_income_income_band_100_000_to_150_000": 3151071428.571429,
"hmrc/property_income_income_band_12_570_to_15_000": 703500000.0,
"hmrc/property_income_income_band_150_000_to_200_000": 1332747524.7524753,
"hmrc/property_income_income_band_15_000_to_20_000": 1651431535.2697096,
"hmrc/property_income_income_band_1_000_000_to_inf": 525041487.8397711,
"hmrc/property_income_income_band_200_000_to_300_000": 1272816349.3840985,
"hmrc/property_income_income_band_20_000_to_30_000": 3831324110.671937,
"hmrc/property_income_income_band_300_000_to_500_000": 927337625.9798431,
"hmrc/property_income_income_band_30_000_to_40_000": 4252163461.538461,
"hmrc/property_income_income_band_40_000_to_50_000": 4229783653.8461537,
"hmrc/property_income_income_band_500_000_to_1_000_000": 580715469.6132597,
"hmrc/property_income_income_band_50_000_to_70_000": 6559199999.999999,
"hmrc/property_income_income_band_70_000_to_100_000": 4579293333.333333
},
"dataset": "arm c9 of PolicyEngine/microcosm#1145 (microcosm_uk_2024_25.h5)"
} |
Part of #1123 (PR A: items 1, 2, 6 and 7; private rent is PR B). Addresses #104.
Why
The K=25 audit found that most local misses surviving the dense solve were on the target side. Local rows were never uprated, 509 national rows were held at an older period with no declared index, nothing refused an undeclared hold, and reconciliation across grains was opt-in. This PR makes the UK target surface one consistent, enforced surface.
What changes
uk/cross_grain_declarations.json: bridges, partitions, relations) or the compile refuses. The lattice gains a nation grain, rows that span several legs, and same-geography partitions. The national register is reconciled across UK, nation and region in both loaders, and the joint local pass refuses a national factor away from one.uk/uprating_holds.json, 16 entries). The compile refuses an undeclared, unsigned, expired or stale hold, nationally and locally. María signed all 16 on 2026-10-11; each expires on 2026-12-11, so every hold needs a review before then. The compile warns 14 days before an expiry, and a test on the committed register fails in the same window, so CI turns red on 2026-11-27.1ee7dfepin that Set UK road-fuel and energy levels in the LCFS stage on a calendar-2024 basis (#1113) #1167 put onmainand UK landlords: bind the SPI net property amounts, leave property_wealth to the engine, add receipts and finance costs #1145 kept:942a1faholds every1ee7dferow. National population, local-authority ages, land, SLC repayments, ISC, DfI and public sector employment move to their 2025 editions. Spine inputs move only by publisher revisions (revised mid-2024 population, DfI rounding).tax_band), and higher rate taxpayers bind by constituency and local authority in England, Wales and Northern Ireland (919 cells). The 13 SPI property-income amounts that UK landlords: bind the SPI net property amounts, leave property_wealth to the engine, add receipts and finance costs #1145 binds carry the same taxpayer filter as every other SPI row here, which is Table 3.7's own universe.mainmeasures them over every landlord in the band. On UK landlords: bind the SPI net property amounts, leave property_wealth to the engine, add receipts and finance costs #1145's calibrated candidate (armc9, policyengine-uk 2.123.0 at 2025), people who pay no income tax hold 2.1% of the property income in those bands (£0.74bn of £35.29bn) and 9.4% in the three bands below £30,000. They are 47,000 of 2.66m landlords; 31,000 of them have a finance-cost reduction, which takes their liability to nil. María decided on 2026-10-12 to keep the filter; the measurement by band is in this comment.uk/local_small_cell_rule.json). Two rules are proposed and unsigned, so neither is in force and their cells are bound: council-tax band cells under 50 dwellings (16 cells) and higher rate area cells of 5,000 or fewer, where HMRC's rounding to the thousand can reach 10% (192 cells).rolled_forward_by_ratiovalue operation; both UK target kernels hash every module that decides a target value;tools/uk_target_surface_receipt.pywrites and diffs receipts of the compiled surface; the national compile scans only each reference's own sources (the national parity receipts drop from about 2h25m to under 20 minutes on the 927k-fact feed, with identical candidate facts).Value moves (receipts in
docs/evidence/uk-target-surface-1123/)The branch is rebased on
mainat51a263726. Before is that commit with only the receipt tool; after iscd39d037e, run on a clean tree. Each receipt records what its working tree adds to the commit it names.Verification
tools/uk_target_surface_receipt.py --communalruns clean end to end on the pinned feed, on a clean tree atcd39d037e.cd39d037e: the engine-free job's files, 16,693 tests with 6 failures that are local to this machine or to live state (two Orrery parser tests need a node package; one CGT test expects no pe-uk in the engine-free venv; one telemetry test is unstable under parallel workers; one head-to-head scorer test needs Python 3.14.7; onemicrocosm-datatest reads a live Hugging Face pointer that CI skips); engine-uk 180 of 180; lint,tools/ci_test_plan.py verifyand the vendored-resource check ok. CI on the previous headb8509c765was green on every lane, engine-us included.microcosm.calibrate.solvemoves the loader golden vector, the US seed digests, the US spec digest, the US coverage evidence and the calibrate graph parity pins; all are re-pinned as main does for solver edits, and recomputed after each rebase wheremainmoved them too.mainretired the West Midlands income-tax deferral (UK landlords: bind the SPI net property amounts, leave property_wealth to the engine, add receipts and finance costs #1145), so that cell is bound against the target this PR moves; its fit shows at the next national build.Departures from the plan
gates.json: closure fails closed at compile instead.Open decisions (María)
Before merge:
tax_bandcompares a Scottish taxpayer's total income with the Scottish thresholds; the Scottish rates apply to non-savings, non-dividend income only policyengine-uk#2350). Keep them bound, or defer them until that issue is fixed.full_frs_tei_band_unavailable,population_universe_private_households).Can follow as issues:
After merge
🤖 Generated with Claude Code