fix(decide): a declined cut-safeguard blocks instead of resolving to noop #888
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!888
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/711-declined-safeguard-blocks-not-noops"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A cut-safeguard that declines can no longer resolve to
mode=noop. Decision recorded at #711 (99974) and authorised by @bosun.The defect
The three fields are emitted only in the
mode=updatebranch, underif d.safeguardFail != "".noop()has no such branch, so a decline that landed there left no trace and exited 0 — a declined safeguard rendered identically to a repo with nothing to release.🔴 Reachability — this is the ordinary post-prepare state, not a corner
The relevance predicate is
bumpLevel == BumpNoneovermaxBump(fragments, conventional-commits). A prepare commit ischore(release): …andchorecontributes nothing to a CC bump. So the state in which Layer 1 matches — a prepare commit at/near HEAD — is the same state that producesBumpNone.The tracker's own origin says so from the other side:
prepareOnRollingBranch'sfeat:commit exists to keep those fixtures off this path, and its comment says so in as many words.Why
blocked, and why not the alternativesnoop/srv/CLAUDE.md§Mechanism design: "a scope note placed inside a success message is decoration" — and noop is the one mode an operator does not investigate.mode=updateupdateinvokesrt prep --rolling-mode, so a run with nothing to release would manufacture a spurious prep.blockedalready means this.mode=blocked::error::+exit 1, no tag, release, manifest write or rolling PR.The workflow's
blocked)arm now branches on$BLOCKED_REASON; the#882pending-cut message is unchanged and moves under*).exit 1sits outside the innercase, so both reasons reach it.safeguard_ungradedstays a separate key. Could-not-grade and refused are different operator investigations — the distinctiondescribeDeclinewas written to preserve, whose comment records that the old form "silently mislabels a could-not-grade as an author-check refusal and sends the operator to the wrong investigation." The arm reads the structural flag, neversafeguard_fail's prose (#689).Mutation verification — closed loop
Two arms, varying exactly one axis: whether a safeguard declined.
🔑 The control staying green under mutation is the load-bearing half.
mode != "noop"passes trivially on any run that never reaches the path; the second arm removes the prepare commit so Layer 1 does not match, proving the blocked outcome is caused by the decline rather than by the fixture's emptiness.⚠️ My first mutation run read the control's result from a filtered view showing one FAIL line — an absence read off a filter. Re-run unfiltered; both verdict lines above are from that second run.
🔴 What this PR does NOT do
noopatdecide.go:230is deliberately untouched. It sits beforedetectCut, so no safeguard has run and it is correctly benign. A fix aimed at "noop drops the safeguard" would naturally cover both and must not. Stated as a non-goal because the over-reach is the plausible wrong version of this change.failandungraded. Both are non-benign and both exit 1; whether they warrant different statuses was left open in the decision.blocked's existing pending-cut prose is unchanged, only relocated under*).Verification
The bash half is gone:
scripts/release-decide.shwas retired ine143ef0(#607) with the differential oracle, sointernal/decideis the whole surface and there is no oracle to keep byte-equal.Reviewed
877c26a0(commit_idomitted). One pass, graded as implementation-with-tests per @bosun's depth instruction — not the ai#585 treatment.combined=success, 12/12,mergeable=true, behind=0.+190/-6across 5 files.✅ The two things flagged as worth attention
The control arm is the load-bearing half and it does what it claims.
TestDecide_711NoopSurvivesWhenNothingDeclinedassertsMode == "noop"and stays GREEN under the mutation, while the convict arm goes red. That is what separates "blocked because the safeguard declined" from "blocked because the fixture was empty" —Mode != "noop"passes trivially on any run that never reaches the path, so without the second arm the first proves only that something changed.The flattened
caseis the right call and the comment says why at the point of use — the bats arm reads to the first;;, so a nested arm truncates the extraction beforeexit 1and the check reports "blocked case does not fail". Two things I'd single out: it names that the guard fails CLOSED, so the shape is not weakening anything; and it repairs the code rather than the check that certifies the code. An author widening the arm that grades his own change is how a gate quietly stops grading. rt#889 for the arm itself is the correct split.✅ Deletions verified relocated, not dropped
All six
-lines are the #882 comment block and its::error::echo, present verbatim in the newelse. Message text preserved. Confined to one file; nothing else deletes.✅ The distinction I'd have blocked on if it were missing
SAFEGUARD_UNGRADEDis read as a structural flag, never by substring-matchingSAFEGUARD_FAIL's prose, anddecide.gokeeps it a separate key rather than folding it into the reason. Could-not-grade and refused are different operator investigations, and the ungraded message says "This is NOT a refusal — a layer could not reach a verdict." That is the could-not-grade-is-not-a-no rule applied where it actually costs something.⚠️ A false alarm of mine, recorded so it does not read as a finding: my first pass grepped the workflow hunk with a line limit that cut off the inner
if, and I briefly had "every decline will report as ungraded". It was my truncation, not your code. Third instance today of a partial view producing a plausible wrong answer.⚠️ Not checked by me
The
reusable-release.ymlchange end-to-end against a real cut — CI's bats arm covers the shape, not a live blocked run. Anddecide.go:230's noop, correctly declared a non-goal.Approving.