Baseline integrity: does the certified world carry the law legislated into its future? (#99) - #101
Baseline integrity: does the certified world carry the law legislated into its future? (#99)#101vahid-ahmadi wants to merge 5 commits into
Conversation
…ture (#99) The repo has a baselines registry that DESCRIBES worlds and ingests that stamp which world a run executed. Nothing checked that the certified policyengine-uk world CONTAINS the measures already legislated into the years it computes at — a gap that is quiet by construction, because the resulting divergence gets attributed to whatever axis the analyst reaches for and the real cause never appears. Five measures, probed against a real policyengine-uk 2.89.2. The result is mixed, which is why the registry records verifications and not only failures: carried two-child limit abolition — 2 in 2025, unbounded from 2026. This also confirms the registered pre_ab2025 world is a genuine counterfactual and not a restatement of the baseline. carried income tax threshold freeze — flat 12,570 / 37,700 through 2028, which is what makes a freeze measurable only against an INDEXED counterfactual (#67's axis). consistent fuel duty freeze extension. No single parameter encodes "a freeze was extended", so rather than guess I checked it against OBR's own scored CPI profile from #75: negative in 2026-27 (-0.129pp), positive in 2027-28 (+0.076pp) — the signature of a freeze that ends and catches up, and the certified rate path steps up in exactly that year. missing High Value Council Tax Surcharge (April 2028) missing property income tax rates (April 2027) Both missing measures are already law and neither has any representation at the pin. The exposure envelope is quantified from the built database: 953 UK claims sit at or beyond 2027 and 581 at or beyond 2028. That is an ENVELOPE TO TRIAGE, not a defect count — whether a given claim moves depends on the quantity, and a council-tax surcharge does not touch a taxpayer count. The point is that the envelope is currently neither triaged nor visible. The validator enforces the discipline rather than the conclusion: a verdict without a probe is an opinion; a "consistent" verdict carries the heaviest evidence requirement because it is the one that could be wishful thinking; and a "missing" measure must name which claims it affects, because an unlocated gap cannot be caveated. The --probe mode re-runs every reading against the engine, so a caveat that becomes removable is noticed rather than left standing. Suite 273 passed, ruff format clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
|
Review request — @MaxGhenis @DTrim99. This one is part of a batch; the whole queue, with a suggested merge order and what is blocked on whom, is in #104 so you can triage in one place rather than PR by PR. |
DTrim99
left a comment
There was a problem hiding this comment.
Verdict: request changes. The headline claim — that this verifies the certified world contains the law legislated into its future — isn't enforced by anything that runs in CI. What CI actually executes checks the registry against itself.
Critical
pipeline/check_baseline_integrity.py→reprobe()is the only function that queries the live policyengine-uk engine, but it's gated behind--probe, is never called by any test, and CI (pytest tests/ -q, no engine installed) never invokes it. Everything CI runs (validate+ the tests) reads the registry's own recorded probe numbers and asserts them against hardcoded literals — a registry-against-itself tautology. Themissingverdicts (the whole point) are author-asserted, never measured;probe: {2028: null}is not confirmed against the engine. To make the check real, wire a CI job that installs the pinned engine and runs--probe(e.g. on a schedule) — otherwise it can't catch the regression it exists to catch.data/uk/baseline_integrity.json+ the PR body citedata/externals/obr-policy-effects.json(#75) as the evidence for theconsistentfuel-duty verdict, but that file does not exist on main. Theconsistentverdict rests on an absent/unmerged artifact, and the test only asserts the string-0.129ppappears in prose — evidence is text, not a computed check.
Should
baseline_integrity.jsonuses bareInfinityliterals (lines ~20, ~29). Python'sjson.loadaccepts them, but the file is not valid JSON — any strict/JS consumer on theapp/side will fail to parse it.pipeline/is a new top-level dir; all existing pipeline code lives inscorecard_db/.ci.ymllistspipelinein ruff targets, but the module isn't imported bybuild_dbor fed into the no-drift build — effectively dead outside its own test.- The
reprobe()pin guard raisesSystemExitif the installed engine ≠ 2.89.2; combined with never running in CI, the engine path will bit-rot silently once the pin advances.
🤖 review via Claude Code
DTrim99's review found that `announced__property_income_tax_rates` was
recorded missing on a verifiably false claim. Re-checking after that
found a SECOND one. Both came from the same mistake, and the review's
other critical point explains why neither was caught.
The mistake: I guessed parameter paths and read a failure to resolve as
absence.
property income tax rates
gov.hmrc.income_tax.rates.property is a SUBTREE with basic/higher/
additional children and ALREADY CARRIES the April-2027 rise
(0.20->0.22, 0.40->0.42, 0.45->0.47). My probe called it as a scalar,
got TypeError, and collapsed that into "absent".
high value council tax surcharge
I probed gov.hmrc.high_value_council_tax_surcharge, which does not
exist. Searching the tree finds
gov.hmrc.council_tax.high_value_surcharge.amount, carrying the full
announced schedule — 2,500 / 3,500 / 5,000 / 7,500 at 2m / 2.5m /
3.5m / 5m from 2027, uprated from 2029. I then misread the first
bracket's 0 (the below-threshold band) as an empty schedule.
So the headline of this PR was wrong. There are no baseline gaps: every
legislated measure checked is CARRIED. The one absent verdict left is
the pension lump sum, which is not law — a scoring capability gap, not a
baseline failure, and flagged as such.
Why CI could not catch it, which was the review's other critical point:
reprobe() was the only function that asks the engine, and it was gated
behind a --probe flag no test and no workflow ever passed. CI validated
the registry against its own recorded numbers. Fixed:
- .github/workflows/baseline-probe.yml installs the pinned engine and
runs the probe for real, on PRs touching any registry, weekly on a
schedule so the engine moving under a pin is noticed with no diff,
and on demand. It covers the Budget and CGT registries too.
- Every reading now records the PATH and DATE it came from, so a
re-probe re-runs exactly that instead of reconstructing a path from
a key name. Reconstruction was itself producing false readings while
this was being repaired.
- A probe records its KIND — scalar, subtree or absent — because
collapsing subtree into absent is what did the damage.
- An `absent` verdict now requires a recorded NAME SEARCH. A guessed
path that fails to resolve proves nothing.
- corrections_log keeps both false gaps in the file, and validate()
refuses to let it be emptied.
Also from the review: bare Infinity literals are gone (valid JSON now,
so strict parsers on the app side can read it), and the fuel-duty
`consistent` verdict flags that its evidence artifact lives on PR #81
and is not on main.
One correction to the review: pipeline/ is not new — main already has
six modules there, including compute_counterparts.py.
A third path error surfaced during the repair (bracket syntax is x[i],
meaning x.brackets[i]) and this registry's own test caught it. That is
the tooling finally working.
Suite 278 passed, two builds agree on content_hash, ruff format clean,
--probe green against a real 2.89.2 (26 readings).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
|
Rewritten, and the PR body with it — it was advertising a headline I've retracted. Your catch on #100 was the same defect as this registry's two So the finding is now the opposite of what it was: the certified world carries the announced law. One genuine absence remains (pension tax-free lump sum), and it's an upstream development item. The probe changes matter more than the two entries. v1 could not distinguish a path that doesn't exist from a path that exists and isn't a scalar — it collapsed both into "missing," which is exactly how a subtree became an engine gap. Readings are now Both errors stay in On your note that |
The probe workflow runs both registries, but pipeline/validate_budget_2026_registry.py arrives with #100 — so this branch failed on a missing file rather than on anything it owns. Guarded on existence, and the skip is ANNOUNCED via ::notice:: rather than silent. A gate that quietly does nothing reads as a gate that passed, which is exactly what #74/#95 were about. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
The previous commit guarded pipeline/validate_budget_2026_registry.py and left validate_cgt_reform_spec.py (#102) failing the same way — a validator that lands on its own branch, absent on this ref. Folded both into one loop that skips a missing validator, announces every skip via ::notice::, and reports how many actually ran. Each starts enforcing the moment its PR merges, with no edit here. Announcing the skip is the point. A gate that quietly validates nothing reads as a gate that passed — the #74/#95 failure mode. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
|
CI is green now. The failure was mine and unrelated to the findings: this workflow probes the sibling registries too, and those validators ( Each is now skipped when absent and starts enforcing the moment its PR merges. The skip is announced via |
Closes #99. Branches from
main— independent of the other UK PRs.The repo has a baselines registry (#13) that describes worlds, and ingests that stamp which world a run executed. Nothing checked that the certified
policyengine-ukworld actually contains the measures already legislated into the years it computes at.That gap is quiet by construction. A counterpart at 2027 or 2028 measured against a baseline missing an announced measure diverges for a reason no axis in #59's registry names — so it gets attributed to whatever axis the analyst reaches for, and the real cause never appears.
Six measures, probed against a real policyengine-uk 2.89.2
carriedcarriedcarriedcarriedgov.hmrc.council_tax.high_value_surcharge.amountconsistentabsentlump/commencement/tax_free_cash/pclsnode in the treeThe honest headline is now the opposite of version 1's: the certified world carries the announced law. That is a weaker story and the correct one. The one real absence is a live PolicyEngine-UK development item, not a scoring blocker for 28 October.
The two-child result remains a bonus finding: it confirms the registered
pre_ab2025world is a genuine counterfactual and not a restatement of the baseline — which #49's case battery and #67's decomposition both lean on.What the two false gaps changed about the probe
The failure was not carelessness about two paths; it was a probe that could not tell three different things apart.
scalar|subtree|absent, never "missing." Version 1 collapsed subtree-and-absent into one bucket.gov.hmrc.income_tax.rates.propertyis a subtree — it did not return a number, and that was read as "not there" when it was the rise itself.absentrequires a NAME SEARCH of the whole tree, not one path that failed. A guessed path proves nothing. Both false gaps would have died at this rule.corrections_logkeeps both errors in the file, with how each was caught. A registry whose job is refusing unearned excuses cannot quietly delete the two it published.The same rule now guards #100's scoreability registry, where the identical failure mode exists in a form
--resolvecannot catch.CI
.github/workflows/baseline-probe.ymlinstalls the pinned engine, runs--probe, and fails on any drift between the committed registry and what the engine actually returns. Weekly schedule, so an engine bump that drops a measure surfaces on its own rather than at the next Budget.