Repository navigation
UK: company cars from the FRS job table, and policyengine-uk 2.165.0 (dashboard #316) - #1430
juaristi22 wants to merge 12 commits into
Conversation
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>
…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>
|
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, Findings1. 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.
2. should: the SPI pay-identity check in the release-blocking gate can't fail. 3. should: one CI failure is not on the body's expected-red list. 4. should: the lock step pulls in more than the body lists.
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 5. nit: unknown-fuel draws never produce a hybrid. 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 VerdictSound 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 ranFresh clone at 21e3623 and
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 |
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.
company_cars, that fills six of those inputs.Without the stage, a file on 2.165.0 still scores the rise at £0.
Status: draft, CI expected red
Three things are still to come:
The HMRC facts. They come from Add HMRC benefits in kind statistics, July 2026, and keep one OBR EFO number per line and year chronicle#399 (HMRC Benefits in kind statistics, July 2026: 544 facts, raw workbook in R2). Once it merges, this PR:
docs/uk-chronicle-feed-repin.md);uk/hmrc_benefits_in_kind_facts.jsonand adds itscountry_package.jsonentry.Until then, every test that builds the UK graph fails on the missing resource, and so do the stage-plan tests.
The regenerations. The H2 fixture and the coverage manifest are regenerated once the facts are vendored; until then their parity and preflight tests fail.
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
gates.jsondigests, the coverage manifest and the H2 fixture.company_car_*inputs at their default (UK_ENGINE_DEFAULT_INPUTS). Whichever lands second removes only the six this stage writes. Availability, private-use payments and the 6 April 2020 registration flag stay signed.Changes
policyengine-uk 2.165.0.
other_investment_incomein market and HBAI income, which the spine fills (UK: data fixes for the Budget 2026 household measures #1418), so HBAI-based outputs move too.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):
They are dropped at the release boundary.
The
company_carsstage, the last in the roster, afterisa_subscriptions, whose income cells reademployment_income. Nothing reads the pay it rewrites. Every draw is keyed on identity.SPI-channel pay. It is the tape's PAY + EPB + TAXTERM, so the engine's car and fuel benefits come out of
employment_income, capped at EPB (María's ruling of 30 October):Pay plus the benefits is the tape's total on every such row.
Targets.
hmrc.spi.employment_income.amount_by_total_income_bandand the localhmrc.employment_income.amountreademployment_income_with_benefits_in_kind_annual, a new named term for employment income plusbenefits_in_kind, which HMRC's employment income counts. The two count rows keepemployment_income_annual.The facts.
hmrc_company_car_statistics.pyreads 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:Gates.
uk_stage_company_cars_invariantsis release-blocking and runs on the synthetic smoke too. It fails on: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.
COMPANY_CAR_TAX_RISE_2030against Table 6.1 row 86 (+£310m).obr.income_tax,obr.ni_employer(Class 1A), and HBAI income (2.164.3).Tests
test_uk_company_cars.py, engine-free and engine (22 tests);test_uk_company_cars_stage_health.py;test_uk_frs_spine.py;test_uk_measure_simulation.py.tools/run_integration_tests.shand the H2 parity test.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
🤖 Generated with Claude Code