Repository navigation
Turn on automatic UK promotion under d1344 - #635
Merged
Merged
Conversation
Policy d1344-replay-regressions-v1, decided by W4 under cos d1344 after two independent reviews (the first disagreed, the second agreed with changes, which this applies). The gate fails what a receipt identifies as a regression: mapping flags; lost results (a coverage row with a base value and no candidate value); lost responses (a measure-year the candidate lists as inert, or a measure-year total of exactly 0 against a base whose absolute value reaches the diff's bound, max(1m, 1%)); and incomplete grids. A gap is known only on base-side evidence, which no receipt carries yet, so there are no known gaps. Every other flagged row goes to review: numerical, structural (sign reversals included), construction and support flags, coverage gains and null/null rows, and inertness on a measure-year the candidate does not list. The policy text ships as data/qualification/replay_policy.md, and the gate records its id, the text's SHA-256, the known gaps, the regressions and the review items. Tests pin the decision record's regression table and the bound's edges, check the first real receipt (6.4.1) fails on exactly its four inert UC measure-years and the incomplete grid with 346 review items, and state the invariants as a property: fail exactly when a regression is recorded, no flagged row ever passes, mapping and lost results always fail, and adding a row never improves the verdict. Nine mutants are killed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nto w4/d1344-automatic-promotion
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With automatic promotion, a target qualified against an earlier certificate than main's must not wait for a manual --force. A promotable pair (qualified, or needs-review with an exact acceptance) that advances main in every component, but whose previous certificate differs from main's, now resolves to "qualify" without force, once per main commit: an attempt from the current base makes later runs duplicates. The promotion plan names that pair (PromotionPlan.requalify), a new `requalify` command prints it, and the two-hourly catch-up dispatches the qualification after it has finished and published waiting receipts. Rejected, unaccepted and sideways pairs are never requalified. Five mutants (promotability, the advance check, the baseline check, the per-commit dedupe and the plan field) are each killed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cos d1344 (15 October 2026): automatic promotion is on once #574 lands on its gates; this PR merges after #574. promotion.automatic is now true, with that basis in uk.json. The automatic step used `gh pr merge --auto`, but main is not branch-protected and the repository has allow_auto_merge off, so it would have failed silently (check=False) and nothing would ever be promoted. Promotion now merges the readied candidate at its qualified commit only once every check on its pull request has passed (a skipped job counts as passed; pending, failed, cancelled, none reported, or an unreadable list waits for the next promote run). --match-head-commit still refuses a branch that moved. Four mutants of the check gate are killed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 16, 2026
High: promotion squash-merged the candidate onto whatever main was at merge time, so main code changed since the candidate's base (with the certificate unchanged) produced a tree no gate had qualified. Promotion now pushes the qualified commit to main without force, once its checks pass. The push succeeds only while main is still the commit the candidate was built on, so main becomes exactly the tested tree or nothing changes; the promote job pushes as the App so main's release workflow runs. When main has moved, the pair is requalified on the new main: candidate_action and the new requalification_target treat a promotable target built on an earlier main commit (the published attempt's key) like one compared against an earlier certificate, once per main commit and never while another requalification is in flight. Medium: one passing check could let a merge through. The gate now needs every check run on the qualified commit finished as success, skipped or neutral, and every pull request workflow run for it finished successfully, including the code-changes workflow that runs the tests; a run still registering its jobs, none reported, or an unreadable list waits. Medium: requalification and recovery dispatches named only the model and data versions, so resolve could pick another core and qualify a different pair while the original stayed the stale target. Dispatches now pass core_version and pair_id; resolve pins the core and fails the run, qualifying nothing, if the versions resolve to another pair. Medium: a mapping flag the counters stated with no flagged row became reviewable. It now fails. Lows: every early failure (unbound, rejected, failed, contradictory) records the policy id, its digest and the failure as the regression; the property test checks each verdict against an independent restatement of the decision record and a source-row zero total is reviewed, not failed; the policy text says a move below the bound raises no numerical flag but is reviewed if another flag fires. Mutation: the reviewer's three survivors, four for the check gate, three for the stale-base rule and the counted mapping flag are each killed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 2 of the #635 review: High, a manual merge could still bypass qualification: promotion readied the target even when main had moved, and a squash merge then combined the pair with untested code. A candidate is now readied only while main is still the commit it was built on (so any merge of it is exactly the qualified tree); otherwise it is kept draft (readiness is undone) and requalified on current main. With automatic promotion it stays draft until its checks pass, is readied only for the push, and is returned to draft if the push fails. Medium, a non-forced push accepted main force-reset to an older ancestor: the push is now leased on the candidate's parent (--force-with-lease=refs/heads/main:<parent>), an atomic compare-and- swap. Medium, green pull-request checks described the candidate merged with an older main: preparation now dispatches the code-changes workflow on the candidate branch, testing the commit's own tree, and promotion requires that run. Medium, an older cancelled run blocked forever: the gate reads the newest run of each workflow and event, and the newest check of each name. refresh-pr-checks also reruns failed dispatched runs. Medium, a pair without latest_attempt.json was stranded: its base is the attempt whose own receipt is the published one, and an unknown base counts as stale. Medium, runs resolved on different main commits could both stage: no pair qualifies while another of its attempts is in flight, and the preparation lock rechecks eligibility against the ledger as it is now (still-eligible), with the resolved force recorded in the candidate. Ten mutants of these changes are killed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nto w4/d1344-automatic-promotion
The 1.58.0 report predates the tool's small-cell rule and published Disabled Students' Allowance's total and caseload, which come from fewer than 10 records, unsuppressed; totals containing it gave it back. The 1.58.1 report at tool commit 228a05a3e publishes those figures as suppressed and rebuilds every total without them. The gate and tests that read the report move to the suppression format in the next commit. History is left as is pending a security decision. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a3e pin Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fixes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t evidence Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 3 of #635: - A job timeout cancels its run, and refresh re-ran only failed and timed-out runs, so a cancelled own-tree run blocked promotion with nothing to re-run it (requalification fires only when main moves). Refresh now re-runs the newest run of each workflow and event when it failed or timed out (its failed jobs) or was cancelled (in full), and stops after MAX_RUN_ATTEMPTS, so a genuine failure is not retried until a flaky test passes. - Refresh re-ran every failed run, including ones a newer run replaced, whose re-run checks then came back newest and blocked. It now leaves replaced runs alone, and the promotion gate does not count checks from a replaced run's check suite. Checks outside any workflow run still count. - The still-eligible recheck gets GH_TOKEN: without it, an earlier attempt's failed run read as live until its stage expired. - cmd_promote returns every ready candidate except the target to draft, and a failed return to draft now fails the run (_set_ready reports it; _promote returns False only then). - Tests: the cancelled-no-jobs own-tree run, a replaced run's re-run checks, an app's check, force reaching still-eligible and resolve's payload, the sweep and a failed undo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rts; signed exclusions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…few records Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 4 of #635: - Medium: the first attempt of a qualified_exact_pair candidate's runs fails by design (its receipt is published after they start), so a cap of three left one re-run for flakes, then stranded the candidate while promote exited 0. MAX_RUN_ATTEMPTS is 4, and a candidate whose newest run is still unsuccessful at the cap fails the promote run with an error naming the runs to re-run by hand. - A re-run GitHub refuses (a run older than 30 days) no longer aborts refresh and the steps after it: it warns and refresh goes on. - Candidate pull requests are listed with their numbers and only from this repository's branches; the sweep returns stale ones to draft by number, so a fork's pull request with a candidate's branch name can neither shadow the real one nor make every run fail. - A target already marked ready goes back to draft while it waits on its checks. - Tests: a real listing with ready, draft and fork pull requests; each failed undo alone (sweep, target after main moved, target after a rejected lease); the cap at attempts 3 and 4; a refused re-run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… from population details Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re-run Round 5 review of #635: - The runs listed at a commit include other pull requests' runs there, a fork's included. Promotion and refresh now count only runs from this repository on the candidate branch; checks of any other run are ignored. - Stranded now covers every finished, unsuccessful newest run refresh will not re-run: results it does not re-run (action_required, skipped, stale), runs at the attempt cap, and runs past GitHub's 30-day re-run window. - The candidate listing reads up to 1000 pull requests and refuses at the limit; every readiness change and close goes by number. - On the waiting path a failed return to draft fails the run. Tests: fakes project to the jq fields and fill in each run's branch and repository; new cases for fork and other-branch runs, the wider stranded rule, the listing limit, a refused re-run listed first and two candidates at one commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nto w4/d1344-automatic-promotion
… stranding - The gh pr list fake returns only the --json fields asked for, so a call that stops asking for isCrossRepository fails a test. - A run that passed on its last allowed attempt is not stranded while the candidate still waits on another run. - The qualification runbook says when the promote job fails for a stranded candidate and the three ways to clear it. All 22 mutants of the round-5 change (the round-5 survivors among them) fail the promotion suites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Rs to main Round 6 of #635: - M1: a pull request from the candidate branch to another base runs the same workflows with the same commit, branch and repository as the candidate's own, and GitHub's runs do not record which pull request a run is for (the API's pull_requests field lists open pull requests matching the head, not the trigger). Promotion now lists the branch's pull requests in every state; while one to another base is open, or after one was closed or merged at the commit, the candidate is kept draft and the run fails with an error naming it (close it and requalify: the new candidate commit leaves its runs behind). Refresh re-runs none of that candidate's runs. An unreadable or full listing waits. - M2: the candidate listing asks for pull requests to main and drops any other, so two pull requests from one branch cannot collapse into one entry and send a readiness change to the wrong one. - The workflow's own lookups (prepare's return to draft, opening or editing the candidate pull request, the verdict edit) now find this repository's pull request to main and write by number. - Coverage the reviewer found missing: the gh fakes run the code's jq with the real jq over API-shaped data (repository and check suite as objects), superseded candidates are closed by number, and both edges of the 30-day re-run window are pinned. - The changelog and runbook say what is isolated and what is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nto w4/d1344-automatic-promotion
…change Round 7 of #635: - M1: gh pr list --head matches the branch name in forks too and stops at 30, so newer forks' pull requests with the candidate branch's name could crowd the candidate's own out of the listing, and prepare would push a replacement head without returning it to draft. The workflow's lookups (prepare's return to draft, opening or editing the candidate pull request, the verdict edit) and the shared-branch check now use GitHub's REST listing filtered by head=<owner>:<branch>, which holds only this repository's pull requests, paginated in full. - M2: a base change does not change what a pull request's earlier runs tested. Among pull requests from the branch whose runs can be at the commit, one now to main whose base was ever changed (GraphQL BaseRefChangedEvent; the candidate's own included) counts as sharing the candidate's runs, so promotion refuses it and refresh leaves its runs alone; an unreadable history waits. - Tests: the REST and GraphQL fakes run the code's jq with the real jq over API-shaped responses; forty newer forks' pull requests no longer hide the candidate's in prepare or the shared-branch check; base-change cases for the candidate's own and closed pull requests. All 10 mutants of this change fail the suites. - Changelog and runbook describe the base-change rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 8 of #635: GitHub records a retarget it makes itself, when a pull request's base branch is merged and deleted, as an AutomaticBaseChangeSucceededEvent, which a query for BASE_REF_CHANGED_EVENT alone misses (live: #244). The base-change query now asks for both, so a pull request automatically retargeted to main is counted like one a person retargeted. The GraphQL fake now answers only for the event types a query asks for, and new cases cover an automatic retarget of the candidate's own and of a closed pull request; dropping either event type from the query fails a test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s base Round 9 of #635: a base branch renamed into main records no timeline event, so a pull request now to main proves nothing about the base its runs at the commit tested (the reviewer showed a rename masking a failed run with no event to see). Any other pull request from the candidate branch that is open, or was closed or merged at the commit, now counts whatever its base, and only the candidate's own pull request is checked for recorded base changes. Since prepare opens the candidate's pull request to main in the job that pushes the commit, and qualification needs that job to succeed, a pull request renamed into main after running at the commit always sits beside prepare's there, and one of them counts. New cases: a closed or merged pull request to main at the commit is refused, one at another commit is not; all 7 mutants of the rule fail the suites. Changelog and runbook updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 10 of #635 modelled an admin renaming the default branch in the seconds between prepare's push and its pull request lookup, so that a run against another base passes as the candidate's. Every round since 6 has needed write or admin access here, and each confirmed that main only ever becomes the exact qualified commit, on current main, after its own-tree run passed. main is not protected (checked live: protected=false, no rulesets), so people who can write here can push it directly; closing this would need event-time provenance from pr_code_changes.yaml, which W4 does not own. The _promote docstring and the runbook now say what the gate guards against (mistakes by writers, anything non-writers can do) and what it does not (deliberate disguise by writers). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 11 of #635 (approved; low): the helper docstring still said prepare always opens a separate pull request, so a rename could not slip by; round 10 showed prepare's lookup can adopt a renamed one. It now says what the check cannot see and points to _promote's trust boundary. That boundary, here and in the runbook, now also names pull requests opened from the candidate branch by anyone who can read it (no write access needed), which make promotion refuse rather than accept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 12 of #635 (approved; low, wording): the trust boundary said pull requests opened from the candidate branch make promotion refuse, which holds for every other one but not for one to main that prepare's lookup adopts as the candidate's own; that one is refused only if its base was ever changed. The _promote docstring and the runbook now say so, in the reviewer's wording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
marked this pull request as ready for review
October 16, 2026 15:45
Contributor
Author
|
Merged at reviewed head 918939b with a merge commit. Gates: 🤖 Generated with Claude Code |
MaxGhenis
added a commit
that referenced
this pull request
Oct 16, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #634
Stacked on #574. Known gaps and review bounds wait on other workstreams (scorecard#183; W3's approved aggregate-impact commit), so they land as follow-up PRs. The strict rule holds until then: no known gaps, and the current bounds only send moves to review. The base is
w4/uk-candidate-qualification. It retargets to main when #574 merges.Plan
d1344-replay-regressions-v1(this commit; details below).diff.base_eventsand the candidate's missing/unsupported lists once they merge on scorecard main, and implement the exact known-gap test. The vendored schema is closed, so receipts carrying those fields are refused until then.promotion.automatic: true, with the d1344 basis in uk.json (0b74ddb). This also fixes a dormant bug. The automatic step usedgh pr merge --auto, but main is not branch-protected and the repository hasallow_auto_mergeoff, so the call would have failed silently (check=False) and nothing would ever promote. Promotion now merges at the qualified commit only once every check on the candidate PR has passed. Skipped jobs count as passed; pending, failed or cancelled checks, none reported, or an unreadable list wait for the next run.--match-head-commitstill refuses a moved branch.qualifywithout force, once per main commit. The promotion plan names it (PromotionPlan.requalify), a newrequalifycommand prints it, and catch-up dispatches it after finishing waiting receipts. Rejected, unaccepted and sideways pairs never requalify. Five mutants are killed.The d1344 basis
cos d1344, decided 15 October 2026: "Yes: delegate the methodology calls the CI plan raises to the workstream sessions, each with independent review agreement and receipt-recorded exceptions". The same decision turns automatic promotion on once #574 lands on its gates.
The replay call was made by W4.
The policy text ships as
src/policyengine/data/qualification/replay_policy.md. Each receipt records its id and the text's SHA-256.Fail:
Known gaps need positive base-side evidence. None exists yet, so there are none, and the rule is strict.
Review: every other flagged row. That includes sign reversals, construction and support flags, coverage gains and null/null rows, and base-only inertness.
Invariants (property-tested)
For every receipt:
test_the_replay_policy_is_monotone_and_never_passes_a_flagchecks these with Hypothesis. The decision record's regression table, the bound's edges (exactly £1m fails, £999,999 is reviewed, a negative base fails, a non-measure-year £0 is reviewed) and the first real receipt are example tests.Differential check on real data. The 6.4.1 receipt (
e3cc6521…, now a fixture) fails on exactly its four inert UC measure-years: £264.1m, £338.6m, £436.3m and £626.6m, each going to £0. Its incomplete spring_statement_2025 grid fails it too, and its other 345 flagged moves and 1 coverage gain go to review. That matches the decision record's stated effect.Nine mutants are killed: the signed bound, a strict bound, any level, the inert list ignored, lost results as review, lost rows also reviewed, no counted flags, integrity-incomplete ignored, and incomplete events ignored.
Tests
tests/test_qualification_contracts.py,test_qualification_ledger_concurrency.pyandtest_uk_qualification_pipeline.py: 192 passed.axiom: n/a: qualification CI infrastructure, no policy encoded.
🤖 Generated with Claude Code