Skip to content

UK: company cars from the FRS job table, and policyengine-uk 2.165.0 (dashboard #316) - #1430

Draft
juaristi22 wants to merge 18 commits into
mainfrom
uk-company-cars-316
Draft

juaristi22 wants to merge 18 commits into
mainfrom
uk-company-cars-316

Conversation

@juaristi22

@juaristi22 juaristi22 commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

What and why

Budget 2026 raises every company car appropriate percentage by one point in 2030-31: Table 6.1 row 86, +£310m in 2030-31 and +£345m in 2031-32 (OBR split: income tax 226.2, NICs 92.4). The dashboard can't score it (PolicyEngine/uk-autumn-budget-dashboard-2026#316), because neither the engine nor the data had company cars.

Without the stage, a file on 2.165.0 still scores the rise at £0.

Status: draft

  1. The HMRC facts are in. The feed is re-pinned to 08962f1, the merge of Add HMRC benefits in kind statistics, July 2026, and keep one OBR EFO number per line and year chronicle#399 on top of UK facts for chronicle#395: Northern Ireland domestic rates, Welsh bus journeys, the Childcare Offer for Wales, VED by payer and CPI gas weights chronicle#396 (1,512,256 rows; the schema is unchanged). uk/hmrc_benefits_in_kind_facts.json vendors the 96 2024-25 rows the stage reads, and is registered in country_package.json.
    • The other 35 vendored resources, and the DESNZ constituency resource, carry the same values under the new identity.
    • The local surface is unchanged apart from its feed label and the DESNZ keys.
    • The coverage manifest and the H2 fixture are regenerated.
  2. The OBR windows read both editions. Full eCPS input coverage: 158 required columns, 8 evidence-backed exclusions #399 keeps one OBR EFO number per line and year (OBR EFO March 2026: drop the years the October 2026 vintage also publishes (PolicyEngine/microcosm#1095) chronicle#397), so March 2026 now carries only FY2024-25 and October 2026 carries FY2025-26 on. The 19 OBR targets that window both years into calendar 2025 could no longer compile, since their window took both years from one series. A new selector key, source_table_by_opening_year, takes FY2024-25 from March's table and FY2025-26 from October's (María's ruling of 31 October).
  3. The build. A pair build on 2.165.0 against a control on the same base, with the results below. The machine order is María's.

Merge order

Changes

  1. policyengine-uk 2.165.0.

    • The lock, the floors, the version mentions and the US worker identity's lock digest.
    • The uprating and bonus-sacrifice pins keep every value.
    • Concept coverage gains the nine company car inputs.
    • 2.164.3 also counts other_investment_income in market and HBAI income, which the spine fills (UK: data fixes for the Budget 2026 household measures #1418), so HBAI-based outputs move too.
  2. The FRS job tab's company car answers. The root stage carries four columns per person from their car job with the lowest JOBTYPE (one car per person, as in the engine):

    • the CARVAL band, or "unknown";
    • FUELTYP, or "unknown";
    • EXPBEN03 (private fuel);
    • CARAMT where CARCON says the employee contributed.

    They are dropped at the release boundary.

  3. The company_cars stage, the last in the roster, after isa_subscriptions, whose income cells read employment_income. Nothing reads the pay it rewrites. Every draw is keyed on identity.

    • Holders. FRS-channel people keep their car. An SPI-channel person keeps their twin's only where their drawn EPB is positive.
    • Fuel. From FUELTYP: a hybrid is an alternative fuel; a diesel meets RDE2. An unknown fuel draws at HMRC Table 2C's shares.
    • List price. Drawn within the CARVAL band from WAS round-8 company cars' list prices, grown by the engine's CPI index from 2021. The open and unknown bands draw by fuel class. A cell with fewer than 3 WAS households (the floor of 3) is drawn uniformly.
    • CO2. Zero for an electric car. For the others, HMRC Table 2A's shares among cars that emit CO2, hybrids first into 1 to 50 g/km, uniformly within each band; a car at 1 to 50 g/km takes a 50-mile electric range.
    • Defaults kept. Availability, payments for private use and the 2020 registration flag keep the engine's defaults.
  4. SPI-channel pay. It is the tape's PAY + EPB + TAXTERM, so the engine's car and fuel benefits come out of employment_income (María's ruling of 30 October). They are capped at EPB, and at the pay the row's pension deductions leave: salary_sacrifice keeps employee contributions and salary sacrifice within pay and bonus sacrifice within half of it, and the carve-out keeps them there.

    • Private fuel goes first where its benefit alone exceeds the cap.
    • A car that still doesn't fit has its list price scaled down, with its contributions dropped.
    • Below £5,000 the car is dropped.

    The stage then has the engine price its output again: on every SPI-channel row, pay plus the engine's benefits must equal the pay the row brought in.

  5. Targets. hmrc.spi.employment_income.amount_by_total_income_band and the local hmrc.employment_income.amount read employment_income_with_benefits_in_kind_annual, a new named term for employment income plus benefits_in_kind, which HMRC's employment income counts. The two count rows keep employment_income_annual.

  6. The facts. hmrc_company_car_statistics.py reads Full eCPS input coverage: 158 required columns, 8 evidence-backed exclusions #399's 2024-25 rows by their source record ids, in persons and pounds:

    • a cell the feed lacks is refused, never read as zero;
    • a resource whose Table 2A or 2C total, or electric count, sits from Table 1 beyond each sheet's rounding is refused too.
  7. Gates. uk_stage_company_cars_invariants is release-blocking and runs on the synthetic smoke too. It fails on:

    • a non-finite or negative value;
    • a holder without a price;
    • inputs on a person without a car;
    • an unknown fuel;
    • a CO2 figure missing for a car that emits it, or set for an electric car;
    • private fuel on an electric car;
    • contributions above the price;
    • an electric range outside 1 to 50 g/km;
    • an SPI benefit above EPB;
    • SPI-channel pay plus the engine's benefits, priced again on the output, more than 5p from the pay the row brought in (single precision).
  8. Measured, not bound. No HMRC company car fact is a calibration target: the stage draws to Tables 2A and 2C, so binding them would be circular (María's ruling of 28 October on the ISA facts). The calibrated file is reported against Tables 1, 3A and 4.

Results

PENDING the pair build. Every figure is checked for disclosure before it goes in.

  • Holders against HMRC's 920,000 (2024-25 provisional), and by income range against Table 3A.
  • Car and fuel taxable value, income tax and Class 1A against Table 1 (£3,070m, £1,090m, £420m).
  • The list price base, electric and not (the receipt's list_price.weighted_total_gbp). Two published comparators frame it: HMRC's 2024-25 taxable value implies roughly £27bn to £37bn, and the OBR's NICs line about £60bn.
  • The static 2030-31 cost of COMPANY_CAR_TAX_RISE_2030 against Table 6.1 row 86 (+£310m). Expect it well under £310m, from the size of the base (docs/uk-company-cars-316.md, "What to expect of the 2030-31 score"). The file holds the build year's fleet: fuel and CO2 are frozen, prices grow by CPI, and the weights are not projected. A file that matches Table 1 exactly should still fall about 30 to 45% short.
  • Gate and fit movement:
    • the re-pointed SPI rows, obr.income_tax, and obr.ni_employer (Class 1A);
    • HBAI income. 2.164.3 adds other_investment_income to market_income, household_market_income and hbai_household_net_income, and 2.164.4 to capital_income. Those feed stage predictors, so the draws below can move for households with other investment income:
      • was_wealth, which materializes household_net_income and capital_income;
      • was_lisa (household_net_income);
      • lcfs_consumption and etb_services (hbai_household_net_income).

Tests

  • New:

    • test_uk_company_cars.py, engine-free and engine (22 tests);
    • test_uk_company_cars_stage_health.py;
    • the carrier tests in test_uk_frs_spine.py;
    • the named term in test_uk_measure_simulation.py.
  • The engine tests on 2.165.0 show that:

    • the engine reads the stage's inputs;
    • SPI pay plus the engine's benefits is the tape's total to the penny;
    • the 2030-31 rise reaches the holders alone.
  • The engine-free suite, run locally in chunks on the re-pin, failed only on the national surface this PR then regenerated, and on three local-only checks:

    • the uk-extra-absent test, since this environment installs the extra;
    • a live release-pointer test;
    • the orrery check, whose Node package isn't installed in this checkout.

    After the regeneration, every file that failed passes, as do the touched surfaces' tests.

  • The 16 feed-gated files, one process each against the pinned feed: 250 passed. CI skips them.

  • The national compile-parity regeneration on the full pinned artifact, with its release gate, passes.

  • The H2 parity test passes on the regenerated fixture.

  • tools/run_integration_tests.sh was not run locally; CI's integration-uk runs it.

Disclosure

The FRS and WAS are UK Data Service data (SN 9563, SN 7215). The stage's receipt carries weighted aggregates, rounded counts and support bands only. The HMRC figures quoted here are published.

Not in this PR

  • Class 1A is not passed through to households (Pass Class 1A NICs through the employer NI incidence policyengine-uk#2752). The 2030-31 rise is scored statically, comparable with the OBR's costing.
  • Vans, fuel provided for part of the year, optional remuneration arrangements, and the rest of the "not modelled" list in policyengine-uk#2750.
  • Whether to anchor holders to Table 3A by income range, if the FRS count falls short of HMRC's. That is María's call after the results.
  • The dashboard measure: policyengine.py certifies a build on ≥2.165.0, then the dashboard adopts the release and adds the measure.

🤖 Generated with Claude Code

juaristi22 and others added 12 commits October 30, 2026 13:40
2.165.0 (PolicyEngine/policyengine-uk#2750) models the company car and
car fuel benefits in kind, and the Class 1A NICs on them, from nine person
inputs that default to no car. The lock moves from #1421's 2.164.2 so the
company_cars stage can write them
(PolicyEngine/uk-autumn-budget-dashboard-2026#316).

The uprating and bonus-sacrifice pins keep every value. Concept coverage
gains the nine inputs, and the liquid-assets reason and its test name the
two GBP stocks among them. The coverage manifest and the H2 spine fixture
are regenerated with the stage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first piece of the company car work for
PolicyEngine/uk-autumn-budget-dashboard-2026#316, not yet declared in the
manifest. policyengine-uk 2.165.0 taxes the company car and car fuel
benefits from nine person inputs that default to no car.

- uk_runtime/company_cars.py writes six of them for each employee the FRS
  job record gives a company car (the root stage's carriers, which
  SPI-channel rows inherit from their twins):
  - fuel from the FRS code (a hybrid is an alternative fuel, a diesel meets
    RDE2 as declared), and an unknown fuel at Table 2C's shares;
  - list price within the respondent's band from WAS round-8 company cars'
    prices grown by the engine's CPI index, the open and unknown bands by
    fuel class, and a uniform draw where a cell has too few donor
    households;
  - CO2 at Table 2A's shares among cars that emit it, hybrids first into
    1 to 50 g/km, which takes the declared electric range;
  - private fuel and capital contributions from the job record.
  The engine's defaults stay for availability, private-use payments and
  the 2020 registration flag, which no survey records.
- On SPI-channel rows, whose pay is the tape's PAY + EPB + TAXTERM, a car
  is kept only where EPB is positive, and the engine's car and fuel
  benefits come out of employment_income, capped at EPB: fuel goes first
  where it alone exceeds EPB, and a car that still does not fit has its
  price scaled down (its contributions dropped), or is dropped below the
  declared minimum price (María's ruling of 30 October 2026).
- uk_runtime/hmrc_company_car_statistics.py types HMRC's Benefits in kind
  statistics (July 2026): the Table 2A and 2C margins the stage draws to
  and the Table 1 totals it is measured against. The vendored resource
  waits for the Chronicle package; until then the loader names that.
- Engine-free tests cover the declaration checks, the donor, the draws'
  bounds and margins, identity keying and the carve-out arithmetic; the
  engine tests check that policyengine-uk 2.165.0 reads the inputs, that
  SPI pay plus its benefits is the tape's total, and that the 2030-31 rise
  reaches the holders alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The root stage now writes four carriers the company_cars stage reads to
set policyengine-uk 2.165.0's company car inputs
(PolicyEngine/uk-autumn-budget-dashboard-2026#316):
frs_company_car_price_band (CARVAL 1 to 9, or 10 for "Don't know" or
unanswered), frs_company_car_fuel_code (FUELTYP 1 to 5, or 6 likewise),
frs_company_car_private_fuel (EXPBEN03) and frs_company_car_contribution
(CARAMT where CARCON is 1, one total to date). A person's company car is
the one on their EXPBEN01 job with the lowest JOBTYPE, so a person has at
most one, as in the engine. The survey asks CARVAL and CARCON of a company
van too; they are taken as the car's.

SPI-channel rows inherit their FRS twin's carriers, as they do every root
column. The carriers are no engine inputs: they are declared in both
manifests, typed in the graph's root cells, and dropped at the release
boundary, and the H2 fixture tool's synthetic job tab gives two employees a
car (the fixture is regenerated with the stage).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The receipt's checks block counts the rows breaking an invariant any
population keeps, for the stage-health gate to require all zero: a
non-finite or negative value, a holder without a list price, an input
written for someone without a car, an unknown fuel type, a car that emits
CO2 without a figure, an electric car with one, private fuel on an electric
car, contributions above the list price, an electric range outside 1 to 50
g/km, and an SPI benefit above its EPB. FRS holders without pay are
reported, not failed: the benefit is taxable whatever the pay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For PolicyEngine/uk-autumn-budget-dashboard-2026#316, before the stage is
declared:

- gates.json: uk_stage_company_cars_invariants (stage_health, transferred,
  release-blocking, no population fact check, so the synthetic smoke
  build blocks on it too) fails on any invariant break the receipt counts
  and on SPI-channel pay plus its carved benefits off the tape's total by
  more than maximum_pay_identity_error_gbp. stage_health gains the
  company_cars check; the spine gate scope, the battery's parameter keys,
  the microcosm-data contract mirrors and every digest the edit moves
  follow (the battery's policy, manifest and fingerprint, the spine and
  release-cut part digests).
- The export surface allows the six person inputs the stage writes, in
  both allow-lists in the same order.
- The source manifest's operation kinds and the sources schema gain the
  stage's six kinds, each a closed branch.
- The stage reads its carriers' names from frs_spine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For PolicyEngine/uk-autumn-budget-dashboard-2026#316:

- uk/spec/sources.yaml, and source_stages.json (its typed projection),
  declare company_cars as the last stage, after isa_subscriptions, whose
  income cells read employment_income: nothing then reads the SPI-channel
  pay the stage rewrites. It reads the vendored HMRC benefits-in-kind facts
  and the WAS round-8 person tab, and has eight operations (the facts
  check, the donor, fuel, list prices, CO2, the SPI carve-out, the mass
  receipt and the reviewed fence). It writes six non-negative person
  inputs and rewrites employment_income.
- Graph and build wiring: the stage's WAS person source, an open read
  surface (one engine materialization over the frame), its typed cells,
  and a reader-isolation boundary, since it rewrites a cell the
  open-surface readers before it bind; the kernel binds its module and
  helpers and its fixture implementation; the spine build gets its
  implementation, its WAS person input and its identity-keyed seeds.
- The stage's output constants follow the manifest (the six inputs and
  the rewritten pay).
- Fixtures and tests: the H2 fixture tool's WAS persons carry company
  vehicles and the household weight, and its stage count is 51; the
  roster tests follow, and isa_subscriptions is no longer the last stage
  in its notes and test.
- docs/uk-company-cars-316.md and the changelog fragment.

The graph tests need the vendored facts resource, which waits for the
Chronicle package: until it is vendored they fail on the missing
hmrc_benefits_in_kind_facts.json, and the H2 fixture and coverage manifest
are regenerated with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HMRC's employment income counts taxable benefits in kind, which
policyengine-uk 2.165.0 keeps out of employment_income. SPI-channel pay is
the tape's PAY + EPB + TAXTERM, from which the company_cars stage now
carves the company car and car fuel benefits, so the rows on HMRC's
employment income would lose them (María's ruling of 30 October 2026,
PolicyEngine/uk-autumn-budget-dashboard-2026#316).

- A named term, employment_income_with_benefits_in_kind
  (employment_income + benefits_in_kind), and its annual measure on the
  annual income simulation.
- hmrc.spi.employment_income.amount_by_total_income_band and the local
  hmrc.employment_income.amount read it: on SPI-channel rows that is the
  tape's total as before the carve-out; on FRS rows, pay plus the car
  benefits. Their notes say so. The two count rows keep filtering on
  employment_income_annual, which only asks whether a person has pay.

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

The local hmrc.employment_income.amount row now reads pay plus
benefits_in_kind, so the stub engine the local metric tests run on
declares it (zero for everyone, so no expected figure moves). The
private-school attendance stage is followed by isa_subscriptions and now
company_cars, neither of which writes its flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A list price band now draws from the WAS round-8 donor cars when at least
3 households carry them, the floor María set for every UK output on 29
October 2026 and CD171's absolute floor, instead of 10. The stage still
refuses a declared minimum below 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loader reads the 2024-25 rows of hmrc-benefits-in-kind-july-2026
(PolicyEngine/chronicle#399) by their source record ids,
<record set>.<row>.<measure>, in persons and pounds as Chronicle carries
them: Table 1's car and fuel recipients, taxable value, income tax and
Class 1A liabilities, Table 2A's twenty CO2 bands, Table 2C's three fuels,
and Tables 3A and 4 by income range and region. Chronicle emits no fact
for a cell HMRC marks as suppressed, so a cell the stage needs and the
feed lacks is refused, never read as zero, and a resource whose Table 2A
or 2C total, or electric count, sits from Table 1 beyond the sheets'
rounding is refused too.

The band and fuel ids are Chronicle's row ids (co2_0 ... co2_170_plus;
diesel_including_hybrids, petrol_including_hybrids, fully_electric), and
the stage's verify_vendored_fact_resource declaration names the twelve
2024-25 record sets. The test support builds a synthetic vendored payload
and types it with the same loader.

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

Copy link
Copy Markdown
Contributor

Review of 21e3623 (draft, so an early look as a comment). First round; there are no earlier comments.

The stage writes what policyengine-uk 2.165.0 expects. I checked the nine inputs at the 2.165.0 tag. The six written ones have the right units, periods and enum names (GBP stocks uprated by CPI, g/km, miles, CarFuelType names, bool). The draws are keyed on identity, there is one car per person by lowest JOBTYPE, household weights pass through, and no company car fact is bound. The stage is the last spine stage, so calibration still runs after it: it adds no weights, but the solve then moves holders, which fits "measured, not bound". There are four shoulds and one nit.

Findings

1. should: on a frozen fleet, expect the static 2030-31 score well under £310m. The gap comes from the size of the base, not the EV share.

  • The rise is one point in every band, zero-emission and the maximum included. So the extra taxable value is about 1% of total list price (less contributions), plus 1% of the fuel multiplier for each fuel-benefit holder. The frozen EV share barely moves the rise. It moves the baseline level instead (EVs go from 3% to 9% by 2029-30).
  • The base the rise acts on is the build year's fleet, carried forward to 2030-31. Fuel type and CO2 are not uprated, list price and contributions rise by CPI, and the weights are not projected.
  • Rough HMRC-implied base for 2024-25: Table 1 car taxable value £3,070m over Table 2A's band-weighted 2024-25 appropriate percentages gives about £27bn to £37bn of list price (depending on PHEV range and EV price premium). With CPI to 2030-31 and 40,000 fuel holders, that is about £330m to £440m of extra taxable value. At HMRC's 2024-25 ratios (income tax 35.5% of taxable value, Class 1A 15%), that is roughly £170m to £225m.
  • The OBR's NICs line (£92.4m / 15%) implies about £616m of extra taxable value, roughly a £60bn base. Its income tax line gives the same 36.7% ratio. So even a file that hits HMRC's 920,000 holders and £3,070m exactly should come in around 30 to 45% short. Recipients rose from 840,000 to 920,000 in one year, and HMRC flags under-reporting of payrolled cars.
  • Matching Table 1's taxable value does not pin this base: a file can hit £3,070m with a different EV mix and different prices. Suggest the receipt add weighted total list price (by electric and not), so Results can compare it with the HMRC-implied range and read the £310m gap as base size.

2. should: the SPI pay-identity check in the release-blocking gate can't fail.
company_cars.py:983-987 sets new_pay = pay - carve.benefit, then measures |new_pay + carve.benefit - pay|, so max_abs_pay_plus_benefit_error_gbp is zero by construction. That means uk_stage_company_cars_invariants (stage_health.py:4954-4960) never tests the claim in its notes. The real risk is the engine's benefit on the written inputs differing from the carved amount. The price scaling assumes the defaults for availability and private-use payments, and that contributions were dropped, so a later engine change would break it without the gate noticing. Suggest one more materialize of company_car_benefit + car_fuel_benefit on the output frame, comparing each SPI-channel row with carved.

3. should: one CI failure is not on the body's expected-red list.
engine-uk fails on tests/engine/uk/test_uk_annual_income_measures.py:156: set(receipt["computed_measures"]) == set(UK_ANNUAL_INCOME_MEASURES). The test never computes the new employment_income_with_benefits_in_kind_annual. This is independent of chronicle#399. Computing it there, and asserting it equals employment_income_annual when nobody has a car, would fix it. The job stops at this first failure ("1 failed" and nothing passed), so CI has not run the new engine tests in test_uk_company_cars.py yet.

4. should: the lock step pulls in more than the body lists.
2.164.2 → 2.165.0 includes 2.164.3 (other_investment_income in market_income, household_market_income and hbai_household_net_income) and 2.164.4 (also in capital_income). The body mentions only HBAI outputs. These engine values are stage predictors:

  • was_wealth materializes household_net_income and capital_income (was_wealth.py:63-89);
  • was_lisa reads household_net_income (was_lisa.py:164);
  • lcfs_consumption and etb_services use hbai_household_net_income (lcfs_consumption.py:650, etb_services.py:62).

So the wealth, LISA, consumption and services draws can move for households with other investment income. Suggest adding these to Results' "gate and fit movement" list for the pair build. 2.165.0 also adds Class 1A to ni_employer, which the body already covers.

5. nit: unknown-fuel draws never produce a hybrid.
company_cars.py:588 draws only ELECTRIC, PETROL or diesel, and :920 flags hybrids from the FRS code alone. So "petrol/diesel including hybrids" from Table 2C never feeds the hybrids-first split into 1 to 50 g/km. The effect is small, but worth a sentence in the doc.

Checked and fine: the three defaults (availability 1, no private-use payments, the 2020 flag) only matter in 2020-22 or slightly raise each car's taxable value for part-year holders; the WAS donor (cars not vans, household weight, floor of 3); CARVAL/FUELTYP mapping and the open band; CO2 bands within Table 2A's bounds; the SPI carve-out arithmetic (fuel first, exact price scaling, £5,000 floor); the re-pointed SPI targets, since taxable_employment_income now includes benefits_in_kind, so total-income bands agree.

Verdict

Sound design. Items 2 and 3 are small code fixes. Item 1 is about setting expectations before the pair build, and item 4 is about what that build should report. Not ready until the #399 vendoring, the regenerations and the build, as the body says.

What I ran

Fresh clone at 21e3623 and uv sync --all-packages --locked (no UK engine extra), deleted afterwards; one process at a time.

  • Engine-free pytest: test_uk_company_cars.py and test_uk_company_cars_stage_health.py gave 24 passed. With test_uk_frs_spine.py, test_uk_measure_simulation.py, test_uk_source_stages.py, test_uk_terminal_gates.py, the ISA, local-target, private-school and stochastic tests, test_engine_core_pins.py, test_country_spec.py and microcosm-data's test_contract.py, 848 passed and 4 failed. All 4 are KeyError: 'hmrc_benefits_in_kind_facts.json', as expected.
  • I read the 2.165.0 variables, parameters and the company_car_tax_rise_2030 scenario at the tag, and the 2.164.2...2.165.0 compare.
  • CI: engine-free fails on the missing facts and integration-uk on the same resource (expected), engine-uk as in item 3; lint, wheels and engine-us pass.

Not verified: the engine tests (no UK engine locally, no build or Modal), any FRS or WAS figure, and the score itself. Item 1's figures are rough arithmetic from published HMRC and OBR numbers.

🤖 Generated with Claude Code

juaristi22 and others added 6 commits October 31, 2026 01:02
The annual measures receipt lists every measure the test computes, and the
test never computed employment_income_with_benefits_in_kind_annual, so
engine-uk failed on the set comparison. With nobody holding a company car,
the measure is the year's pay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ns within pay

- The stage prices its output again on the engine. On every SPI-channel
  row, pay plus the engine's car and fuel benefits must be the pay the
  row brought in. The receipt records the largest miss rounded up to the
  penny, and uk_stage_company_cars_invariants allows 5p for the engine's
  single precision. The old check compared the carve-out with itself, so
  it could not fail.
- The carve-out's budget also keeps the pay the row's pension deductions
  need. salary_sacrifice bounds employee contributions and salary
  sacrifice by pay and bonus sacrifice by half of it, and a later pay
  rewrite must hold those bounds.
- The receipt records the weighted total list price of electric cars and
  of the others: the base a one-point rise acts on.
- Every weighted figure in the receipt is withheld when a cell it rests
  on has fewer distinct source households than the floor. Shares that sum
  to one are withheld together. Invariant counts are banded.
- docs/uk-company-cars-316.md says what to expect of the 2030-31 score,
  and that an unknown fuel never draws a hybrid.
- The gate battery and certification digests are re-pinned for the
  gates.json change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…benefits in kind facts

The UK Chronicle feed moves to 08962f1, the merge of
PolicyEngine/chronicle#399, on top of 7d5d627, the merge of
PolicyEngine/chronicle#396. It has 1,512,256 rows: 53815b2's 1,511,063,
plus #396's 1,076 UK facts from PolicyEngine/chronicle#395 and #399's 544
benefits in kind facts, less 427 March EFO record sets. The schema digest
is unchanged. The merge commit's tree is the pull request head's
(ac9d7bb), so the artifact was built from that head's bundle and carries
the merge commit's identity.

- Vendored resources:
  - a new hmrc_benefits_in_kind_facts.json (96 rows in six selections),
    registered in the country package: the 2024-25 rows the company cars
    stage reads;
  - the 35 other vendored resources keep their rows under the new feed
    identity;
  - the DESNZ constituency resource keeps its 2,526 values, under keys
    re-derived from the new identity.
- obr.winter_fuel_allowance loses its pin to March's table. #399 keeps
  one OBR EFO number per line and year (PolicyEngine/chronicle#397), so
  FY2025-26 is October's outturn, 0.02% above the March projection.
- The local surface was regenerated from a cut of the artifact by the
  200bf6d rules. The references and the compile-parity receipt are
  byte-identical, the membership moves its feed label and the 1,263
  DESNZ keys, and the census restates the pin.
- The coverage manifest is regenerated. It also catches up with the
  company_cars stage.
- The H2 fixture is regenerated. The fixture tool runs company_cars in
  its legacy plan and audits its fuel type as a string column.

The national references, membership and compile-parity receipt follow in
the next commit. The 19 targets that window FY2024-25 and FY2025-26 into
calendar 2025 cannot compile on this feed: their window takes both years
from one series, and no EFO edition now carries both.

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

- The spine graph test counts 51 manifest stages.
- The acceptance receipt's pending transformations append company_cars
  after isa_subscriptions, as the production plan runs it.
- The facts loader drops its pre-vendoring guard, as the ISA loader reads
  its resource, and its test now loads hmrc_benefits_in_kind_facts.json
  against the feed pin and checks Table 1 and 2C's 2024-25 values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PolicyEngine/chronicle#397 keeps one OBR EFO number per line and year, the
latest edition's, so the March 2026 tables carry FY2024-25 and the October
2026 tables FY2025-26. Each edition's record sets are their own series, so
calendar_year_window found no series with both years.

A new selector key, source_table_by_opening_year, maps each opening year to
the table that carries it, as source_measure_id_by_opening_year does for
measures:

- a fact matches only the table its own year declares;
- the record set drops out of the series identity, so the mapped tables
  form the window's one series;
- a list-valued source_concept still names several series, each read
  across the tables;
- the selector cannot also declare source_table or the measure map;
- calendar_year_window_scaled_by_ratio reads its window the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
María's ruling of 31 October: the 19 OBR targets that window FY2024-25 and
FY2025-26 into calendar 2025 take FY2024-25 from the March 2026 EFO and
FY2025-26 from the October 2026 EFO, each year's latest edition after
PolicyEngine/chronicle#397. Their selectors declare
source_table_by_opening_year, and each note says so.

- Fifteen of the 19 move by under 1%: income tax -0.5% and UC +0.7%. The
  four larger moves are domestic rates +3.2%, Pension Credit +2.3%,
  Jobseeker's Allowance -2.5% and SDLT -1.2%.
- Three report-only diagnostics pinned March's FY2025-26 forecasts, which
  the feed no longer carries: the OBR's capital gains tax receipts, State
  Pension spending and total NICs. They pin October's outturns
  (observation, observed_only), with their notes restated. The NICs total
  now sits 2.1% above the class rows at the window.
- The test feed fixture's captured March rows give way to the feed's
  current ones: October's FY2025-26 income tax, incapacity benefits and
  capital gains tax, and March's FY2024-25 income tax in its v4 form. The
  national calibration tests' synthetic window facts carry each year's
  table.
- Regenerated from the 08962f1 national cut: the national references and
  membership, and the national compile-parity receipt. The receipt's OBR
  rationales name the two editions, and Winter Fuel, which binds FY2025-26
  alone, has a rationale of its own.
- The membership also catches up with policyengine-uk 2.164.1's October-EFO
  indices, which #1421 locked after #1420 had generated it: the eight SPI
  Table 3.10 self-employment bands move -0.55% and the thirteen dividend
  bands under 0.001%. The receipt already had these values.
- docs/uk-income-anchors-280.md describes the map, the re-pin runbook gains
  the 08962f1 entry, and the changelog fragment is complete.

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

Copy link
Copy Markdown
Collaborator Author

Thanks, Vahid. All five items are addressed. Item 2 led to one more fix, and to a disclosure pass on the stage's receipt.

1. The 2030-31 expectation and the list price base.

  • The receipt now records list_price.weighted_total_gbp for electric cars and for the others, each with its support band.
  • docs/uk-company-cars-316.md has a new section, "What to expect of the 2030-31 score". It says why the rise acts on list price, why the frozen electric share moves the baseline but not the rise, and why the static cost should come in well under £310m: the OBR's NICs line implies a base of about £60bn, against roughly half of that from HMRC's 2024-25 taxable value. It credits your arithmetic for the 30 to 45% range.
  • The body's Results now compare the receipt's base with that range, so the gap reads as base size.

2. The pay identity check couldn't fail.

  • The stage now prices its output again on the engine. On every SPI-channel row, pay plus the engine's company_car_benefit and car_fuel_benefit on the written inputs must equal the pay the row brought into the stage.
  • The receipt records the largest miss as spi_carve_out.max_abs_pay_identity_error_gbp, rounded up to the penny. uk_stage_company_cars_invariants allows 5p, for the engine's single precision (maximum_pay_identity_error_gbp: 0.05). The battery and certification digests are re-pinned for that gates.json change.
  • A new test runs the stage with an engine stub whose car benefit ignores the list price. The check reads a miss above £1,000, so it fails as it should.
  • While doing this I found one more thing to fix. salary_sacrifice keeps employee contributions and salary sacrifice within pay, and bonus sacrifice within half of it, and its comment asks any later pay rewrite to hold those bounds. The carve-out didn't. Its budget now also leaves the pay those deductions need, and the receipt counts the rows where that binds (limited_by_pension_deductions).
  • The disclosure pass covers every weighted figure in the receipt:
    • each one is withheld when a cell it rests on has fewer distinct source households than the floor, so support copies count once;
    • shares that sum to one are withheld together;
    • invariant counts are zero exact and banded otherwise.

3. The engine-uk failure. test_uk_annual_income_measures.py now computes employment_income_with_benefits_in_kind_annual and checks that it equals employment_income_annual when nobody has a car. Locally on 2.165.0, that file gives 4 passed and the company cars engine tests 3 passed.

4. The 2.164.3 and 2.164.4 predictor moves. They are in Results' gate and fit movement list:

  • was_wealth (household_net_income, capital_income);
  • was_lisa (household_net_income);
  • lcfs_consumption and etb_services (hbai_household_net_income).

5. Hybrids. The doc now says that an unknown fuel never draws a hybrid, since Table 2C counts hybrids inside petrol and diesel, and that the effect is small.

What I ran:

  • engine-free company cars and stage-health tests: 28 passed;
  • the engine tests above;
  • the contract, gate battery pin, country spec, release certification and terminal gate tests: 523 passed.

The chronicle#399 re-pin and the BiK vendoring are pushed alongside. So is the fix for the 19 OBR windows that #397 split across the March and October EFOs: they take FY2024-25 from March and FY2025-26 from October (María's ruling of 31 October). The body's Status says what each moved.

🤖 Generated with Claude Code

@juaristi22

Copy link
Copy Markdown
Collaborator Author

Independent review of #1430 at e67e76c29

This is an independent adversarial review. Four reviewers each took one area:

  • the stage;
  • facts and windows;
  • a cross-cutting pass;
  • conventions.

A verifier then reproduced each finding on synthetic data or published tables, and a last pass looked for gaps. Vahid's round isn't repeated. Nothing here uses licensed data. Paths are relative to packages/microcosm-build/src/microcosm/build/ unless they start with tools/ or docs/.

Verdict: two things to fix before the pair build (1 and 2), and two that will skew how its Results read (3 and 4).

Fix before the build

1. obr.ni_employer now counts Class 1A; OBR's line doesn't.

  • The row binds ni_employer (uk/uk_population_targets.json:2111). In policyengine-uk 2.165.0 that variable adds ni_class_1a, which is the rate × benefits_in_kind.
  • OBR's line is Class 1 employer NICs only. Class 1A sits in "Other NIC" (October EFO table 4.4, row 21, footnote).
  • Once this stage fills the car inputs, the measure gains Class 1A on them, about £420m in 2024-25 on HMRC Table 1, and the solve moves weight to absorb it.
  • Fix: bind ni_class_1_employer. The notes at :677 and :34829 still say no household carries Class 1A.

2. The carve-out breaks the identity-stability tool.

  • tools/verify_uk_identity_stability.py:2555 requires that pay plus salary sacrifice equal the pre-conversion pay exactly, on every converted record.
  • company_cars lowers SPI pay after salary_sacrifice has run, and keeps no carrier for the carved amount. salary_sacrifice converts SPI rows too (salary_sacrifice.py:658).
  • So a converted SPI person with a car makes --check e4, e5 and e8 raise. At full scale that overlap is near-certain.
  • e8 also re-bands CGT on pay the CGT stages never saw.

What the Results will measure

3. SPI copies keep a car only on an independent EPB draw (uk_runtime/company_cars.py:991).

  • A copy keeps its twin's car only where its drawn EPB is positive. EPB is drawn on identity-keyed uniforms that are never conditioned on the car (spi_rank_transport.py:123; spi_income.py:168-175).
  • The SPI channel holds half of the prior working-age mass (sources.yaml:1415-1416). So before calibration, holders and list price come to about (0.5 + 0.5p) of the FRS level, where p is the share of copies that draw a positive EPB.
  • No car target restores this.
  • The receipt gives only a rounded row count (spi_dropped_without_epb). Meanwhile the doc's "What to expect" puts the whole shortfall against £310m down to base size.
  • Suggestion: record a weighted figure for the dropped holders, or condition the keep on the twin; either would make this measurable.

4. Assumptions documented as "baseline-only" drive the 2030-31 base, through the price scaling (:918-925).

  • A scaled SPI car's price is the room ÷ the appropriate percentage at 2024-25 rates, and the 2030-31 rise is 1% of price.
  • On a £2,000 room and a £45k car, the scaled price is:
    • £25,000 at 8% (the fixed 50-mile range);
    • £14,286 at 14% (under 30 miles);
    • £45,000, unscaled, for an electric car at 2%;
    • £8,000 for an RDE2 diesel at 25%;
    • £6,897 at 29% for a diesel without RDE2.
  • That contradicts sources.yaml:6360 ("matters only to 2027-28") and docs/uk-company-cars-316.md:40-41 and :86-87.

5. The CO2 receipt predates the carve that skews the fleet (:1009-1017, :1147).

  • The carve drops or scales cars in inverse proportion to their appropriate percentage. So electric and low-CO2 cars survive, and high-CO2 cars drop first.
  • On synthetic data, the kept 1-50 g/km share rises from 0.39 to 0.50, and the electric share from 0.25 to 0.38.
  • The receipt reports the pre-carve shares, so "matches Table 2A" isn't true of the output.

6. The list-price base is read at pre-calibration weights (:1134).

  • list_price.weighted_total_gbp is at the stage's weights, which carry the SPI channel's prior 50%. Calibration moves holders after this point, and nothing measures list price on the calibrated file.
  • Yet the doc (:96-99) and the body plan to read it as the score's base.

Stage behaviour

7. Fuel-first ordering drops cars that would fit without fuel (:910-928).

  • With EPB of £9,000, an appropriate percentage of 29%, a £35k car and an £8,062 fuel benefit, nothing is kept. Without the fuel flag, the car is kept at £31,034.
  • This follows the 30 October ruling's wording literally, but the result isn't monotone.
  • Related: an unknown-fuel holder who reported private fuel draws electric at Table 2C's share (about 51%) and so loses that fuel (:1018).

8. Count rows drop SPI rows whose pay the carve takes to zero.

  • The employment-income counts filter on employment_income_annual != 0 (uk/uk_population_targets.json:3696, :31014), while the amounts read the new term.
  • An SPI row whose pay is all EPB can end with pay 0 and its car kept: carve_spi_benefits(epb=2000, pay=2000, retained=0, car=6000) does this. Such a row leaves the counts but stays in the amounts.
  • Suggestion: filter the counts on the new term.

9. The band-1 fallback draws prices from £0 (:197, :735).

  • With no band-1 WAS donors, prices are drawn uniformly on (0, £10,000]. About half fall below £5,000, the least price the carve treats as plausible.
  • A CARAMT capped at a near-zero price then zeroes the benefit.

10. Carving last leaves upstream pay rules unrestored (:188-192).

  • These were all drawn on pre-carve pay:
    • the bonus-sacrifice candidacy (pay ≥ UEL, salary_sacrifice.py:286);
    • UC take-up awards;
    • the Carer's Allowance earnings test;
    • the gainful self-employment flag.
  • So "nothing reads the pay it rewrites" holds only for later stages.
  • Suggestion: split the car part out of EPB inside the SPI income stage. That keeps these rules consistent and also clears item 2.

11. SPI-channel rows that aren't tape rows lose the twin's car (:538-543, :992).

  • Undrawn copies and dependants aged 16 to 19 keep their twin's pay with EPB 0, so no_epb drops the car.
  • spi_income documents person_is_spi_income_recipient for exactly this distinction, and property_components uses it.

Coverage and texts

12. company_cars has no coverage-manifest family.

  • Of the 15 stages that declare record_mass_conservation_receipt, it is the only one without a family_coverage entry. tools/build_uk_release_input_coverage_manifest.py:931 lists isa_subscriptions but not this stage.
  • So release_input_coverage.py never checks its mass record, nor requires it in the plan.

13. Small items:

  • the annual measure scales pay by employment_income_annual_share but not the car inputs, which puts a full-year benefit on part-year FRS pay (small);
  • the BIK fact fence's count is popped but never compared;
  • company_cars.py:196-216 re-declares FRS constants from frs_spine.py:957-963 (one of them unused), against docs/shared-constants.md;
  • _STAGE_HELPER_MODULES["company_cars"] omits uk_engine_adapter and salary_sacrifice;
  • the stage notes omit the pension-deduction cap and the re-pricing check;
  • graph.py:586 still says one engine materialization.

Cross-PR with #1422

Checked and found true

  • The OBR windows. The weights are 3/12 and 9/12, and income tax reproduces exactly. Editions match by row header for all 19 rows, the refusals work, and the value moves are as stated.
  • The BIK facts. There are 96 rows, and the tolerances match Chronicle's stated rounding. The re-pin changes only feed identity on the other 35 resources, and only the keys of the DESNZ resource.
  • The stage's mechanics:
    • the engine inputs' names, units and periods match 2.165.0;
    • the price scaling is exact;
    • the float32 re-pricing error stays under 2.3p;
    • the draws are deterministic;
    • the carriers are dropped at the release boundary.
  • Fixtures. The H2 fixture CSVs and the test frames are synthetic.

Disclosure-control items went to the author separately.

🤖 Generated with Claude Code

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