HMRC personal tax adapter (mode 1): liabilities tables + ready reckoner - #45
Conversation
Third UK mode-1 adapter (#36, parent #3). Two artifacts, one source lane: the July-2026 Income Tax liabilities statistics (SPI-based, 2023-24 outturn + projections to 2026-27) and the June-2025 tax ready reckoner (still current; HMRC deferred the scheduled July 2026 update). 1,213 tidy rows at data/externals/hmrc-personal-tax.json: - Table 2.1: taxpayer counts by marginal rate / sex / age, 1990-91 to 2026-27 ([not applicable] band-structure cells skipped, not suppressed) - Table 2.5 x 4 year-sheets: counts and liabilities by income range x band, plus totals/averages - the full ready reckoner: 75 illustrative changes x 3 forecast years across 12 tax heads, emitted as reform deltas whose subgroup is the verbatim change description ((cost)/(yield) asymmetry preserved), with the indexed-baseline identity recorded as baseline_key in source.json per #13 Validation pins 10 figures: summary-statistics headlines (36.7M/40.8M taxpayers, band counts, GBP274bn 2023-24 liability), a 2.1-vs-2.5 cross-table equality, and two reckoner cells. Annotations record the delta-not-level contract, HMRC behavioural adjustments (static PE counterparts expected to overshoot where elasticities bite), SPI-projection vintage, liability-vs-receipts, and devolved-rates scope. Lane hmrc-personal-tax: registered -> ingested. Closes #36 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
HMRC publishes ready-reckoner effects as magnitudes with the direction
carried only in the English label, so a cost and a yield are both
positive: 'Increase additional rate by 1p (yield)' is +£145m and
'Decrease additional rate by 1p (cost)' is +£175m in 2026-27. A PE
counterpart computed as (reform - baseline) returns -£175m for the cost
row and reads as a 100% divergence -- or matches under any abs()
comparison, which is worse. Genuine negatives (28 of 225 rows) sit in
the same column, so the mixed convention is invisible by inspection.
Every revenue_effect row now carries:
sign_convention 'magnitude_with_direction_in_label'
direction cost | yield | stated_change
change_direction increase | decrease | unsigned
Both are read off the verbatim label only. Explicit (cost)/(yield)
markers give cost/yield; everything else is stated_change -- no
direction is invented for the plain 'Change X by 1p' form, and an
unmarked 'Increase ...' is NOT assumed to be a yield, because HMRC
publishes several of them negative ('Increase lower Capital Gains Tax
rate by 10 percentage points' is -£130m in 2026-27). Labels the
classifier cannot place raise rather than defaulting. No value is
changed or flipped; levels rows are untouched.
tests/test_hmrc_adapter.py covers this in CI, which never runs sources/
and so never saw the adapter's inline checks. It pins the row count,
asserts every reckoner row is classified, and asserts the additional
rate (cost)/(yield) pair is both positive AND separable by direction --
the exact ambiguity that motivated the change.
annotations.json gains reckoner-sign-convention stating the convention
as a comparison constraint, so the honesty record matches the data.
1213 rows, all 10 inline checks pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixed: ready-reckoner rows had a sign convention nobody declaredThe reckoner emitted HMRC's own convention verbatim — positive magnitudes with the direction living only in the English label: Both positive. A PE counterpart computed as (reform − baseline) returns −£175m for the second and scores as a 100% divergence; an Every
Census over the 225 rows: cost 42, yield 36, stated_change 147. What I deliberately did not doBoth fields are derived from the verbatim label and nothing else. Explicit They state the direction of the change, not of the fiscal effect, and those come apart in this data. So No value changed, none flipped, levels rows untouched (byte-compared against Test
Verification
Follow-up (not this PR)#48 maps these rows to |
Closes #36. Third UK mode-1 adapter (parent #3), and the external side the campaign's
hmrc_reckoner_t2family (14 staged PE results) compares against.What's here
sources/hmrc-personal-tax/raw/— the collated Income Tax liabilities tables 2.1–2.6 (July 2026 edition, 2023-24 SPI outturn + projections to 2026-27) and the June 2025 tax ready reckoner (still the current edition — HMRC deferred the scheduled July 2026 update pending an assumptions review)sources/hmrc-personal-tax/adapter.py— stdlib-only parser → 1,213 tidy rows atdata/externals/hmrc-personal-tax.jsonsource.jsonwith an explicitbaseline_keyblock (per Baseline as a first-class attribute of every score #13) + 7 annotationsdata/lanes.json—hmrc-personal-tax:registered → ingestedCoverage
[not applicable]cells (bands that didn't exist that year) are skipped as structure, not suppressed as data.Validation
10 inline checks pass: HMRC summary-statistics headlines (36.7M taxpayers 2023-24 → 40.8M 2026-27; 31.4M/7.70M/1.29M/415k by band; £274bn total liability 2023-24), a cross-table equality (Table 2.5 all-ranges band counts ≡ Table 2.1 marginal-rate counts), and two reckoner cells (basic rate 1p = £6.9bn, employer NICs 1pp = £11.15bn in 2026-27).
Honesty boundary
Annotations pin the comparison contract: reckoner rows are deltas against HMRC's indexed baseline (OBR Spring Statement 2025, April 2026 start) and include HMRC's behavioural adjustments — a static PE counterpart is expected to overshoot where elasticities bite, and that wedge is construction, not model error. Projected years (2024-25 onward) are forecast-vs-forecast. Liabilities are accrued, never comparable to receipts. Tables 2.2 (region/country) / 2.4 / 2.6 are vendored for a follow-up emission.
🤖 Generated with Claude Code