OBR costings divergence decomposition: axes registry + identity-checked pipeline (#59) - #67
OBR costings divergence decomposition: axes registry + identity-checked pipeline (#59)#67vahid-ahmadi wants to merge 5 commits into
Conversation
…ts from primary sources, identity-checked pipeline Builds #59. Consumes PR #56's COMPARISON.csv by path (no shared commits); a verbatim fixture copy makes the arithmetic executable now. Sized from fetched primary sources: the employer NICs package's missing Employment Allowance construction (+4,581m/+4,631m) and static-vs-post-behavioural difference (+723m/+703m) from the OBR's 8 May 2025 supplementary static-costing release (Tables 1.3-1.5), and HICBC's missing welfare head (+391m/+426m) from the comparison's own head rows. Everything else is registered unsized with an explicit sizing recipe; the pipeline forces decomposition_status=partial and labels the remainder residual_plus_unsized so a partial decomposition never presents as explained. gap = sized + residual holds exactly and is test-pinned. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review — OBR costings divergence decompositionThe mechanical scaffolding (schema, "never explained while unsized" gate, JSON hygiene) is sound, but I think the decomposition has a substantive correctness problem that the current test locks in rather than catches. Requesting changes. The identity "check" is a tautology
Working the employer-NICs 2026-27 row by hand: Sign / attribution of the sized componentsBoth employer-NICs sized components ( Should
To be clear on what's fine: the "never mark Reviewed with Claude Code assistance. |
… diagnostics, quoted-vs-derived contract The identity residual = gap - sum(valued) stays as bookkeeping but no longer masquerades as a check. Every valued component is classified explains_gap / masks_gap against the gap's sign; any masking component sets divergence_understated with the widened like_for_like_gap_gbp, |residual| > |gap| is flagged, and explained_share is emitted only for complete decompositions with no masking components. The behavioural component is re-statused 'derived' (formula mandatory, travels into the artifact); sized now means verbatim-quoted only and load_registry rejects a sized component carrying a derivation. The employer-NICs measure gains its dominant missing component (employee_incidence_and_base, unsized with a paired-run recipe: PE's employee_incidence=1 drives the -8.5bn IT head vs OBR's -0.3bn) and the EA component's provenance now carries the head-attribution caveat from the review. The review's hand-worked row is a pinned regression: gap -7.364bn, both valued components masking, residual -12.668bn exceeding the gap, no explained claim. Tests 10 -> 17 (synthetic masking/overshoot cases that fail on the old semantics); full suite 180 passed / 4 skipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
All three points addressed in ff38e0e, including the substantive one. Point by point: The tautology. Agreed — Sign / attribution. I re-checked the orientation chain and the two components' signs are individually defensible (EA-absence contributes +4.58bn to PE−OBR because the missing elements are revenue-reducing in OBR's leg; static−post-behavioural is +723m by Table 1.5 arithmetic) — but you're right that presenting them as decomposition steps on this row was wrong, and right about the head attribution: the EA effect lands on the NICs head where PE is only +0.83bn above OBR, so ~−3.75bn of offsetting NICs-base factors must coexist, and the total gap is dominated by the IT head (PE −8.51bn vs OBR −0.31bn) — which is PE's Derived vs quoted. The behavioural component is re-statused Tests 10 → 17; full suite 180 passed / 4 skipped; ruff clean. |
Re-review — request-changes resolved ✅This fully addresses my earlier review — nicely done, and thank you for the rework. Verified against the live pipeline (17 tests pass; 12 records, all
Remaining bits are honestly disclosed and non-blocking (the EA head-attribution still awaits the paired run to confirm; a render-only unicode console quirk). Happy to see this move forward — clearing my change request. |
DTrim99
left a comment
There was a problem hiding this comment.
Re-reviewed — my earlier change request is fully resolved: the identity is de-tautologized with failable checks, the employer-NICs components are reclassified masks_gap (I reproduced the arithmetic), and the behavioural term is status: derived under an enforced sized-vs-derived contract. Approving — thanks for the thorough rework.
Gate round 1 — the arithmetic closes; the decomposition doesn't yetClean: exact arithmetic closure on all 12 fixture rows, 13 focused tests, formatting.
The shape to aim for: each axis = an executed paired run (issue #78's job), sized contributions in exhibits, and 🤖 Generated with Claude Code |
1. The focal divergences are no longer wholly undecomposed. Recipes are EXECUTED now, not asserted: a `computed` component names an executor the pipeline runs against the comparison rows on every run and may not carry a hard-coded value at all. The adjacent-CY 3:1 interpolation is the first, and it sizes FY2026-27 for both March-2026 measures from the fixture's own runs — +£1.443bn on the PA/HRT freezes and +£23.5m on the additional-rate reduction, the figures the review worked by hand. Both residuals are now smaller than their gaps. FY2027-28 needs a CY2028 run the fixture does not have, so it stays unsized and names the missing run rather than extrapolating. 2. The diagnostics can fail. `residual` is no longer an axis — it was both a registry axis and the quantity the pipeline computes, so a component could be filed under the name of the thing it explained. The employer-NICs "behavioural" term becomes static_vs_post_behavioural_joint on a new joint_behavioural_and_scope axis: it subtracts a full-measure static total from a mapped-head post-behavioural subtotal, so it carries scope and basis together and cannot be attributed to behaviour. Employment Allowance becomes `derived` — it sums Table 1.3 and Table 1.4 and the source prints neither total. HICBC's welfare_head_missing declares that it overlaps its own take-up axis; overlapping valued components are recorded and withhold explained_share, because two terms sharing a channel are not additive. And explained_share outside [0, 1] is withheld with a stated reason: the synthetic test used to assert 1.5, which is a residual of the opposite sign wearing an explanation's name. 3. The axis definitions match the engines. The registry claimed OBR has "no comparable pass-through"; the October 2024 EFO assumes 60% in 2025-26 rising to 76%, with indirect effects reported separately — so the recipe now says a PE incidence 1->0 pair sizes PE's WHOLE wage channel and the wedge needs OBR's separately-reported leg too. The PA/HRT baseline recipe no longer proposes substituting earnings determinants PE 2.89.2 already uses (differencing a world against itself) and separates data uprating from threshold indexation; the counterfactual it actually needs, obr_announcement_baseline_efo_march_2026, is now a REGISTERED world — it was claimed as a baseline while being registered nowhere. Every CY derivation states the equal-accrual assumption and the FY-END-year identity convention. 4. It stands alone and it persists. A clean invocation fell over on #56's absent COMPARISON.csv; it now falls back to the committed fixture and SAYS SO in the output and in a PROVENANCE.json, and it compares the fixture against #56's live file when both exist so a frozen snapshot cannot silently miss later baseline fixes. scorecard_db/ingest_obr_divergence.py registers the lane in build_db — the artifact used to vanish on every CI rebuild — and writes it as "not run" when no decomposition exists rather than writing nothing. Suite 277 passed, two builds agree on content_hash, no-drift clean, ruff format clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
|
All four addressed in
Suite 277 passed, two builds agree on |
data/lanes.json and its app/public mirror are DERIVED feeds and both sides appended a lane, so the textual conflict is not a real one — regenerated from a build rather than hand-merged. 33 lanes now, including the BE country-report lane from #82 alongside this branch's divergence-decomposition lane. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
The #30 pattern applied to the mode-2 UK lane: every #56 comparison row decomposes into signed GBP components summing exactly to gap = PE − OBR, with a residual, under two honesty rules enforced by the pipeline and tests — a sized component must carry per-FY values with verbatim fetched-primary-source provenance, and any unsized component forces
decomposition_status: partialwith the remainder labelledresidual_plus_unsized, neverexplained.What's sized with real published numbers (fetched, quoted in the registry)
Files
data/uk/obr_divergence_axes.json— 6-measure registry (JSON, not YAML: CI installs pytest only). Axes: behavioural_adjustment, baseline_vintage, cy_proxies_fy, head_scope, construction_scope, residual.pipeline/decompose_uk_obr_divergence.py— engine-free csv/json arithmetic; emitsresults/uk/obr_divergence/DECOMPOSITION.{json,md}; a measure whose registered row_kind is absent errors rather than decomposing a different scope.tests/fixtures/obr_costings_comparison_20260817.csv— verbatim copy oforigin/obr-costings-mode2:results/uk/obr_costings/COMPARISON.csv(provenance in the test module docstring) so the pipeline is executable before OBR policy costings (mode 2): registry, offline certified compute, staging, descriptive comparison #56 merges.tests/test_obr_divergence_decomposition.py— 10 tests: registry contract (sized⇒provenance, unsized⇒recipe), the exact identity, partial-never-explained, the pinned employer-NICs and HICBC sizings, missing-row refusal.Suite: 173 passed / 4 skipped; ruff clean.
Dependency: consumes #56's
COMPARISON.csvby path only — no shared commits; when #56 merges, running the pipeline against the real artifact replaces the fixture path. 12/12 decompositions arepartialby design until the unsized axes get their runs.Builds #59.
🤖 Generated with Claude Code