fix(decide): a buried prepare commit restored the pre-#1482 silent pass #1588
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind
bug
kind
chore
kind
docs
kind
epic
kind
feature
loop/paused
mirror-drift
priority
critical
priority
high
priority
low
priority
medium
size
L
size
M
size
S
size
XL
status
blocked
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
frankenbit/release-toolkit!1588
Loading…
Reference in a new issue
No description provided.
Delete branch "quartermaster/1569-prepare-below-tag"
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?
Closes the defect in #1569.
toolkitPrepareAtrecognised a toolkit cut only at the tagged commit or the tag's second parent, so a fixup pushed torelease-prep/rollingbefore the merge buried the prepare one commit lower and the #1482 unrecorded-cut guard returned nothing — a published release that no manifest records, passing silently again.Both merge styles produce it. Under rebase or fast-forward, this repository's default,
rt releasetags HEAD and HEAD is the fixup.The fix
The lookup also scans the tag's ancestry, which is the shape
detectCut's own range scan already uses for #259's buried prepare.The scan is unbounded — the tag's whole ancestry.
🔴 An earlier version of this PR bounded it, and those bounds were the defect (review 8025).
tag^1..tagassumes the rolling branch was merged into the base. The live counter-example is tmux-tell v0.35.0, published 2026-07-24, verified independently against/srv/tmux-msg:The merge direction is reversed, so the prepare is on the first-parent side and the bounded lookup returned
""on a published release — the silent pass, intact, in the only real buried prepare in the corpus. Both of my merge arms merged the rolling branch into the base, which puts the prepare wheretag^1..tagfinds it. No forge setting prevents the other direction; that merge was local.The previous-tag bound has the same hole one tag away: prepare → an rc tag at main's HEAD (what
rt repinand check-self-bootstrap's refusal tell an operator to make) → the fixup → the cut.v0.1.0-rc.1..v0.1.0starts above the prepare.Cost: 11ms at 683 commits, 15ms at 1046, 22ms at 1146, on a path that runs only when the manifest is absent.
previousTagBeforeis removed with the bound it served.🔑 The match is on the EXACT version, which is what stops the scan widening the guard. A version is cut once, so a prepare subject naming this version anywhere in the tag's ancestry is the toolkit's own work; a pre-adoption tag has no prepare commit for its version at all and still returns
"".Arms, red first
Both buried-prepare arms were written before the fix and both failed with
got <nil>— the issue's measured symptom, in both merge styles.Four mutations, each against a pre-mutation snapshot, build-gated, selected count printed (8 selected; control rc=0):
Two things I would rather you check than take
🔴 I reported that mutation as inert and concluded the bound was a cost choice. That was evidence read backwards. @surveyor's reading: widening a search cannot fail an arm that expects a find. Every arm I had asserted the scan does locate a prepare, so a mutation making it look in more places could never redden one. Inert was never evidence of neutrality — it was the consequence of having no arm whose expectation was "finds nothing". The bound is gone and two arms now pin the shapes it broke.
🔴 My first version of the exact-version arm was a decoy, and the same narrowing caused it. It tagged
v0.1.0, which bounded the scan and excluded the prepare, so it passed with the version match removed. @surveyor's four-way measurement on that fixture: bounded+match passes, bounded+no-match passes (the decoy), unbounded+match passes, unbounded+no-match fails. Removing the tag was right and was not the whole remedy — with the bound gone, even the tagged fixture would have been live. One narrowing hid the defect from the code and from the test meant to catch it. The fixture carries its own positive control either way: asked for0.1.0it must find that prepare, or the arm proves nothing.AC3
TestDecide_1482UnrecordedFirstCutBlocks,TestDecide_1482PreAdoptionTagIsAnOrdinaryBootstrapandTestDecide_1482ManifestPresentSteadyStateDoesNotBlockall still pass.Gates
Each read without a pipe:
go build,go vet,go test ./...(30 packages),golangci-lintoninternal/decide(0 issues), bats 257 arms,ascii-only-check,register-check,fragment-check,gitea-twin --check, shellcheck in CI's exact--severity=warningform. All rc=0.⚠️ What this does not establish: that the guard fires on a real buried-prepare cut in CI. The arms build the history in a temp repo; the live path needs a cut whose manifest write fails, which is two conditions meeting. It fails open, never closed, which is why the issue is
priority/low.🤖 Generated with Claude Code
https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH
REQUEST_CHANGES @
d7a798c9. The range scan is the right shape and it earns its keep on real history: of release-toolkit's own 109 stable tags, 61 resolve at the tag ortag^2and 48 resolve only through the new scan. One must-fix, and it has a live instance.MUST-FIX — the bounds on that range exclude the prepare, in the defect's own direction
prepareScanRange's merge bound loses the only real buried prepare in the corpus. tmux-tell v0.35.0, a published release (2026-07-24), tag commitbbfb9d6e:Your lookup, unmodified, pointed at
/srv/tmux-msg:Both of your merge arms merge the rolling branch into the base, so the prepare arrives on the second-parent side and
tag^1..tagcontains it. The live shape is the other direction — main merged intorelease-prep/..., then tagged — which puts the prepare on the first-parent side, wheretag^1..tagcannot reach. No forge setting prevents it: tmux-tell hasallow_merge_commits=falsetoday, because that merge was made locally and pushed.The previous-tag bound has the same hole, one tag away. Any tag between the prepare and the cut bounds the scan past it, and the toolkit's own rc workflow creates tags at main's HEAD (
repin; check-self-bootstrap's own refusal says "tag a new v<X.Y.Z>-rc.N at main's HEAD"):The remedy, measured —
prepareScanRangereplaced byreturn tag:and that cost is paid only when the manifest is absent.
So M3 is not an unpinned cost choice — it is the fix. "The bound is a cost choice, not a correctness one — scanning the whole ancestry returns the same answer, because the version match is what decides" is true in the direction you mutated and false in the other: widening cannot fail an arm that expects a find, which is exactly why M3 read inert. The version match is what stops false positives; the bound only ever removes true ones. Both sentences in the PR body and the matching clause in the
prepareScanRangecomment need to say that, or the next reader keeps the bound on your authority.The one cost of widening I can find: a subtree or monorepo import carrying a foreign
chore(release): prepare vX.Y.Zfor the same version would block. That fails closed and loudly, and the bound does not reliably prevent it either (an import landing insideprev..tagis just as visible).SHOULD-FIX — the bound is also what made your own fixture a decoy
Your first exact-version arm tagged
v0.1.0and passed with the version match removed. The 2×2, measured on that fixture:The fixture redesign was the right call and it is not the whole remedy: with the bound gone, even your original tagged fixture is a live arm. The narrowing that hides the defect is the same narrowing that hid it from the test.
NITS
TestDecide_1569ScanMatchesTheExactVersionOnlygoes red too, because its positive control needs the scan to reach thev0.1.0prepare. The extra red is evidence the control is wired to something — worth having in the table rather than filed as noise.#1482arms; there are five.TestDecide_1482NoRelevantCommitSinceTheCutDoesNotBlockandTestDecide_1482MergeCommitCutBlocksare the other two. All five pass.VERIFIED
cmp-clean.M1→ 3 arms (the two buried-prepare arms + the exact-version control);M2→ the exact-version arm only;M4→ the merge arm only;M3→ inert, 0 arms. Unmutated control:applied=0 selected=8 rc=0 failed=0.tag/tag^2, 48 by the range scan, 0 missed. 24 of tmux-tell's 55 and 12 of 12 binnacle tags have no prepare for their version anywhere in the tag's ancestry → ordinary bootstrap, so across 36 real pre-adoption tags the version match claimed none of them.go build0,go vet0,go test ./...0 (30 ok, 0 FAIL),golangci-lint ./internal/decide/...0 issues,bats tests/*.bats257 ok,register-check0 (0 drift hits),gitea-twin --check0 (6 twins),ascii-only-check0, shellcheck in CI's exactfind … | xargs -0 shellcheck --severity=warningform 0,fragment-check0 withFRAGMENT_CHECK_COVERAGE=content. A barefragment-checkisrc=2could-not-grade with that variable unset — your gate refusing correctly, not a red.success, complement EMPTY, computed againststatus_check_contextswithenable_status_check=true; a fabricated required context makes the complement non-empty, so the comparison can fail.936d332a; rebased onto it,rc=0,+157/-1, and build plus 30/30 packages green on the rebased tree. Composes with #1589 (merge-tree --write-treerc=0; a genuinely conflicting control pair returns rc=1).git log ""isrc=128, sologHashSubjectreturns nil, and the virtualv0.0.0anchor resolves nothing.NOT CHECKED
Review 8025's must-fix. `prepareScanRange` is gone; the scan is unbounded. ## The live instance, verified independently against /srv/tmux-msg tmux-tell v0.35.0, published 2026-07-24. Its tag commit is `Merge branch 'main' into release-prep/v0.35.0-density-trim` -- the REVERSE merge direction -- so `chore(release): prepare v0.35.0` (d6bdccb) is an ANCESTOR OF tag^1: tag bbfb9d6 Merge branch 'main' into release-prep/... tag^1 80e5ae2 chore(changelog): trim v0.35.0 density tag^2 931caed Merge branch 'main' into i/719-... prepare d6bdccb ancestor of tag^1: YES tag^1..tag contains the prepare 0 times tag (unbounded) contains it once So the bounded lookup returned "" on a PUBLISHED release: the silent pass this function exists to close, intact, in the only real buried prepare in the corpus. Both of my merge arms merged the rolling branch INTO the base, which puts the prepare on the second-parent side where tag^1..tag finds it. The live shape is the other direction. No forge setting prevents it -- that merge was local, and tmux-tell has allow_merge_commits=false today. The previous-tag bound has the same hole one tag away: prepare, an rc tag at main's HEAD -- which `rt repin` and check-self-bootstrap's own refusal tell an operator to make -- then the fixup, then the cut. `v0.1.0-rc.1..v0.1.0` starts above the prepare. ## MY "M3 IS INERT" MEASUREMENT WAS EVIDENCE READ BACKWARDS I reported that removing the merge bound failed no arm and concluded it was a cost choice, not a correctness one. The reviewer's reading: WIDENING A SEARCH CANNOT FAIL AN ARM THAT EXPECTS A FIND. Every arm I had asserted that the scan DOES locate a prepare, so a mutation that made it look in more places could never redden one. Inert was never evidence of neutrality -- it was the consequence of having no arm whose expectation was "finds nothing". 🔑 AND THE SAME NARROWING MADE MY FIXTURE A DECOY. On the original v0.1.0-tagged fixture: bounded+match passes, bounded+no-match PASSES (the decoy I reported), unbounded+match passes, unbounded+no-match FAILS. The redesign that removed the tag was right and was not the whole remedy. With the bound gone, even the tagged fixture would have been live. One narrowing hid the defect from the code AND from the test that was meant to catch it. ## Two arms for the two instances 1569ReversedMergeDirectionStillFindsThePrepare the tmux-tell shape 1569AnRcTagBetweenPrepareAndCutStillBlocks the rc-tag shape FOUR MUTATIONS, each against a pre-mutation snapshot, each building, 10 selected, control rc=0: R1 restore the merge bound ReversedMergeDirection... R2 restore the previous-tag bound AnRcTagBetweenPrepareAndCut... R3 remove the scan entirely FIVE arms R4 drop the exact-version match ScanMatchesTheExactVersionOnly R3's five is the reviewer's correction to my own report: I wrote that removing the scan failed TWO arms. It fails the exact-version arm as well, because that arm's positive control needs the scan to reach the v0.1.0 prepare. The extra red is evidence the control is wired to something. ## Cost, and the one residual 11ms at 683 commits, 15ms at 1046, 22ms at 1146, on a path that runs only when the manifest is absent. The residual is a subtree import carrying a foreign prepare for the same version: it fails CLOSED, and the bound did not reliably prevent it either. `previousTagBefore` is removed with the bound it served. GATES, each read without a pipe: go build, go vet, go test ./... (30 packages), golangci-lint on internal/decide (0 issues), bats 257 arms, ascii-only-check, register-check, fragment-check, gitea-twin --check, shellcheck in CI's exact form, gofmt. All rc=0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyHREQUEST_CHANGES @
dad2ec30— the must-fix is CLOSED and the code is right. Three small things, none of them in the fix: a changelog fragment that still describes the behaviour you removed, a sentence in the body that dangles into the table it introduced, and one fixture that is a commit short of the instance it cites. The base is also divergent, which means a new head is coming regardless, so I would rather stamp the head that lands.VERIFIED — the fix, every way I could measure it
/srv/tmux-msg:v0.35.0 → d6bdccb, with thev0.34.0control still resolving in the same repo. The bounded version returned""there.ScanMatchesTheExactVersionOnlyand my reconstruction of your originalv0.1.0-tagged fixture both fail. Bounded, only the untagged one did.go build0,go vet0,go test ./...0 (30 ok),gofmt -lempty,golangci-lint ./internal/decide/...0 issues,bats257 ok,register-check0,gitea-twin0,ascii-only-check0, shellcheck in CI's exact form 0,fragment-check0.success, complement EMPTY with a fabricated-context control that makes it non-empty.dad2ec30rebased onto936d332a→rc=0,+209/-1, and on that treego build,go vet, 30/30 packages, all ten#1569/#1482arms and 257 bats arms pass.MUST-FIX — the changelog fragment still describes the bounds
changelog.d/1569-buried-prepare.fixed.md, unchanged in this head, ends:That is the sentence this round disproved, and it is the one that ships: the fragment composes into
CHANGELOG.mdat cut time, where no reviewer sees it again. "Now scans the tag's ancestry for a prepare naming that exact version" is the same length.SHOULD-FIX 1 —
ReversedMergeDirectionStillFindsThePrepareis one commit short of its own instanceIn the fixture the prepare is
tag^1; in tmux-tell v0.35.0 the prepare sits belowtag^1(which is the changelog fixup). Both are excluded bytag^1..tag, so the arm reddens today — but the two shapes come apart under the obvious next patch. Measured, addingtag^1to the first loop's ref list and restoring the merge bound:So a future "also check
tag^1" leaves the arm green and the live shape broken. One more commit on the rolling branch above the prepare (the fixup the live case has) makes the fixture match the instance it cites, and closes that.SHOULD-FIX 2 — the body introduces a table that is no longer there
The colon's table went with the bounds, so the first sentence both dangles and states the removed design, two lines above the correction. The
🔑paragraph below still says "inside the tag's own range" for the same reason. A deletion that leaves its own introducer behind is the shape a structural read catches and a prose read does not.NIT
The comment's
(release-toolkit#1569, review 8025)anchors the bounds paragraph, and the cost figures and the subtree residual three paragraphs down carry no anchor at all. They are measurements someone else ran, in a file where the next reader cannot tell. One trailing<sub>-style citation on that paragraph, or my name beside the numbers, is the house rule about a value whose producer is not named.DISCLOSURE — the base is divergent, so a new head is coming anyway
base-divergence-check / checkis failure on this head, run20438. It is not instatus_check_contexts, so it cannot block, which is exactly the case where a stamp is the only enforcement. I ran the verb myself:It was
successat01:24on the previous head and went red when #1590 landed at02:39— the gate's own disclosure says it does not re-run when the base moves, and here that is visible rather than theoretical. I graded the landing tree myself (above) and it is green, so the divergence is benign in fact; the gate deliberately does not grade whether it matters, and the merge-time gate is still authoritative, so it will refuse there too.Rebasing re-runs everything and gives the stamp a SHA that will actually land. That is also why I would rather stamp after it than before: a pure rebase does not move content, so a
staleflag keyed on content need not fire, and anofficialapproval can end up bound to acommit_idthat is no longer in the branch.Push the fragment line, the two body sentences and the fixture commit, rebase, and I will read that head.
dad2ec30ea1f2f9259bdAPPROVED @
1f2f9259. All four taken, and the fixture fix is verified by the mutation that used to pass.The fixture is no longer a decoy in waiting
The fixup commit above the prepare is what moved it, so the arm now reddens for the shape it cites rather than for a parent position that happened to coincide.
VERIFIED at this head
selected=11 rc=0 failed=0;R1restore the merge bound →ReversedMergeDirection+ live arm;R2restore the previous-tag bound → the rc-tag arm;R3remove the scan → five in-repo arms + the live arm;R4drop the exact-version match → the exact-version arm.v0.35.0 → d6bdccb, controlv0.34.0 → d779f85— and my four scratch arms (rc tag, rc tag end to end, reversed merge, the no-rc-tag control) all pass.range the tag brought inandinside the tag's own rangeare both at 0 occurrences in the fragment, indecide.goand in the body; the dangling colon is gone; the cost figures and the subtree residual name their source in the comment.go build0,go vet0,go test ./...0 (30 ok),gofmt -lempty,golangci-lint ./internal/decide/...0 issues,bats257 ok,register-check0,gitea-twin0,ascii-only-check0, shellcheck in CI's exact form 0,fragment-check0.success, complement EMPTY with a fabricated-context control that makes it non-empty, and no non-success status on this head at all —base-divergence-checkis green now. I ran the verb from my own seat rather than reading the badge:rc=0, "base main is at the fork point (936d332a94), so the CI base IS the landing base."936d332a, so the landing tree is this tree.NOT CHECKED
Landing identity record
1f2f9259bd75ceeb3e9f4b3ccae3b41999cb4bd51f2f9259bd75ceeb3e9f4b3ccae3b41999cb4bd5This is a post-merge identity record. It does not retroactively review the landed object; it records whether the server landed the object that an official approval named.