HMRC ready reckoner (mode 2): registry, offline compute, full-population triage - #66
HMRC ready reckoner (mode 2): registry, offline compute, full-population triage#66vahid-ahmadi wants to merge 5 commits into
Conversation
… compute skeleton, full-population triage - data/uk/hmrc_reckoner_reforms.json: all 75 (program, label) reckoner families triaged (24 expressible / 22 partial / 29 not_expressible), deltas not absolute schedules, program-prefixed measure keys (VAT/IPT share a verbatim label), paths_verified flag distinguishing OBR-registry-verified paths from candidates. - pipeline/compute_uk_reckoner.py: engine-free-importable compute with --dry-run registry validation and a path-resolution gate; a delta is never applied to a guessed parameter (whole-measure pe_gap demotion). - tests/test_reckoner_registry.py: 12 tests (whole-population join, slug rule, signed-direction consistency, fraction-not-pp rate deltas, delta arithmetic, staged-row contract, engine-free dry run). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review — HMRC ready reckoner (mode 2) registryVerified the core claims directly against the registry and
Should address (robustness nits, none blocking)
Note18 of 24 "expressible" measures have Reviewed with Claude Code assistance. |
…ine-instant note - test_rate_deltas_are_fractions: parenthesized so a future .rates.*.threshold path ending in "main" is not miscaught by and-binds-tighter-than-or. - head_var lookup uses .get() with a pe_gap demotion instead of a bare KeyError for a future expressible measure outside the three programs. - reform_values documents the mid-year baseline read vs full-year reform window: identical for flat-year params, revisit for step-mid-year paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
All three robustness nits fixed in the latest commit:
On the note: agreed the Suite: 175 passed / 4 skipped; ruff clean. |
Re-review — all addressed ✅
Coverage 75, VAT/IPT split, and deltas-not-schedules all still hold. Good to go. |
DTrim99
left a comment
There was a problem hiding this comment.
Re-reviewed — all findings addressed (test precedence parens, head_var .get()+gap fallback, mid-year read documented). Approving.
Gate round 1 — the declarative triage is right; six findings, five blockingThe closed 24/50 declaration over the 74 distinct labels (VAT/IPT duplicate deliberate) is correct and matches the merged registry. The findings are in what happens at runtime:
The path of least rework: treat the existing 14 as the anchor set, extend with the producer/claim_id-direct pattern (#72) for the new measures and years, and stage through the campaign contract rather than a parallel shape. 🤖 Generated with Claude Code |
…tract 1. The merged campaign's 14 resolved reckoner results are the ANCHOR SET. Every (measure, year) now resolves to exactly one ingested claim and is reconciled against them before anything is emitted: an anchored claim is retained and this lane emits nothing for it, and the reconciliation REFUSES to run if any of the 14 is unreachable. Without that, wiring the lane in would have shadowed 11 via latest-result selection and regressed 3 — savings allowance, dividend allowance, savings starting-rate limit — to pe_gap despite exact executed paths on main. The manifest records retained vs new and is testable. 2. Reproducibility. The registry pins policyengine-uk 2.89.2 and the run refuses any other version, on the import AND on the managed sim's own reported version — the earlier dry-run passed under whatever ambient engine happened to be installed (2.75.3) while the prose said 2.89.2, so resolving every path proved nothing about the run's engine. The run writes per-measure artifacts plus a manifest carrying a sha256 over every artifact and the staged file. The run itself is still #77; what is fixed here is that it can no longer be reproducible-in-prose. 3. Baseline honesty is encoded. The run executes CURRENT LAW while the claim keys hmrc_indexed_baseline_spring_2025; both keys are stamped on the staged row and on the artifact so the guard can verify the two worlds differ, and successes emit `constructed` rather than `ok` — which is not a valid ComparisonStatus at all. Modelled on the merged campaign rows. 4. The runtime no longer contradicts the triage. The three acknowledged partial, missing-leg reforms carried deltas and passed the compute gate, giving 27 ok / 48 gap against a declared 24 / 51; their deltas are now null with the reason recorded, the validator rejects a partial that carries one, and the declared triage is checked against the number of measures that actually carry a delta. Both employer-NIC measures read `ni_employer` — the head the committed campaign artifacts establish — via a registry field rather than a dict inside the runtime, and every delta-carrying measure names its head. 5. Gap and magnitude semantics. All 29 not_expressible measures carry an action_link (gate #9), the registry's free-text note travels onto the staged row instead of being dropped, and annotations are a LIST OF STRINGS — the ingest contract — carrying the established abs(PE)/abs(HMRC) ratio, the sign convention, the CY/FY proxy and the basis mismatch. 6. The staging shape can ingest. Rows stage claim_id-direct through the campaign contract, which needs no family normalizer, instead of naming hmrc-personal-tax / revenue_effect / a string FY where DB claims are uk_hmrc / revenue_change / FY-END integers; computed_at and data_bundle are present. The validator is year-aware: a missing or duplicated FY claim now fails instead of passing. Suite 272 passed, build deterministic, ruff format clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
|
All six addressed in
Suite 272 passed, build deterministic, ruff format clean. |
Verified against the 2.89.2 wheel: policyengine_uk exposes no __version__ attribute at all, so the pin check I added read None and would have refused to run on the CORRECT engine — the gate was unusable rather than strict. importlib.metadata.version() is the version of record; __version__ stays as a fallback in case a future release adds one. With that fixed the dry-run now RUNS against a real pinned 2.89.2: registry valid, and every one of the 24 delta-carrying measures' parameter paths resolves against the live tree. The pin gate is also confirmed live — bumping the registry pin refuses with the installed version named. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
|
Follow-up on the engine pin, because I found a defect in my own fix and it is worth being explicit about. I installed a real policyengine-uk 2.89.2 rather than reasoning about it, and the pin check I added was broken in the direction that matters least visibly: With that fixed the dry-run actually runs against the pinned engine, which is the thing this PR could previously only claim:
So finding 2's "reproducible" claim is now evidenced up to the engine boundary. The managed run itself is still #77. |
What's here
data/uk/hmrc_reckoner_reforms.json— measure → reform registry covering the entire external reckoner population (75(program, verbatim label)families from the committeddata/externals/hmrc-personal-tax.json; a test pins registry == external vocabulary, so coverage gaps are labelled, never silent). Triage: 24 expressible / 22 partial / 29 not_expressible, each with a note. Registry stores deltas (absolute or relative), never absolute schedules — the reckoner's own construction is baseline-plus-illustrative-change, and the pipeline reads the live baseline value per year.reckoner_202506__vat__change_standard_rate_by_1_percentage_point): HMRC prints the identical label under both VAT and IPT — the same defect class the UK externals → DB ingest path (16,924 claims, five sources) #48 review caught — and the label-only key collided exactly there; a validator defect list plus a duplicate-key test pin it.pipeline/compute_uk_reckoner.py— offline compute under the UK counterpart compute pipeline (managed sims, pe_gap-degrading registry) #51/OBR policy costings (mode 2): registry, offline certified compute, staging, descriptive comparison #56 conventions: managed sims on the certified bundle, one baseline per year reused across measures, per-run artifacts, staged JSONL under the campaign ingest contract (external_claim_matchwith the verbatim label as subgroup), signed PE delta plus the adapter'smagnitude_with_direction_in_labelorientation annotations (the pipeline never flips the external value).--dry-runvalidates the registry and, when policyengine-uk is importable, resolves every parameter path — a delta is never applied to a guessed parameter; any missing path demotes the whole measure tope_gap.tests/test_reckoner_registry.py— 12 tests: whole-population join, slug rule, expressible⇒delta, rate deltas are fractions not percentage points, directioned labels carry matching delta signs, delta arithmetic, staged-row contract, engine-free import and dry run. Suite: 175 passed / 4 skipped; ruff clean.Honest caveats
paths_verified: truemarks only paths resolved by the OBR registry (obr-costings-mode2) against pe-uk 2.89.2 (PA amount, uk/dividends band thresholds); NICs, child benefit and rate-field paths are candidates flaggedfalseand must resolve in--dry-runfirst. policyengine-uk was not installed at authoring.uv run --with pytestwith no PyYAML, which is why OBR policy costings (mode 2): registry, offline certified compute, staging, descriptive comparison #56's pytest job currently fails onimport yaml; this lane stays stdlib-parseable.Builds #60.
🤖 Generated with Claude Code