Skip to content

Qualify UK model and data candidates automatically before promotion - #574

Draft
MaxGhenis wants to merge 27 commits into
mainfrom
w4/uk-candidate-qualification
Draft

MaxGhenis wants to merge 27 commits into
mainfrom
w4/uk-candidate-qualification

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #573

Draft, stacked on #571 (receipt schema v1). The diff includes #571's commits until that merges; review origin/w4/receipt-schema-v1...HEAD.

Part of the UK Autumn Budget 2026 CI plan (workstream W4). The Budget is on 28 October and there is no model freeze, so the latest qualified model/data pair is what runs that day. This PR builds the workflow that qualifies a candidate pair and promotes it.

What it adds

Workflow (.github/workflows/certify-uk-release.yaml, scripts/check_uk_candidate.py)

  • Triggered by a policyengine-uk or UK data release dispatch, by hand, and on a schedule that catches up anything missed.
  • resolve finds the candidate pair and deduplicates: a pair that is already certified, stale or in flight is not run twice.
  • prepare certifies the candidate on a branch: manifest, pins, lockfile and TRO are regenerated and tested together, and a draft PR is opened.
  • gates runs the local gates; request-replay and poll-replay run the scorecard replay; finalize writes the qualification receipt to the ledger.
  • promote is serialized (concurrency promote-uk) and picks the dominating promotable pair, whatever order the candidates finished in. accept records a human acceptance of a needs_review receipt.

Gates (provenance/uk_candidate.py). All eleven are mandatory; a gate that cannot run reports unavailable, which makes the verdict incomplete, never qualified.

Gate What it checks
installed_versions, core_compatibility The installed model and core are the candidate's, and the model's own requirement admits that core.
wheel_hashes The downloaded wheels match PyPI and the pin, their members match their RECORDs, and the installed files match the wheels.
data_access, dataset_bytes, supported_years The release manifest and dataset are reachable; the simulated file has the pinned bytes and the UK layout; the required years are supported.
budget_inputs 52 inputs the Budget measures depend on are present and usable, compared with the previous pair.
reform_contract W1's contract runner passes on every route. The gate reads the whole report: cases, sampled properties and population checks. It rejects a report that is not a full run of this candidate (other versions, dataset, seed or sample size; a property that did not run; a holds or summary that disagrees with the rows).
metrics_review W3's metrics tool, vendored from a pinned policyengine-uk commit, on the candidate and the previous pair, including W3's standard reform costings. Hard checks fail; moves outside the review bounds need review.
reform_responsiveness Every one of W3's ten standard reforms ran and moved its target variable in the stated direction by at least its yearly minimum, in every year from 2025-26 to 2031-32. The set, targets and minimums are the metrics tool's own (standard_reforms.yaml); the tool checks them as responsive:<id>. This gate requires those checks to be present and passing, and recomputes each verdict from the tool's per-year numbers. A reform that returns £0 or is nearly inert fails the candidate; there is no review path.
replay W6's replay corpus, through an immutable request whose id is the digest of its content. The receipt must name that request, match the status file's digest, and name the candidate's wheels, data and bundle. Structural, coverage, mapping, inertness and construction flags block; numerical and support flags need review.

Certification (certification.py, manifest.py)

  • A new basis, qualified_exact_pair, records the pair id and its ledger URL and certifies exactly one model version, one core version and one dataset digest. It is never broadened to a range.
  • The runtime accepts that basis only for the exact model version and build id.

Ledger (provenance/qualification_ledger.py, .github/qualification-ledger.sh): an orphan branch, uk-qualification-ledger, with one directory per pair and one per attempt.

Consumers (push.yaml, scripts/release_consumers.py): on release, a policyengine-release dispatch carries the UK bundle identity (model, core, data package version, dataset SHA-256, certificate digest) and the qualification verdict, promotable flag and receipt digest. It goes to sim-api and api as before, and now to the dashboard and the scorecard, which also gets a released-bundle replay request.

Invariants (each tested)

  1. The verdict is a pure, monotone function of the gates: rejected > incomplete > needs_review > qualified. Making any gate worse never improves it (Hypothesis).
  2. Promotion is independent of arrival order and never moves to a dominated pair (Hypothesis over permutations).
  3. A request's id is the RFC 8785 digest of its document, and the scorecard's golden request and receipt reproduce byte for byte.
  4. The mirrors and the scorecard's schemas agree (differential): both accept the golden documents and reject the same mutated ones.
  5. An exact-pair certificate needs evidence: validation refuses one without a promotable ledger receipt for that pair.
  6. The contract gate never passes a partial or foreign report. Tests use W1's real reports: the full all-pass run passes, the pinned 2.125.1 run fails on its 101 defects, and each of ten tamperings is an error.
  7. A reform that does nothing never qualifies. The responsiveness gate is monotone in each reform's response: shrinking a value toward zero or flipping its sign never turns a failure into a pass (Hypothesis). Tests cover exactly £0, nearly inert, one inert year, wrong sign, a missing year, a reform that did not run and a report with no reform checks at all, on W3's real reform list. A differential test makes the gate error when the tool's verdict disagrees with its own numbers.
  8. expected_release_bundle is computed as the scorecard computes it: no tokens and no network for the manifest request, so the packaged certification is used. A test runs the manifest fetch in that environment and sees it fail over.

Open items

  • Decision d1344. Automatic promotion and the review bounds wait on it. Until then promotion.automatic is false, the bounds in uk_review_bounds.json are proposals, and promotion is a reviewed PR merge. The responsiveness minimums are W3's proposals in their tool, also pending d1344.
  • Metrics tool pin. The tool is pinned to policyengine-uk#2335's head when I read it (fd944159), an unmerged commit. It moves to the merged commit when that PR lands.
  • A real run is queued. One end-to-end run of the runner and this gate on the certified pair is waiting in the host's heavy-run queue; I'll add its result here.
  • Admin, with the coordinating session: the GitHub App on policyengine-scorecard and uk-autumn-budget-dashboard-2026 with Contents: write; sender tokens that cover policyengine.py; the scorecard's Hugging Face secret.
  • 2031. The Budget horizon includes 2031, but policyengine-uk projects to 2030. The supported_years gate reports that as needs_review.
  • Dependencies. The reform_contract gate reports unavailable until W1's runner (Add the UK Budget reform contract runner #577, after Route UK dated reforms through policyengine-uk's statutory pipeline #575) is importable from the installed wrapper. The replay schemas are pinned to an unmerged scorecard commit (dc040de).

Checks run locally

  • 166 tests pass in the qualification, gate, pipeline and certification suites.
  • ruff is clean; actionlint is clean on the workflow.
  • Independent review (GPT-6.1 Sol) is running at def8b8d. The responsiveness gate (28c90ea, 4da37e4) landed after it started and will get its own review pass.

axiom: n/a: release/qualification infrastructure, no policy change.

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits October 11, 2026 11:25
A receipt names exactly what produced a published result. It records the
installed wrapper, model and core versions; the data release and the
SHA-256 of the simulated dataset bytes; the certificate digest, attached
only when it covers that model and those bytes; reform and baseline digests
bound to their representation; and the fiscal-year convention. Digests use
RFC 8785 canonical JSON, so the dashboard and scorecard can recompute them.
The JSON Schema ships in the wheel.

Fixes #570

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ificate

Independent review of #571 found ways a receipt could claim more than
what ran. Reform digests no longer collide for Scenario.from_reform
closures, lambdas, partials or bound methods: those are refused, and a
module-level modifier is bound by source, bytecode and revision. Digests
are over strict JSON, so numpy scalars, sets, Decimals and colliding keys
are converted exactly or refused instead of stringified. canonical_json
writes numpy floats and int subclasses as their values. Certification now
requires the installed core version, records whether the certificate was
packaged or caller-supplied, and verify_certificate checks it against a
published manifest. Wheel hashes are recorded only when the builder
verified the installed files against the wheel RECORD. The execution
identity no longer depends on how the builder was called, reform and
baseline are required, models parse strictly, overlay datasets get
uncertified receipts, and v1 covers UK results.

Refs #570

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 7 commits October 11, 2026 16:55
- verify_certificate rebuilds the certificate from the bundle record with
  the builder's own derivation and compares every identity member plus the
  whole data identity; a forged receipt that keeps the genuine record digest
  is now reported. The "or require certificate_source packaged" rule is gone.
- Modifier identity no longer digests marshalled bytecode or the Python
  version, so the digest is the same in any checkout. The source is
  recompiled and compared with the loaded code (ignoring file and line
  numbers), defaults and the module source are bound, and code_revision
  needs a full commit sha.
- Wheel verification hashes every archive member against the wheel's own
  RECORD (one to one) and exempts only the launchers an installer generates
  for the wheel's declared console/GUI scripts; stray .pyc and INSTALLER
  files outside their installer locations fail.
- The public metadata_module hook is removed; versions always come from
  importlib.metadata.
- scenario_reform_identity refuses anything that is not a Scenario.
- strict_json_value refuses numpy longdouble and deep nesting with
  ReceiptError; content_sha256 raises ReceiptError for unsafe integers.
- The schema fixes package names and the UK fiscal-year convention as
  consts, and drops the never-populated release_manifest_sha256.

Refs #570

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
QualificationReceipt records a candidate pair, the policyengine.py commit
that qualified it, every mandatory gate and a verdict that is a pure
function of the gates. select_promotion_target picks the newest promotable
pair that strictly advances the certified pair, independent of arrival
order. The replay request, status and receipt mirror the scorecard's v1
JSON Schemas (vendored unchanged) and reproduce its golden digests.

Refs #573

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ispatch

Adds the certify-uk-release workflow and scripts/check_uk_candidate.py:
resolve and deduplicate a candidate pair, build a candidate commit with the
manifest, pins, lock, TRO and test constants regenerated together, run the
mandatory gates on it (no skips), send the scorecard an immutable replay
request, record the qualification receipt in the ledger branch, and promote
the newest promotable pair in order. Certification gains an exact
qualified_exact_pair basis tied to a ledger receipt and a core check; the
runtime honours it only for exactly its model and data build. The release
dispatch carries the certified bundle identity and reaches the dashboard and
the scorecard without risking the existing consumers.

Refs #573

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The receipt builder now verifies installed files against wheel RECORDs
itself, so the qualification gates use the same implementation and hand
the receipt builder wheel paths rather than asserted hashes.

Refs #573

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #573

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scorecard relaxed two rules: requested_by.workflow_run_id is optional,
and a failed receipt may carry no variant or execution when it failed
before the candidate was identified. The mirrors follow, and such a
receipt fails the replay gate.

Refs #573

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reform contract gate now fails on any failing or erroring population
check as well as cases and properties, and treats a report as an error
unless it is a full run of this candidate: sampled properties ran, the
population checks ran when a dataset was given, the reported versions,
dataset digest, seed and sample size are the ones requested, and the
runner's own holds and summary agree with its rows. Tests use W1's real
reports (pinned policyengine-uk 2.125.1 fails on its 101 defects). The
wheel gate's summary reports the archive-member check too.

Refs #573

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis force-pushed the w4/uk-candidate-qualification branch from 414cc55 to ca3840d Compare October 11, 2026 21:06
MaxGhenis and others added 11 commits October 11, 2026 17:31
Replaces the synthetic all-pass report with W1's full run (every route,
20 examples, the certified dataset): 267 pass, 6 pending d1344, 67
unsupported with reasons, both properties and 13/13 population checks.

Refs #573

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Re-vendor the replay schemas and golden fixtures at scorecard dc040de:
  the golden receipt gains results.manifest_sha256.
- ReplayReceipt mirror: a rejected receipt has no variant, identity or
  execution and integrity not_run; a complete receipt has its variant
  identity and a sealed results manifest; execution.python is anchored.
  The request mirror refuses '..' path segments. Differential tests check
  the mirror and the schemas reject the same documents.
- expected_release_bundle runs in the scorecard's certification
  environment (no tokens, no proxy exceptions, proxies to 127.0.0.1:9),
  so packaged certification is used for a private or public data repo; a
  test executes the manifest fetch under it and sees it fail over.
- The release dispatch carries the receipt's promotable flag.

Refs #573

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plan invariant "Reform responsiveness" (11 Oct 2026). A new mandatory
gate, reform_responsiveness, reads the candidate's metrics report and
fails unless every standard reform ran and, in every horizon year, changed
the government balance in the direction of its expected impact by at
least the tool's reform threshold (GBP 0.1bn), and in the reference year
by at least a share of the expected impact (25%, a proposal pending
d1344). A reform that returns exactly zero or is nearly inert fails; it
never passes as a small number.

The standard set is the metrics tool's own reform list, vendored at the
pinned commit with the tool, plus additional_reforms. The metrics runner
now passes those reforms to the tool, which it did not before, so the
reform costings also enter the review-bounds comparison.

Refs #573

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
W3 moved the standard reform set and the responsiveness rule into the
metrics tool (policyengine-uk#2335, pinned here at fd944159): ten
reforms, each with the model variable it must move, the direction and
a yearly minimum, checked by the tool as responsive:<id>.

The gate now holds the candidate's report to those checks instead of
keeping a second definition: reform_runs:<id> and responsive:<id> must
be present and passing for every reform in the tool's standard set, the
report must cover the horizon, and the tool's verdict is recomputed from
its reform_targets.<id> values (a disagreement is an error). The runner
no longer passes a reform list, so the tool costs its own set. Both
check families are hard in the metrics review as well.

Refs #573

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…checks

- Pin the metrics tool to policyengine-uk#2335 at c7a6c9639.
- overlap_guard: joins the known-defect checks (it fails on main until
  policyengine-uk#2235), so it fails a candidate only when it worsens.
- A known-defect check has worsened when the previous pair did not fail
  it (passed, skipped or absent), or failed it in fewer scopes, when the
  tool reports failing_scopes on both rows.
- skipped is neither a pass nor a failure; a check the previous pair
  passed that is now skipped needs review.

Refs #573

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Pin the metrics tool at policyengine-uk#2335 bd8bff74c, which adds
  failing_scopes to each check row (the only change since c7a6c9639).
- Add W3's real compute_budget_metrics report on uk-data 1.58.0 as a
  fixture: seven years, the overlap guard and the ten standard reforms.
  On it the responsiveness gate passes, the UC standard allowance and
  State Pension reforms clear their minimums in every year, and the four
  checks that fail on main are all no-worsening checks, so a pair
  compared with itself passes.
- The backtest's two failure modes, applied to the real report, fail the
  gate (and are an error while the tool's own check still says pass).

Refs #573

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Swap the real metrics report for the one produced at the pinned tool
commit (bd8bff74c), whose check rows carry failing_scopes. Its 1,470
metric rows are identical to the earlier run's. On it, sign:vat and the
overlap guard fail in all seven years and the two counted-once failures
have no year. A candidate that fails sign:vat in a year the previous pair
did not has worsened and fails; one that fails it in fewer years passes.

Refs #573

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing receipts

An independent review of #574 was cut off by a transport failure, but its
reproductions showed ten ways a gate passed without evidence. All ten
are fixed here and tested.

Reform contract:
- A full run must cover what the runner's own module declares (ROUTES,
  each case in CASES on its routes, PROPERTIES). A recounted partial
  report, a dropped route, case or property, or a runner that declares
  no coverage is an error.
- A sampled property that passed must have run the examples requested.

Metrics review:
- A report whose status is not ok, or with no metrics or no checks, fails
  (two empty reports used to compare as a pass).
- A check with a status outside pass, warn, fail and skipped fails.
- A hard check the previous pair ran that the candidate lacks fails;
  other vanished checks need review.

Replay:
- Flags count wherever the receipt states them, never from the summary
  counters alone, and a receipt that contradicts itself fails: flagged
  rows of a kind exceeding its count, failures under a complete
  integrity, supported years with no artifacts, nothing replayed.
- The comparison base must be the one the request asked for.
- Inert measure-years with no inertness flag need review.

Refs #573

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A core the publisher never claimed is admitted only by the qualified
  pair, so the certificate's basis is then qualified_exact_pair whatever
  the model's own claim. Before, a claimed model with an unclaimed core
  was certified on the model's basis, the certificate named no pair, and
  no ledger evidence was ever required.
- The evidence check now covers the default dataset's reference (the
  repository, path, revision and bytes the runtime resolves) and the
  certified dataset name, and a record with no model wheel hash is
  refused instead of assumed to match.
- At run time an exact-pair certificate holds only for the core it was
  tested with: the installed core is read and compared.

Refs #573

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A candidate branch names its pair id, so two pairs that share model and
  data versions (another core, another wheel) can never share a branch.
  Before, a qualified pair could ready an incomplete pair's branch.
- Promotion readies a branch only while its pull request is at exactly
  the commit the qualification ran on, and an automatic merge passes
  --match-head-commit, so the replayed and tested bytes are the promoted
  bytes.
- A forced re-qualification stages a new attempt key when one already
  exists for that base, instead of being stopped by the duplicate guard.
- The ledger publish retry replays its commit with cherry-pick on the
  fetched tip, so two runs that both create the ledger no longer fail on
  HEAD~1; a test runs that race against a real bare repository.

Refs #573

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine-uk#2335 moved one commit: the tool digest is now taken when
the module loads, so a result is stamped with the code that produced it.
standard_reforms.yaml is byte-identical and the report format unchanged,
so the fixtures stay the bd8bff74c run; the README says why. Also sorts
one import block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 6 commits October 12, 2026 08:29
Co-Authored-By: GPT-6.1 Sol via Subfleet <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol via Subfleet <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and immutable

Co-Authored-By: GPT-6.1 Sol via Subfleet <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e tested pair

Co-Authored-By: GPT-6.1 Sol via Subfleet <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol via Subfleet <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unner's own declarations

The fixture is the real contract report (673 pass, 6 pending d1344, 79
unsupported by design, 19 population rows); the test reads the coverage
from the merged runner module, so a stricter coverage rule cannot reject a
real full run unnoticed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Qualify each new policyengine-uk or uk-data release automatically before promotion

1 participant