Repository navigation
UK: company cars from the FRS job table, and policyengine-uk 2.165.0 (dashboard #316) - #1430
juaristi22 wants to merge 18 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 |
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>
|
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.
2. The pay identity check couldn't fail.
3. The engine-uk failure. 4. The 2.164.3 and 2.164.4 predictor moves. They are in Results' gate and fit movement list:
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:
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 |
Independent review of #1430 at
|
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
uk/hmrc_benefits_in_kind_facts.jsonvendors the 96 2024-25 rows the stage reads, and is registered incountry_package.json.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).obr.capital_gains_tax,obr.state_pensionand theobr.nitotal. They now pin October's outturns. None of them is in the fit.obr.winter_fuel_allowancereads October's FY2025-26 (+0.02%).Merge order
gates.jsondigests, the coverage manifest, the H2 fixture, and the national references, membership and compile-parity receipt.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.ledger_targets.pyand its tests, as this PR does for the table map. Whichever lands second expects a conflict there and keeps both changes. UK: take up #1095's actionable items on policyengine-uk 2.165.1 #1422 stays on Chronicle 53815b2 (María's call), so the table maps arrive with this PR.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(María's ruling of 30 October). They are capped at EPB, and at the pay the row's pension deductions leave:salary_sacrificekeeps employee contributions and salary sacrifice within pay and bonus sacrifice within half of it, and the carve-out keeps them there.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.
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.
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.COMPANY_CAR_TAX_RISE_2030against 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.obr.income_tax, andobr.ni_employer(Class 1A);other_investment_incometomarket_income,household_market_incomeandhbai_household_net_income, and 2.164.4 tocapital_income. Those feed stage predictors, so the draws below can move for households with other investment income:was_wealth, which materializeshousehold_net_incomeandcapital_income;was_lisa(household_net_income);lcfs_consumptionandetb_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;test_uk_frs_spine.py;test_uk_measure_simulation.py.The engine tests on 2.165.0 show that:
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:
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.shwas not run locally; CI'sintegration-ukruns 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
🤖 Generated with Claude Code