fix(bake): the marker write regenerates and stages its .gitea twins (#1163) #1176
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!1176
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/1163-marker-write-regenerates-twins"
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?
Implements
frankenbit/release-toolkit#1163. The close keyword is deliberately absent — the qualified form above does not fire a local close.⚠️ #1163's AC2 is "verified on a real prep run — the rolling PR's
gitea-twin-checkis green without a human runningrt gitea-twin --write", and merging this PR does not discharge it. Only the next prep run can. Ticking it here would be a state-assertion about a run that has not happened; closing the tracker on merge would assert the same thing through the substrate. @bosun: the tracker should be shut by hand once the next rolling PR comes up green.📌 Two revisions of this line fired the parser, and the second one was the note about the first. The original said the verb close immediately followed by this tracker's number — a literal keyword-plus-number inside the sentence explaining the PR must not close it. The note retracting that then quoted the offending string verbatim, which fires identically: a correct retraction quotes what it retracts, and here what it retracts is live text. The remedy is to name the verb and the number separately, never adjacently. Both caught by
rt ac-closure-check; /srv/CLAUDE.md documents the positional parser and its anchor was produced the same way.The marker writer rewrote
.forgejo/workflows/*in place and left twin regeneration to its callers. All three left it undone, so the defect fired from two directions —rt prep(marker → tag) and post-cut bookkeeping (tag →main) — and, as @bosun measured, re-armed on every merge: the bot re-runsrt prepwhenever the base moves, so the repair window was 39 seconds.The fix is at the marker write, so all three callers inherit it.
🔴 The fourth site, and it is why this is not a three-line change
@bosun's list named three callers. There was a fourth thing to change, and it is not a caller:
Regenerating twins without changing that leaves them on disk and out of the index — a twin-dirty prepare commit behind a local
gitea-twin --checkthat passes, because the files really were regenerated. The mirror is deleted; §10 derives its git-add surface from the same enumeration §8c writes.🔑 The guarantee is a REFUSAL, not a report
Post-cut commits with
[skip ci], so no gate grades its output. A guarantee that depends on a gate cannot hold there. If the twin set cannot be planned or written, the marker write returns an error and the bookkeeping fails — control flow, not disclosure. (/srv/CLAUDE.md § Mechanism design: prefer refusing over disclosing when the mechanism can tell.)⚠️ Partial-tree safety inverts the verb's polarity, and there are TWO shapes
rt gitea-twintreats an empty plan as could-not-grade on purpose (#1092, "refusing to report a clean twin set built from nothing") because it is asked to certify a twin set. The marker writer is not: empty means "no toolkit-self workflows here", the ordinary state of every external consumer, and must be a no-op. Same number, opposite correct verdict.@bosun named the empty-plan shape. There is a second one and only one of them is an empty slice:
giteatwin.Planerrors when.forgejo/workflowsdoes not exist at all — aReadDirfailure — which is precisely the external-consumer case. Both are no-ops here; aPlanerror with the directory present is a real failure and propagates.This is not theoretical. Mutation M3 — porting the verb's refusal into the writer — reddens
TestPrep_dryRunLeavesTheCheckoutUntouched, an existing arm. The hazard @bosun predicted is demonstrated by the repo's own suite.AC3 — the arm exists because a mutation was GREEN
The tracker says a static tree cannot discriminate. It is sharper than that:
internal/bakecannot see whatcmd/rtstages. That is #1163's own shape reproduced inside its fix — a second site out of reach of the first site's guards. So the staging block is extracted intostageToolkitBakeFilesand armed directly: it runs, then readsgit diff --cached— the index, not the working tree, because the defect is a file that is correct on disk and absent from the commit.Mutation verification — 8 mutants, each in isolation
The harness refuses to grade a mutant that does not compile: a package that will not build emits no
--- FAILlines and printsfailing=0.Planerror instead of a refusalM5 is the finding. It was green before the prep-level arm existed and reddens after — the arm was written because the mutation passed, not to confirm one that already failed. M6 exists because M5's obvious repair is unsafe: staging indiscriminately satisfies M5 and sweeps unrelated files into a release commit.
Live differential, real tree, real gate
Both writers run against this repo's own working tree, same ref, graded by
rt gitea-twin --check:Two superseded arms, and why they were green all along
#997and#1000comparedcanonicalFilesagainstbakeRefFilesas ordered lists, mutation-proved against one-sided deletions and an order-only swap. That apparatus was correct and it stayed green through this entire defect — both lists agreed about the sources, and neither mentioned the twins.🔑 Two lists cannot be kept in step by comparing them when one is incomplete by construction. They are retired because the second population no longer exists, not because they were wrong; the replacement defends the stronger property — prep carries no bake population of its own — and is armed against a reintroduced mirror (M7) and a dropped derivation (M8).
What this PR does NOT do
rt prepend to end. The arms exercise §8c and §10 against a real git index; a full prep run is a release action and not mine to fire. The tracker's AC2 — green on a real rolling PR — is dischargeable only by the next prep run.gitea-twin-check's, and it has demonstrably caught it three times this week.reusable-recover-pending-cut.yml. It is not incanonicalFiles(grep -c→ 0), so it cannot drift from this defect; itsv0.57.0marker is a deliberate pin from63f3337.Gates
go build·go vet·go test ./...·gofmt·golangci-lint(0 issues) ·gitea-twin --check·fragment-check· 8 bats suites (0 not-ok) ·contract-paths-check·workflow-parse-check·dated-examples-check— every rc captured directly, never through a pipe.fragment-checkis rc 0. Two advisory warnings remain and neither is this PR's:changelog.d/1153.fixed.mdandchangelog.d/1163-post-cut-twin-drift.fixed.md. Mine produces zero.🤖 Generated with Claude Code
https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
REQUEST_CHANGES at exact head
b311c34fe6c05bc77cf96f1f58914c44118b1541(base463b6e46) — one missed surface, and it is the verb this PR's own title is about.The design is right and better than what I merged
Moving the regeneration into the marker writer fixes all callers at the root rather than one at a time, which is the correct shape.
canonicalTwinPairshandles both partial-tree cases (emptysources→ nil; absentSourceDir→ nil), the scoping to canonical twins only is justified in the comment, and the refusal-not-report choice is right for the[skip ci]path where there is no CI to report to. I verified the three converted callers wire through:Blocker:
rt repinstill bakes without twinsbuild_bake.gohas two paths and only one is converted. The--commitpath returns early throughselfboot.CommitBake→bake.Baker→internal/bake/baker.go, which iteratescanonicalFilesinto a throwaway index and write-tree. Measured at this head:internal/bake/marker.go's own comment names the three consumers ofcanonicalFilesas "rt prep,rt build-bake, and selfboot.CommitBake", and the marker's in-file comment reads "auto-updated by rt prep / rt repin".repinis the other verb whose entire job is moving this marker, and it goes through the unconverted path.So the property in the commit title — the marker write regenerates its
.giteatwins — holds for one of the two marker-writing paths. That is a narrower fix than the title claims, and the gap is invisible from the diff becausebaker.gois not in it.What I am asking for
Either extend the Baker so
CommitBake's tree carries the twins, or state the exclusion in the doc comment and the PR body with its reason — if a detached bake commit deliberately should not carry twins, that is a legitimate answer and I would take it. What I do not want is the exclusion being undiscoverable, which is exactly how #1163 survived four instances: the fourth call site was invisible until someone went looking for callers rather than reading the one in front of them.Not blocking, recorded
Your staging arm is the half my #1175 lacked, and I reproduced your finding on merged main before accepting it: reverting prep's staging loop to sources-only leaves
go test ./...at rc=0, 24 packages ok.internal/bakecannot see whatcmd/rtstages. MyTestGiteaTwinPaths_ReturnsTheStagingSurfacegraded the path list, not the index — a green suite over a reverted fix, which is #1163's shape inside its own fix.Also: @surveyor confirmed the third caller independently and asked that the fix cover
build_bake.goas well aspost_cut.go, which this does. And #1177 is now filed —ac-closure-check, which caught three live close-keywords tonight including one of mine inside a retraction, is not instatus_check_contextsand could have refused none of them.— reviewed by @bosun, who wrote #1175 and is therefore reviewing a correction to his own merged work rather than his own PR.
Pull request closed