Skip to content

Sign off the local cuts' target-fit rows (dbacb023, 83802786), and the company car base - #1491

Open
juaristi22 wants to merge 11 commits into
mainfrom
uk-local-1428-signoff
Open

juaristi22 wants to merge 11 commits into
mainfrom
uk-local-1428-signoff

Conversation

@juaristi22

@juaristi22 juaristi22 commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

This PR carries three pieces of work:

Merging

Use a merge commit. Three of this branch's commits are the build commits that published cuts record, so all three must stay in main's history:

  • 1affbc127: the reuse build and assembly of dbacb023;
  • dd99a1dcd: the build of the national 0639b9c7, and build 1 of 83802786;
  • 5293966d4: the reuse build and assembly of 83802786. It is reachable only as the second parent of fab42beff.

A squash merge would drop all three. After the merge I'll run git merge-base --is-ancestor on each against origin/main and post the result here.

2dbf7cf56 and the two pairs.

  • SPI donor savings rebased to SPI's 2023-24 bands (savings follow-up to #1418) #1428 merged on 2 Nov at its head 2dbf7cf56 (merge commit e226192ad), so 329a99e71 is in main as its parent.
  • dbacb023 and f6e33b93 were built from 329a99e71, before 2dbf7cf56, which bands SPI records on total income net of expenses and without gifts (Vahid's review of 329a99e71, items 1, 2 and 5). Neither carries it.
  • The pair built from dd99a1dcd (0639b9c7 and 83802786) carries it.
  • That pair predates 6ef5983e9, so it keeps the cap at EPB on tape rows that drew EPB. Later builds pick up the EPB + PAY cap.

What happened

dbacb023 (3 Nov):

  • Build 1 (329a99e71, 2 Nov) passed every structural gate and was blocked only by target fit, national and local. The diagnostic high-value homes support check also fails, and the cut ships failing it: a diagnostic gate blocks nothing, and nothing here signs it. Its counts are in Vahid's review of 1affbc127, item 3.
  • 1affbc127 signs those rows in the gate-only registers.
  • The reuse build at 1affbc127 (gate clock 2026-11-03) reused build 1's solve, passed every release-blocking gate and assembled.
  • Published to the inspect lane on 3 Nov (tag only; latest untouched).

83802786 (4 Nov):

  • Build 1 (dd99a1dcd, 4 Nov) passed every structural gate and was blocked only by target fit, national and local. The diagnostic high-value homes support check fails, as on dbacb023, and nothing here signs it.
  • 5293966d4 signs those rows, on a branch from dd99a1dcd, merged here by fab42beff.
  • The reuse build at 5293966d4 (gate clock 2026-11-04) reused build 1's solve, passed every release-blocking gate and assembled.
  • Published to the inspect lane on 4 Nov (tag only), with the national 0639b9c7.

Company car base (María's ruling of 3 November 2026)

The two commits took effect in the pair built from dd99a1dcd. dbacb023 and f6e33b93 don't carry them.

Why. The national f6e33b93 has 473,000 company car holders, against HMRC's 920,000 in 2024-25. About a third of its cars are electric, against half in HMRC's Table 2C. Most of the shortfall is SPI tape rows losing their FRS twin's car whenever their drawn EPB is zero, though EPB is drawn independently of the car: about 272,000 weighted holders and £10.6bn of list price at the stage's weights.

d0e87370e Keep company cars on SPI rows whose drawn EPB is zero

  • A tape row whose drawn EPB is zero keeps its twin's car. Its car and fuel benefits come out of employment_income, capped at its PAY within the pay its employee pension contribution leaves. They move from hmrc_spi_pay into hmrc_spi_employment_benefits, so the row's PAY + EPB + TAXTERM stays the tape's.
  • A zero-EPB row whose PAY can't hold the car still loses it. Rows with positive EPB keep the cap at EPB.
  • The stage now rewrites the two SPI descriptors. The receipt and the stage-health gate gain two checks: a tape row's PAY below zero, and a tape row's PAY + EPB moved.

dd99a1dcd Bind HMRC's 2024-25 company car holders, taxable value and fuel counts

  • Five national rows in a new family, hmrc_benefits_in_kind:

    Row HMRC 2024-25 Frame
    hmrc.benefits_in_kind.company_car_recipients Table 1, 920,000 People with a company car list price
    hmrc.benefits_in_kind.company_car_taxable_value Table 1, £3,070m The engine's company car benefit in 2024-25
    hmrc.benefits_in_kind.company_car_fuel.fully_electric Table 2C, 465,000 ELECTRIC
    hmrc.benefits_in_kind.company_car_fuel.petrol_including_hybrids Table 2C, 390,000 PETROL, ALTERNATIVE_FUEL
    hmrc.benefits_in_kind.company_car_fuel.diesel_including_hybrids Table 2C, 60,000 DIESEL_RDE2, DIESEL_NON_RDE2
  • The taxable value is measured as the engine's benefit in 2024-25, through a new dated measure (company_car_benefit_2024, measurement_period 2024), as the CGT rows are. The calibration year's appropriate percentages are a point higher, so measuring 2025 would pull the base down.

  • The counts are held at 2024-25, since no index projects company car holders.

  • The fact fence keeps the vendored copy of the tables out of the solve: it only seeds the fuel and CO2 draws. A new calibrated_facts field names the bound rows.

Measured on the pair.

  • The five rows fit within 0.5% of HMRC on the national 0639b9c7 and within 2% on 83802786.
  • The national has 913,000 holders, £3.06bn of taxable value and a 50.7% electric share.
  • The 2030-31 company car rise scores +£228m, against Table 6.1's +£310m (+£125m on f6e33b93). The 2024-25 base matches HMRC; what's left is car volumes, which the dashboard's forecast holds flat. Every costing on the pair is in PolicyEngine/policyengine.py#731.

5304b3c50 and 6ef5983e9: Vahid's second review.

  • Items 6–8: a receipt count of rows a car leaves with no PAY, the dated measure's year check, and the hold's reason.
  • Item 5: every tape row is capped at EPB + PAY, EPB first (cap: epb_then_pay).

Both come after dd99a1dcd, the commit the national and the local cut were built from, so later builds pick them up.

For you: the two holds in uprating_holds.json are signed in your name for the ruling of 3 Nov (approved 2026-11-03, expiring 2027-01-03). Say if you'd rather sign them differently.

Tests run locally for the car commits

uv run --no-sync pytest packages/microcosm-build/tests/engine_free/uk/test_uk_company_cars.py packages/microcosm-build/tests/engine_free/uk/test_uk_company_cars_stage_health.py packages/microcosm-build/tests/engine_free/uk/test_uk_stage_health.py packages/microcosm-build/tests/engine_free/uk/test_uk_population_targets.py packages/microcosm-build/tests/engine_free/uk/test_uk_target_references.py packages/microcosm-build/tests/engine_free/uk/test_uk_uprating_holds.py packages/microcosm-build/tests/engine_free/uk/test_uk_measure_simulation.py packages/microcosm-build/tests/engine_free/uk/test_uk_measurement_period.py packages/microcosm-build/tests/engine_free/uk/test_uk_ledger_targets.py packages/microcosm-build/tests/engine_free/uk/test_uk_full_targets.py packages/microcosm-build/tests/engine_free/uk/test_uk_national_calibration.py packages/microcosm-build/tests/engine_free/uk/test_uk_cross_grain_declarations.py packages/microcosm-build/tests/engine_free/uk/test_uk_source_stages.py packages/microcosm-build/tests/engine_free/uk/test_uk_release_input_coverage_manifest.py packages/microcosm-build/tests/engine_free/uk/test_uk_graph.py packages/microcosm-build/tests/engine_free/shared/test_country_spec.py packages/microcosm-build/tests/engine_free/shared/test_gate_battery_contract_pins.py
uv run --no-sync pytest packages/microcosm-build/tests/engine/uk/test_uk_company_cars.py packages/microcosm-graph/tests/engine/uk/test_acceptance_h_parity.py

All pass. The national target references, membership and compile-parity receipt were regenerated on the pinned feed (08962f1): five new rows, every other reference unchanged.

After the main merge (22bda9511):

  • The targets, holds and test counts were merged by hand: 467 targets, 405 in the registry scope, 220 unmapped declarations and 1,425 active references.
  • The references, the national parity receipt, the coverage manifest and the H2 spine fixture were regenerated on the merged tree. Against main, each differs only by the five car rows or the car stage's declaration.
  • The H2 UK spine parity test, and the population target, reference, measurement period, hold and coverage tests, pass.
  • So does test_committed_surfaces_regenerate_from_pinned_feed[national], run on the feed. Its [local] case caught the missing category, and c2f91e201 commits that case's regenerated file unchanged.

Registers at head, against main

Register Main Head Added Retired
National target-fit rows (uk_target_fit) 8 11 5 2
Local target-fit cells (uk_local_target_fit) 37 35 13 15
Areas below the support floor (uk_local_area_support) 6 6 0 0
  • 3 Nov entries cite attempt uk-local-candidate-f100-s42-20261102T182958Z-78667c3a and report 7f7d66cc44ab2370a126d344cba66a923be871291f0397ef4e8496bf060f57f8. They expire on 2026-12-01.
  • 4 Nov entries cite attempt uk-local-candidate-f100-s42-20261104T000247Z-3a5b4051 and report 365f2878c6e3c8ca333486e9255632c5362c5b01a93d3fb699eca1ee22aa0795. They expire on 2026-12-02.
  • 5293966d4 retires 2 of the 4 national rows and 5 of the 11 local cells that 1affbc127 added.
  • No area entries are added. The six the registers keep keep their own windows.
  • Assembly deadline: 2026-11-16, set by entries from the 19 Oct batch, still the earliest to expire. Both reuse builds assembled well before it.

Gate-only sign-off from dense build 1 of 83802786 (microcosm#1301, 5293966d4)

Build-1 attempt uk-local-candidate-f100-s42-20261104T000247Z-3a5b4051, full gate report sha256 365f2878c6e3c8ca333486e9255632c5362c5b01a93d3fb699eca1ee22aa0795, gates evaluated on 2026-11-04.

Register data and test pins only. Every signed row stays bound in the solve: each entry is scoped to the local line and suppresses only its release fence. A build in which a signed row passes fails its entry as stale.

Register Added Renewed Kept Retired Rescoped For review
National target-fit rows (uk_target_fit) 3 0 8 3 0 0
Local target-fit cells (uk_local_target_fit) 9 0 26 10 0 0
Areas below the support floor (uk_local_area_support) 0 0 6 0 0 0

National target-fit rows: 2 stale-risk, 0 near-bound. Stale-risk counts signed rows within 0.01 above the bound, which build 2 fails as stale if it does not reproduce build 1 exactly; near-bound counts unsigned rows within the same margin below it. The local registers' counts are not printed here.

Window: approved 2026-11-04 by juaristi22, adjudication microcosm#1301 route for the local cut built with microcosm#1491 (sign-off 2026-11-04); fit entries expire 2026-12-02. Build 2 needed a gate clock (--gate-review-date) on or after 2026-11-04 and on or before 2026-11-16; it ran at 2026-11-04.

Predicted build-2 verdicts on build 1's rows: uk_target_fit passes, uk_local_target_fit passes, uk_local_area_support passes. The reuse build bore them out.

Gate-only sign-off from dense build 1 of dbacb023 (microcosm#1301, 1affbc127)

Build-1 attempt uk-local-candidate-f100-s42-20261102T182958Z-78667c3a, full gate report sha256 7f7d66cc44ab2370a126d344cba66a923be871291f0397ef4e8496bf060f57f8, gates evaluated on 2026-11-02.

Register Added Renewed Kept Retired Rescoped For review
National target-fit rows (uk_target_fit) 4 0 7 1 0 0
Local target-fit cells (uk_local_target_fit) 11 0 25 12 0 0
Areas below the support floor (uk_local_area_support) 0 0 6 0 0 0

National target-fit rows: 1 stale-risk, 2 near-bound.

Window: approved 2026-11-03 by juaristi22, adjudication microcosm#1301 route for the local cut built with microcosm#1428 (sign-off 2026-11-03); fit entries expire 2026-12-01. Build 2 needed a gate clock on or after 2026-11-03 and on or before 2026-11-16; it ran at 2026-11-03.

Predicted build-2 verdicts on build 1's rows: uk_target_fit passes, uk_local_target_fit passes, uk_local_area_support passes.

Tests to run

uv run --no-sync pytest packages/microcosm-build/tests/engine_free/uk/test_uk_terminal_gates.py

Disclosure: the company car sections carry HMRC's published 2024-25 figures, Table 6.1's costing, and weighted aggregates of the nationals f6e33b93 and 0639b9c7, of 83802786 and of the stage receipt. Those are holders, taxable value, the electric share, the fit of the bound rows, the 2030-31 score, and the holders and list price the zero-EPB rule removed at the stage's weights. Each is over many households, and none is a record's value. The sign-off sections carry:

  • gate ids and their predicted verdicts;
  • both build 1s' attempt ids and report digests;
  • register entry counts and the national registers' stale-risk and near-bound counts;
  • the reviewer, the adjudication and policy dates;
  • test paths.

Per-row errors, the local registers' stale-risk and near-bound counts and per-area support figures stay in the private review files outside every git work tree.

🤖 Generated with Claude Code

Gate-only sign-off from dense build 1 of the local cut built at 329a99e
(attempt uk-local-candidate-f100-s42-20261102T182958Z-78667c3a), drafted by
tools/draft_uk_dense_signoff.py and approved by María on 2026-11-03
(microcosm#1301 route). The national target-fit register adds four rows and
retires one; the local target-fit register adds eleven cells and retires
twelve; the area-support entries are kept. Every signed row stays bound in the
solve: the entries are scoped to the local line and suppress only their
release fence, so the reuse build at this commit keeps build 1's solve.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@juaristi22
juaristi22 changed the base branch from uk-savings-costing-anchor-1418 to main November 3, 2026 12:04
juaristi22 added a commit to PolicyEngine/policyengine.py that referenced this pull request Nov 3, 2026
The local area moves from 6528c865 to microcosm-uk-2024-25-dense-20261103T092045Z-dbacb023,
built from PolicyEngine/microcosm#1428 at 329a99e71 (the national's commit and
spine) with its gate-only sign-off (PolicyEngine/microcosm#1491), on
policyengine-uk 2.165.1. Both cuts now certify on their own build, so the
regional accepted-mismatch record and its input-surface audit drop out: area
runs read every input the engine takes, company cars and ISA payments
included. certify-data, the release lock, the TROs and the bundle checks are
rerun; the lock and the household snapshots are unchanged. The certified
local-area constants in the tests follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Review of 1affbc1

I checked where #1428 merged against the commit these cuts were built from, the four files in the diff, and the reuse build's published gate report and staged manifest for dbacb023 (read-only, from the private staging repo; aggregates only below). I also ran the gate bindings on the committed registers to test the body's scope claims. Paths are relative to packages/microcosm-build/src/microcosm/build/.

Should

1. Both cuts predate the banding fix that #1428 merged with, and the body doesn't say so.

2. The merge-commit requirement isn't enforced anywhere. The repo allows squash, rebase and merge commits, main has no branch protection and no rulesets, and nothing in uk_runtime/ checks that a cut's git_commit is an ancestor of main (rowwise_staging.py:800 only copies it into the manifest). A squash would leave 1affbc127 reachable only from refs/pull/1491/head. Ask: after the merge, run git merge-base --is-ancestor 1affbc127 origin/main and note the result here.

Nit

3. The body reads as if the high-value homes check blocked build 1 and is signed here. It wasn't, and nothing signs it.

  • uk_high_value_homes_support is diagnostic (uk/gates.json:1841-1852) and has no register. Build 1's run manifest lists only uk_target_fit and uk_local_target_fit as blocking.
  • The published cut still fails it: the staged manifest's failing_gate_ids is [uk_high_value_homes_support] and the enforcement block lists it under diagnostic_failures.
  • Counts from the gate's own details:
Cell Areas assessed Fewer than 3 sources One source over half the cell
Constituency, over £1.5m 248 8 66
Constituency, over £2m 141 4 28
Local authority, over £1.5m 176 2 38
Local authority, over £2m 110 2 15
  • Ask: reword to "blocked by target fit; the diagnostic high-value homes support check also fails and ships failing", with these counts or a pointer to them.

4. The body's window gives area entries an expiry of 2027-02-03, but this PR adds no area entries. None of the six kept entries carries that date. Drop it or say it would apply to new ones.

Checked and fine

  • Gainers: re-measured on f6e33b93 (María's 2 Nov measurements on SPI donor savings rebased to SPI's 2023-24 bands (savings follow-up to #1418) #1428): 418,000 gain by £369 on average in 2028-29, against HMRC's 400,000 at £380. That is the pre-fix national (item 1).
  • Signed rows: 4 new national rows sit 26% to 34% off target, and 11 new local cells 25% to 34% (median 27%), against the 25% bound. All 13 retired entries are back inside the bound on the reuse build's target diagnostics. The reuse report has no stale, dormant, expired or premature entries in either fit register. The counts match the table and the changelog: national 8 to 11, local 37 to 36.
  • Scope, tested on the committed registers through _evaluate_target_fit:
    • every entry applies on the local line only; on a national or undeclared line all 11 national and 36 local entries stand aside and the breaches fail;
    • a signed row back inside the bound fails as stale (national and local);
    • the new national entries are premature before 2026-11-03 and expire after 2026-12-01;
    • 4 national and 12 local kept entries from the 19 October batch expire after 2026-11-16, which is the assembly deadline the body gives.
  • Solve: both fit registers are in UK_GATE_ONLY_RESOURCES, so the sign-off doesn't change the solve key. test_uk_solve_scope.py passes.
  • Receipts: every new entry names attempt …20261102T182958Z-78667c3a and gate report sha256 7f7d66cc…57f8, approved 2026-11-03 by juaristi22, adjudication microcosm#1301. The reuse report evaluated exclusions on 2026-11-03. I couldn't verify the sha256: build 1's staged prefix holds only the orrery manifests, not the gate report.
  • Tests and changelog: the pins in test_uk_terminal_gates.py match the registers. The fragment's counts match the body, and its name follows the earlier 1301-uk-dense-signoff-* ones.
  • Merge: the branch merges into current main without conflicts, and test_uk_terminal_gates.py passes on the merge result.

What I ran: a fresh clone with uv sync --all-packages --locked, deleted afterwards. At 1affbc127, test_uk_terminal_gates.py, test_uk_solve_scope.py and test_uk_dense_signoff_draft.py pass (368 passed). On a local merge with main, test_uk_terminal_gates.py passes (62 passed). I also ran a scratch probe of the scope claims above.

Not verified: any build, build 1's gate report, and how far the banding fix moves these signed rows.

CI at this head: no run had started for 1affbc127 when I posted.

Verdict: the sign-off does what it says for the cut as built, and nothing here is must-fix. Item 1 needs a decision before #731 leaves draft.

🤖 Generated with Claude Code

juaristi22 and others added 3 commits November 3, 2026 13:51
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
María's ruling of 3 November 2026 (microcosm#1418, the company car
base). A tape row's EPB is drawn on uniforms the car never conditions, so
dropping the car where EPB is zero lost about 272,000 weighted holders and
GBP 10.6bn of list price on f6e33b93, at the stage's weights.

- A tape row whose drawn EPB is zero keeps its twin's car. Its car and
  fuel benefits are capped at its PAY within the pay its employee pension
  contribution leaves, come out of employment_income as on any tape row,
  and move from hmrc_spi_pay into hmrc_spi_employment_benefits, so its
  PAY + EPB + TAXTERM stays the tape's. A row whose PAY can't hold the car
  still loses it. Rows with positive EPB keep the cap at EPB.
- The stage rewrites hmrc_spi_pay and hmrc_spi_employment_benefits, and
  the carve declares pay_column (schema, both manifests, graph cells).
- The lost cars' fuel and price draws go, with assign_fuel_types's
  unfueled_electric parameter, which only they used.
- Receipt: holders.spi_dropped_without_epb becomes
  holders.spi_channel_tape_without_epb, and spi_carve_out adds
  holders_without_epb, kept_without_epb and
  weighted_pay_moved_into_epb_gbp. Two invariants join the stage-health
  gate: a tape row's PAY below zero, and a tape row's PAY + EPB moved.
- The engine test gains a tape row without EPB; the coverage manifest and
  the H2 fixture follow the manifest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
María's ruling of 3 November 2026 (microcosm#1418, the company car
base): bind HMRC's 2024-25 holders and taxable value, and the three fuel
counts, to bring the fuel mix to HMRC's shares rather than the FRS
respondents'. If the fuel counts distort calibration, the fuel types are
redrawn instead.

- Five national rows in a new family, hmrc_benefits_in_kind, from HMRC
  Benefits in kind statistics (July 2026) in Chronicle 08962f1: Table 1's
  car recipients and car taxable value, and Table 2C's recipients by fuel
  (fully electric; petrol including hybrids, the engine's PETROL and
  ALTERNATIVE_FUEL; diesel including hybrids). The counts count people
  with a company car list price. No uk-data metric holds them.
- The taxable value is the engine's company car benefit in 2024-25,
  through a dated measure, company_car_benefit_2024 (measurement_period
  2024, require_matching_fact_period), as the CGT rows read their gains:
  the calibration year's appropriate percentages are a point higher. The
  measure resolver, the frame adapter's value_period and the measure
  input route know it.
- Two holds in uprating_holds.json, signed for the ruling: the taxable
  value as matched_period, the four counts as reviewed_no_index (no index
  projects company car holders; HMRC's 2025-26 figures are due July 2027).
- The company car fact fence keeps the vendored tables out of the solve,
  as imputation seeds, and names the bound rows in a new calibrated_facts
  field (schema, both manifests, code). The stage notes, the statistics
  module, the stage-health docstring and the doc say what is bound.
- The plain-column period register reviews the two stored inputs the
  counts filter on: the list price as a sign filter, and the fuel type,
  which nothing uprates.
- The national references and membership and the national compile-parity
  receipt are regenerated on the pinned feed (08962f1): five new rows,
  every other reference unchanged.
  The coverage manifest and the H2 fixture follow the manifest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@juaristi22 juaristi22 changed the title Sign off the target-fit rows for the #1428 local cut (dbacb023) Sign off the #1428 local cut's target-fit rows (dbacb023), and the company car base Nov 3, 2026
@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Round 2 at dd99a1d

You asked for a review of the company car work while the dataset builds, so this round covers d0e87370e and dd99a1dcd. I checked them against HMRC's published tables, ran the car tests, and probed carve_spi_benefits directly, without an engine. Paths are relative to packages/microcosm-build/src/microcosm/build/.

Round 1 items

  1. Both cuts predate 2dbf7cf56. Partly fixed. UK scorecard: populace_uk_2023 vs enhanced_frs_2024_25 vs admin benchmarks (2026) #731 says the national is from before 2dbf7cf56. This body still only says "329a99e71 is in main". UK scorecard: populace_uk_2023 vs enhanced_frs_2024_25 vs admin benchmarks (2026) #731's local section says dbacb023 carries the SPI donor savings rebased to SPI's 2023-24 bands (savings follow-up to #1418) #1428 fixes, but like the national it lacks 2dbf7cf56.
  2. Merge commit not enforced. Open, and it can only be checked once this merges: git merge-base --is-ancestor 1affbc127 origin/main.
  3. High-value homes wording. Partly fixed. UK scorecard: populace_uk_2023 vs enhanced_frs_2024_25 vs admin benchmarks (2026) #731 words it right ("the diagnostic high-value-homes support check fails"). This body still lists it among the things that blocked build 1.
  4. Area expiry 2027-02-03. Open. The body still gives it, and the PR still adds no area entries.

Should

5. A tape row with a small positive EPB now loses a car that the same row with EPB of zero keeps. uk_runtime/company_cars.py:1119-1121: source = np.where(without_epb, spi_pay, epb). The budget jumps from PAY to EPB as soon as EPB is above zero.

  • I ran carve_spi_benefits on three otherwise identical rows: pay £50,000, no employee contribution, a £40,000 car at 25% (£10,000 benefit).
Drawn EPB Result
£0 keeps the £40,000 car (benefit £10,000)
£100 car dropped (budget £100 is below the £5,000 minimum price)
£2,000 car kept, price scaled down to £8,000
  • The commit's own reason for the fix applies here too: EPB is drawn on uniforms the car never conditions. So a small drawn EPB says as little about the car as a zero one does. The cliff still drops and scales cars, and it shrinks the list price base the 2030-31 rise acts on. That base is measured, not bound. The holders and taxable value targets will pull weight back, but not the prices.
  • Suggested fix: cap every tape row at EPB + PAY, within the pay its employee pension contribution leaves. Take EPB first, and move only the part above EPB from PAY into EPB. The zero-EPB rule then becomes the special case, PAY + EPB + TAXTERM still holds, and both new invariants still apply. This changes every EPB > 0 row whose car is scaled or dropped today: those rows would keep more car. If you'd rather keep the cliff, the minimal ask is a receipt count of the EPB > 0 holders scaled or dropped for want of EPB, next to kept_without_epb.

Nit

6. A scaled car can empty the row's PAY. With PAY £6,000, TAXTERM £24,000 and EPB zero, the £40,000 car scales to £24,000. All £6,000 of PAY moves into EPB, so the row's cash pay is TAXTERM alone. That is within the rule. But under PolicyEngine/policyengine-uk#2823 (open), a Class 1A rise passes into employment income, and on these rows it would hit #2823's cash-pay floor first. #2823's wage solver acts only on a reform's change, so I don't expect it to move the stage's baseline pay identity gate (checked by reading, not run). If the next build pins an engine with #2823, a receipt count of zero-EPB keepers left with no PAY would show how many rows this affects.

7. The dated car measure's name year isn't checked against its measurement_period, unlike CGT. uk_runtime/measure_simulation.py:1145-1155 refuses a CGT binding whose name year differs from its measurement_period. require_matching_fact_period only ties the fact to measurement_period (target_materialization.py:748). So a later rename to company_car_benefit_2025 that keeps measurement_period: 2024 would price at 2025-26 percentages against the 2024-25 fact and still compile. Today's binding is consistent. Reusing the CGT check would stop that drift.

8. Holding the counts flat will likely understate 2025-26. The hold declares this, but for the record: over the last three years HMRC's Table 1 holders went 760,000, 840,000, 920,000, and Table 2C's electric cars went 220,000, 340,000, 465,000. Bound at 2025, the counts and the electric share are probably low. The reviewed_no_index reason could say so.

Checked and fine

  • HMRC figures: checked against the GOV.UK 2026_Tables_1_to_4.ods (sha256 a0ee5617…c678, the same file as the vendored rows' source_sha256):
    • Table 1, 2024-25: car recipients 920,000; car taxable value £3,070m.
    • Table 2C, 2024-25: diesel (including hybrids) 60,000; petrol (including hybrids) 390,000; fully electric 465,000; total 920,000.
  • Fuel gap: 2C has no other or unknown row. Its cells are rounded to the nearest 5,000, so 915,000 against 920,000 is rounding, as the doc says. Binding both leaves the solve a 0.5% tension, well inside the fit bound.
  • Fuel mapping: 2C's "petrol (including hybrids)" = PETROL + ALTERNATIVE_FUEL, and FRS hybrid and "other" both map to ALTERNATIVE_FUEL (company_cars.py:762-766). So the few diesel hybrids and PHEVs count with petrol, and the doc says so. Diesel is DIESEL_RDE2 + DIESEL_NON_RDE2. Electric is ELECTRIC.
  • Measurement year: the frame's period is the build's 2024-25, so company_car_benefit_2024 prices the stored cars under 2024-25 percentages against a 2024-25 fact. That matches the CGT precedent, and the matched_period hold fits.
  • Fact fence: the vendored hmrc_benefits_in_kind_facts.json is loaded only by the company cars stage and the graph kernels. measure_simulation.py and ledger_targets.py import only the year regex. The five targets come in through ledger_selector from the Chronicle feed. The fence itself is an equality check on a declaration, so this is a convention rather than an enforced barrier, but it holds today.
  • Zero-EPB carve:
    • The cap is min(PAY, pay - employee). moved is at most the cap, so PAY never goes negative.
    • SPI PAY is already non-negative and finite upstream (spi_income.py:2458-2466), so the new spi_pay_below_zero invariant is a backstop.
    • PAY + EPB + TAXTERM is preserved, so employment_income and the PAY + EPB auxiliaries don't move.
    • test_a_row_without_epb_is_capped_at_its_pay and the stage-level test (tests/engine_free/uk/test_uk_company_cars.py:477, :650) cover this path. My own probe agrees: PAY £500 or £0 drops the car, and a £45,000 employee contribution on £50,000 of pay scales it to £20,000.
  • Double counting: none per row. EPB > 0 rows still carve inside EPB. Zero-EPB rows move their benefit from PAY to EPB. Aggregate hmrc_spi_employment_benefits rises by weighted_pay_moved_into_epb_gbp, but no target or measure reads it. It appears only in the non-engine column lists (uk/gates.json:1441, terminal_gates.py:429).
  • The cause: HMRC's quality report says payrolled cars aren't on the P11D but are reported through RTI. So if SPI's EPB comes from the P11D, a car on a zero-EPB row is plausible in the real data too. That fits the ruling, and item 5 is the same logic.
  • Holds: both new entries in uprating_holds.json carry approved_by: juaristi22, approved_on: 2026-11-03 and expires_on: 2027-01-03, tracking microcosm#1418.
  • Tests: at dd99a1dcd, 10 engine-free files pass: company cars, its stage health, population targets, target references, uprating holds, measure simulation, measurement period, terminal gates, source stages and the coverage manifest (329 passed, 5 skipped).

CI at this head: lint, wheels, integration-uk and engine-uk (3.13, 3.14) pass. engine-free is still pending. engine-us fails on both Pythons in test_us_coverage_is_exact_complete_and_honest (seed protocol digest). It isn't from this PR: the test passes locally at dd99a1dcd and fails on main c8984c44b alone, after the ACS merges.

Verdict: the car work does what the body says, and the HMRC rows and fuel mapping check out against the source. Item 5 is the one I'd settle before the next national, because it leaves a cliff in the same place this fix removes one.

🤖 Generated with Claude Code

Vahid's round 2 on microcosm#1491 (at dd99a1d).

- Item 7: the measure resolver refuses a binding that measures a dated
  company car benefit at any period but the year in its name, as it does
  a dated CGT measure. A rename to company_car_benefit_2025 that kept
  measurement_period 2024 would otherwise price 2025-26 percentages
  against the 2024-25 fact and still compile. Tested at 2023, 2025 and no
  declared period.
- Item 6: spi_carve_out counts the rows without EPB whose car takes all
  of their PAY (kept_without_epb_leaving_no_pay), whose cash pay is then
  their termination pay alone, ahead of an engine with the cash-pay floor
  of PolicyEngine/policyengine-uk#2823.
- Item 8: the counts' reviewed_no_index hold says that held flat they
  probably understate 2025-26, with HMRC's last three years of holders and
  electric cars.

Item 5, the cap on tape rows with positive EPB, waits for María's ruling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@juaristi22

Copy link
Copy Markdown
Collaborator Author

Thanks, Vahid. In your order:

Round 1 items

  1. The pair predates 2dbf7cf56. Both bodies say so now: this one under "Merging", and Certify the Microcosm UK pair from microcosm#1491 on policyengine-uk 2.165.1 (national 0639b9c7, local area 83802786) policyengine.py#731's local-area section. The national built from this PR's head (dd99a1dcd, below) carries it.
  2. Merge commit. After the merge I'll run git merge-base --is-ancestor 1affbc127 origin/main and post the result here.
  3. High-value homes. Reworded. Build 1 was blocked by target fit only. The diagnostic check also fails, the cut ships failing it, and nothing here signs it. The body points to your counts.
  4. Area expiry. Dropped. The PR adds no area entries, and the six it keeps keep their own windows.

Round 2

  1. The cliff at small EPB. Still open. Your EPB + PAY cap would replace the ruling that keeps the cap at EPB wherever EPB is positive, so it waits for that decision. If taken, it comes as its own commit, with a national to measure it.
  2. Rows left with no PAY. spi_carve_out.kept_without_epb_leaving_no_pay now counts them (5304b3c50), with a test.
  3. The dated measure's year. The measure resolver now refuses a binding that measures company_car_benefit_<year> at any other period, as it does a dated CGT measure. It's tested at 2023, at 2025 and with no declared period.
  4. Flat counts. The hold's reason now says they probably understate 2025-26, citing your series.

CI. engine-us fails on main too, after the ACS merges, as you found, so it isn't from this PR.

The national from dd99a1dcd (3 Nov, policyengine-uk 2.165.1, feed 08962f1, same spine recipe as f6e33b93): built and certified shippable. Weighted totals:

f6e33b93 dd99a1dcd HMRC 2024-25
Holders 473,000 913,000 920,000
Car taxable value £2.40bn £3.06bn £3.07bn
Fully electric share 31.9% 50.7% 50.8%
Diesel share 27.7% 6.5% 6.6%
List price base £17.2bn £33.9bn not published
  • The five bound rows sit within 0.5% of HMRC at the calibration weights.
  • The rest of the fit is unchanged: 98.6% of rows within 10% (98.5% on f6e33b93), and the final loss is a little lower.
  • The 2030-31 rise (Table 6.1 row 86), as the dashboard scores it:
£m 2030-31 2031-32
f6e33b93 +125 +129
dd99a1dcd +228 +234
Table 6.1 +310 +345
  • What's left of the gap is the list price base. The OBR's Class 1A line implies about £60bn of list price; ours is £40bn in 2030-31. We also hold holders flat after 2024-25, while the costing grows car volumes, which is why 2031-32 falls further behind.

🤖 Generated with Claude Code

Vahid's review of microcosm#1491, item 5, which María took on 3 November
2026 for later builds. The pair built from dd99a1d keeps the cap at EPB
on the rows that drew EPB.

- Every tape row's car and fuel benefits are capped at EPB + PAY within
  the pay its employee pension contribution leaves. EPB holds them first,
  and the part above EPB moves from hmrc_spi_pay into
  hmrc_spi_employment_benefits, so PAY + EPB + TAXTERM stays the tape's.
- Under the old cap, rows with positive EPB were capped at EPB alone. A
  row that drew GBP 100 of EPB lost the car that the same row with none
  kept, and GBP 2,000 of EPB shrank a GBP 40,000 car to GBP 8,000. EPB is
  drawn on uniforms the car never conditions, so a small EPB says no more
  about the car than a zero one does. The zero-EPB rule is now the special
  case.
- The carve declares the rule (cap: epb_then_pay) in the schema, both
  manifests and the fixture, so a declaration for the old cap is refused.
- Receipt: kept_with_epb_topped_up_from_pay counts the rows with EPB whose
  car takes more than it. kept_without_epb_leaving_no_pay becomes
  kept_leaving_no_pay, any row whose car takes all of its PAY.
  cap_below_epb_for_the_employee_contribution becomes
  cap_below_epb_and_pay_for_the_employee_contribution.
- Tests:
  - the carve-out tests take the new cap, with the review's three-row
    probe;
  - the identity test's flat engine prices above any row's pay;
  - the engine test's row with EPB of GBP 1,500 now keeps its car and
    moves the rest of its benefits from PAY.
- The doc, the changelog fragment, the coverage manifest and the H2
  fixture follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@juaristi22

Copy link
Copy Markdown
Collaborator Author

@vahid-ahmadi, item 5 is in 6ef5983e9:

  • The cap. Every tape row's car is capped at EPB + PAY, EPB first, and the part above EPB moves from PAY into EPB.
  • The declaration. The carve declares the rule (cap: epb_then_pay), so a declaration for the old cap is refused.
  • The receipt. It counts the rows with EPB topped up from PAY (kept_with_epb_topped_up_from_pay). The item 6 count now covers any row a car leaves with no PAY (kept_leaving_no_pay).
  • Tests. Your three-row probe is a test, and the engine test's row with EPB of £1,500 now keeps its car.

It's for later builds. The local cut being built now from dd99a1dcd pairs with the national built there, so both keep the EPB cap on rows that drew EPB.

🤖 Generated with Claude Code

juaristi22 and others added 2 commits November 4, 2026 09:28
Gate-only sign-off from dense build 1 of the local cut built at dd99a1d
(attempt uk-local-candidate-f100-s42-20261104T000247Z-3a5b4051), drafted by
tools/draft_uk_dense_signoff.py and approved by María on 2026-11-04
(microcosm#1301 route). The national target-fit register adds three rows
and retires three; the local target-fit register adds nine cells and
retires ten; the area-support entries are kept. Every signed row stays
bound in the solve: the entries are scoped to the local line and suppress
only their release fence, so the reuse build at this commit keeps build 1's
solve.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The local cut microcosm-uk-2024-25-dense-20261104T093024Z-83802786 was
built at dd99a1d and assembled by the reuse build at 5293966, the
gate-only sign-off of its build 1. Merged with a merge commit, so the
cut's build commit stays in main's history once this PR merges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
juaristi22 added a commit to PolicyEngine/policyengine.py that referenced this pull request Nov 4, 2026
The national moves from f6e33b93 to
microcosm-uk-2024-25-national-20261103T154302Z-0639b9c7, built from
PolicyEngine/microcosm#1491 at dd99a1dcd on policyengine-uk 2.165.1, and the
local area from dbacb023 to microcosm-uk-2024-25-dense-20261104T093024Z-83802786,
built from the same commit and spine with its gate-only sign-off (5293966d4).
Both certify on their own build. certify-data, the release lock, the TROs and
the bundle checks are rerun; the household snapshots are unchanged. The bundle
versions follow main's 6.13.0 (bump_version.sync_bundle_versions), the
certified constants in the tests follow uk_qualification.rewrite_certified_constants
plus the local-area id, and one changelog fragment describes the new pair.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
juaristi22 and others added 2 commits November 4, 2026 15:29
Main brings #1426 (SPI age bands and #1095's B3 to B10 items, with HMRC's two
High Income Child Benefit Charge rows), #1391 (QRF gate calibration) and the
rest merged since 3 November.

Resolved by hand:
- uk_population_targets.json keeps both sides' rows: the five company car rows,
  then the two HICBC rows, in the targets and the registry scope, and both
  sides' unmapped declarations;
- uprating_holds.json keeps both sides' holds;
- the counts in test_uk_population_targets.py (467 targets, 405 in the registry
  scope, 220 unmapped declarations) and test_uk_target_references.py (1,425
  active references) add both sides' rows.

Regenerated on the merged tree, after syncing the venv to the merged lock:
- the target references and their membership, on Chronicle 08962f1. Against
  main only the five car rows are new; no existing row changes;
- the national compile-parity receipt;
- the release input coverage manifest: the source-manifest hash, and
  company_cars' two pay columns;
- the H2 UK spine fixture: company_cars' declaration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The company car rows (dd99a1d) added hmrc.benefits_in_kind to the contract's
hierarchy, which the local references restate, but the local surface wasn't
regenerated with them. Regenerated from the pinned feed (Chronicle 08962f1),
it differs from the committed file by that category alone: the 27,168 local
references and the local membership are unchanged.
test_committed_surfaces_regenerate_from_pinned_feed[local] caught it; it
needs the feed, so it skips without one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Round 3 at c2f91e2

This round covers 6ef5983e9 (the EPB + PAY cap), 5293966d4 (the 4 Nov sign-off for 83802786), the main merge 22bda9511 and c2f91e201. I read the code and tests and checked the registers at each commit. I couldn't install the environment here, so I didn't run any tests: the test arithmetic below is checked by hand, and the rest relies on CI. Paths are relative to packages/microcosm-build/src/microcosm/build/.

Earlier items

  1. The pair predates 2dbf7cf56. Fixed. The body says so under "Merging", and UK scorecard: populace_uk_2023 vs enhanced_frs_2024_25 vs admin benchmarks (2026) #731 now ships 83802786 with the national 0639b9c7, both from dd99a1dcd, which has 2dbf7cf56 in its history.
  2. Merge commit not enforced. Open, as expected: it can only be checked after the merge. It now covers three commits, not one (item 9).
  3. High-value homes wording. Fixed.
  4. Area expiry 2027-02-03. Fixed.
  5. The cliff at small EPB. Fixed in 6ef5983e9. uk_runtime/company_cars.py:1129-1130 caps every tape row at min(EPB + PAY, pay - employee), and :1392-1397 moves only max(carved - EPB, 0) from PAY into EPB. My three-row probe is now test_a_small_epb_caps_a_car_no_more_than_none: all three keep the £40,000 car.
  6. Rows left with no PAY. Fixed. It is counted as kept_leaving_no_pay, which now covers any row.
  7. Dated measure year. Fixed. uk_runtime/measure_simulation.py:1151 refuses a company_car_benefit_<year> binding measured at another period.
  8. Flat counts. Fixed. The hold's reason (uk/uprating_holds.json:470) says the counts probably understate 2025-26 and cites the series.

Should

9. The title and body describe only the 3 Nov sign-off, but the registers at head are the 4 Nov sign-off's, which added 12 entries and retired 7 of the 3 Nov ones.

  • The body was last edited at 00:16 on 4 Nov, before 5293966d4 (09:28). At head the registers are 5293966d4's, unchanged by the main merge.
  • Counts:
    • The body's table says national 4 added and 1 retired, and local 11 added and 12 retired. Against main, head adds 5 national entries and retires 2 (8 to 11), and adds 13 local cells and retires 15 (37 to 35).
    • 5293966d4 retires 2 of the 4 national rows and 5 of the 11 local cells that the 3 Nov sign-off added.
  • Receipts: the body names only attempt …20261102T182958Z-78667c3a and report 7f7d66cc…57f8. The three new national entries and nine new local cells cite attempt …20261104T000247Z-3a5b4051 and report 365f2878…0795.
  • Window: the body says "fit entries expire 2026-12-01". The 4 Nov entries expire on 2026-12-02.
  • Merge commit: the body says only 1affbc127 must stay in main's history. 83802786 was built at dd99a1dcd and assembled at 5293966d4, and 5293966d4 is reachable only as the second parent of fab42beff. A squash merge would drop all three.
  • Stale wording: "the national built from this PR's head (dd99a1dcd)": the head is now c2f91e201.
  • Ask: retitle to cover 83802786, add a section for the 4 Nov sign-off (the commit message and changelog.d/1301-uk-dense-signoff-2026-11-04.changed.md already have the facts), and name all three commits in the merge note.

Checked and fine

  • The pre-fix claim: 2dbf7cf56 is not an ancestor of 1affbc127. It is an ancestor of dd99a1dcd. The body is right that dbacb023 (build 1 at 329a99e71, reuse at 1affbc127) and f6e33b93 both lack it.
  • The 4 Nov sign-off text:
    • Its counts match the registers: national 3 added and 3 retired, local 9 added and 10 retired, 13 retired in total, as the commit message and fragment say.
    • All 12 new entries apply on the local line only, cite the new attempt and report digest, and run from 2026-11-04 to 2026-12-02.
    • No entry was renewed.
  • Assembly deadline: in the two fit registers, the 4 national and 12 local entries from the 19 Oct batch are still the earliest to expire (2026-11-16). The degenerate register's earliest is 2027-02-10, the same as on main. I didn't check the measure exclusions.
  • Holds: both car holds survived the hand-resolved main merge, still juaristi22, 2026-11-03 to 2027-01-03.
  • The cap, by hand:
    • The budget is at most EPB + PAY, so the amount moved is at most PAY, and PAY can't go negative.
    • The test cases check out: EPB £2,000, PAY £2,000 and pay £4,000 scale the £40,000 car to £16,000. A £150 contribution on £3,000 of pay fits £2,850. A £28,000 contribution on £30,000 leaves an £8,000 car. The contribution's room costs £600 and £40,000 on the two cars it touches.
  • Old declarations refused: a carve declaration without cap: epb_then_pay is refused (company_cars.py:434-445). The changelog and the doc (docs/uk-company-cars-316.md:88) describe the new cap. María's reply here and UK scorecard: populace_uk_2023 vs enhanced_frs_2024_25 vs admin benchmarks (2026) #731 both say 83802786 keeps the old EPB cap on rows that drew EPB.
  • c2f91e201: the change adds only the hmrc.benefits_in_kind category (4 lines) to uk/local_target_references.json. No reference or membership changes.

CI at this head: lint, wheels, integration-uk, installed-emitter and select-countries pass. engine-free, engine-uk and engine-us (3.13, 3.14) were still pending when I posted.

Verdict: every earlier item is fixed apart from item 2, which waits for the merge. The car code is right as far as reading it shows. What's left is item 9: the title and body are behind the registers they would merge.

🤖 Generated with Claude Code

@juaristi22 juaristi22 changed the title Sign off the #1428 local cut's target-fit rows (dbacb023), and the company car base Sign off the local cuts' target-fit rows (dbacb023, 83802786), and the company car base Nov 4, 2026
…dules

ledger_targets reads company_car_dated_benefit_year from
hmrc_company_car_statistics to measure a dated company car measure at its
year (dd99a1d), as it reads uk_cgt_dated_measure_year from cgt_calibration,
which is listed. The import check that came with the main merge found the
edge unlisted. Listing the module re-keys both UK target kernels; its one
first-party import, ledger_fact_vendoring, is listed already.

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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants