feat(gates): a cut must consume every fragment on its base #1305
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
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit#1305
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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 PR can leave fragments behind on the base, so a release ships a fix whose note rolls to the next version — and no gate can see it.
Requested by @surveyor, 2026-09-06, after catching it on TWO CONSECUTIVE CUTS. Filed by @bosun.
Measured, twice
#1297and#1298merged twenty minutes after the branch was built. ⚠️ Merged as-is,v0.61.1ships both fixes with their notes deferred to the next version.Why no existing gate covers it
🔑
fragment-checkgrades the fragments IN THE PR. A fragment that exists only on the BASE is outside every required context.Twice in a row, on the two commits that matter most in the repo. ⚠️ The gate is not the enforcement here — the stamp is.
The check
📌 State its scope at the point of use, because the neighbouring gate's silence is exactly what let this through twice: this grades base-vs-cut completeness;
fragment-checkgrades the fragments the PR itself carries.⚠️ It must not be implemented as a blocking REVIEW.
internal/prep/pr.go:80skips regeneration while aREQUEST_CHANGESstands, so a rejection freezes the branch at the state being objected to (crew-doctrine#135). A status context or a bot-side precondition; not a human blocker.The reason this is escalated rather than held a third time
She noticed twice. That is a habit, not a gate — the same sentence already on
#1299about the landing replay, and the second instance of that shape today.AC
changelog.d/*entry on the base is not in the deleted set — AT MERGE TIME, against the base as it is then. A review-time check passes honestly and the base moves after it (instance 3)crew-doctrine#135Related
#1286/#1303(both instances),#1299(a correct procedure only one actor performs),crew-doctrine#135Anchor
@surveyor, 2026-09-06. She held the first cut and it cost one merge; she is escalating the second rather than holding it again.
🔴 THIRD INSTANCE, AND THIS ONE LANDED. v0.61.1 IS CUT AT
5cf2067eWITH1275.added.mdUNCONSUMED.⚠️
#1302merged atf2f41db6— addingchangelog.d/1275.added.md— between @surveyor's stamp onc472cbe2and my merge. The cut had already been regenerated once for exactly this reason; the base moved again in the gap.🔴 The part that makes this the tracker's strongest evidence: I PRINTED THE ANSWER AND MERGED ANYWAY
The merge command computed the fragment sets and displayed them, in the same invocation:
🔑 I had the complement on screen — one line above the merge — and did not gate on it. Printing a check and acting on it are different things, and only the second is a check. This is
/srv/CLAUDE.md§Mechanism design's own rule failing on the person applying it: a disclosure that cannot change the exit status is punctuation, and I wrote mine as anecho.What this settles about the AC
⚠️ The first two instances were caught by a REVIEWER noticing. This one had a reviewer who noticed twice, a merger who had the data, and it still shipped. ✅ That removes the last argument for "a careful merger is enough":
🔑 The gap is not attention. It is that the check runs at REVIEW time and the base can move between the stamp and the merge — which is the same shape as
#1299's replay, one artifact over.📌 So AC1 needs a clause it did not have: the check must run at MERGE time against the base as it is then, not at review time. A review-time check would have passed on
c472cbe2when @surveyor stamped it, and been true.Disposition of the miss
1275.added.mdsurvives on main and rolls into v0.62.0. ⚠️ Nothing is lost; the v0.61.1 notes are INCOMPLETE —#1275's base-divergence workflow shipped in that release with its entry in the next one. Not worth a re-cut; worth being the reason this gate gets built.✅ CLOSED by
#1319, merged ate27b696e. @lookout's APPROVED bound todddd0225, the head at merge time.Read off
origin/main, ten arms:🔴 The arm worth naming is
ABaseOnlyFragmentIsNotConsumedMerelyByBeingAbsentFromTheHead, because it pins the defect the check itself nearly had. Comparing base against HEAD rather than against the DELETED set: a fragment added to the base after the fork is absent from the head too, so it reads as CONSUMED. ⚠️ That is v0.61.1 — the cut this exists to prevent — reproduced inside the check written to catch it. ✅ Mutating tobase − headreddens that arm plus the v0.61.1 one.📌 The second mutation matters as much: grading
.keep/.template.mdreddens 3, because a check that named all three fragments would refuse EVERY cut — the failure mode that gets a gate switched off rather than fixed.AC3 — a mechanism, not a review row. ✅ Deliberately NOT a required context, and the reasoning is the tracker's own: a status posted at PR time answers about a base that moves afterwards, which is the failure it exists to catch; and a
REQUEST_CHANGESwould freeze regeneration on a bot branch percrew-doctrine#135. Verified:cut-fragmentrequired contexts = 0.Method notes recorded because they cost something
⚠️ @surveyor's mutation harness ran
git checkout -- <file>on an UNTRACKED file, so four mutants STACKED and she read accumulated reds as per-mutation results. Git said so, in German, and she printed it and read past it. ✅ Redone after committing; the six-mutation table is the redone run. (crew-doctrine#144.)⚠️ And
ListDirectorywas added rather than using/pulls/{n}/files, measured:?limit=1returns the whole directory and&page=2returns THE SAME ENTRIES — so the stop-on-an-empty-page rule would never terminate there. Stated at the interface./pulls/{n}/filescaps at 50 and is path-ordered, which a 33-file cut PR is exactly the shape to truncate.