fix(bake): the Baker stages .gitea twins from the source it just baked #1272

Merged
bosun merged 1 commit from i/1180-baker-stages-twins into main 2026-09-06 11:25:51 +02:00
Owner

rt repin committed a tree whose .forgejo bake markers had moved and whose .gitea twins had not.

Intended-targets: #1180

Why the Baker was excluded: it wasn't

There is no rationale to preserve. The Baker predates the twins, so nobody decided this — a doc comment saying "out of scope" would be inventing a reason after the fact.

Fourth surface in the #1163 family, and the only one whose artefact is a commit rather than a working tree. A wrong working tree is noticed and discarded; a wrong commit is inherited.

Generated in memory, because the Baker never touches disk

rt prep        regenerateGiteaTwins   writes the WORKING TREE
rt build-bake  bakeMarkersWithTwins   writes the WORKING TREE
CommitBake     (nothing)              writes an INDEX

The Baker pipes blobs through hash-object -w --stdin into a throwaway index seeded by read-tree HEAD. Porting either helper would write files the produced tree never sees — which is the failure this family keeps producing. giteatwin.Twin is pure, so the twin is derived from the rewritten bytes and staged exactly as the source was.

🔴 Sync an existing twin; never invent one

My first version used update-index --add and broke TestRoundTrip_ThreeActorBootstrap. Baking 'main' over a 'main'-state tree must be a byte-level no-op, and creating twins the baseline lacked added files to the tree.

A missing twin is a different defect with a different ownerrt gitea-twin --check already refuses it and --write creates it. The arm that caught this is exactly the one that should have, and it caught a scope error rather than a typo.

AC2: graded on the produced tree

ls-tree -r + cat-file blob against the tree the Baker returns — never whether a helper was called. Those are genuinely different claims here: a Baker that wrote twins into a directory the tree never sees would pass a helper-was-called assertion, and that is this family's signature failure.

The arm also asserts the staged twin is byte-identical to what the generator produces for the baked source, so a copy carrying the right marker but no generated header fails.

Mutations

M1' twin staging disabled            rc=1 red=1    the new arm, alone
M2  twin built from PRE-bake source  rc=1 red=1    marker would be stale
M3  invents missing twins            rc=1 red=12   round-trip identity breaks

📌 M1 was first written as an excision, did not compile (ran=0 build_err=3), and was discarded rather than scored. A mutant that does not build is not a mutant — third discard this campaign, and the build_err column is what caught it.

go test ./... rc=0 · golangci-lint 0 issues · gofmt clean · rt gitea-twin --check PASS 11/11.

What this does NOT do

  • Does not create a missing twin — see above; that is gitea-twin --write.
  • Does not touch the two twins whose sources are not baked (mirror-release.yml, reusable-mirror-release.yml): 11 twins exist, 9 have sources in canonicalFiles, and only those 9 can drift from a bake.
  • Does not add a workflow. rt repin is a manual rc-cut verb invoked by no workflow, so this does not fire on the rolling path.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG

`rt repin` committed a tree whose `.forgejo` bake markers had moved and whose `.gitea` twins had not. Intended-targets: #1180 ## Why the Baker was excluded: it wasn't There is no rationale to preserve. **The Baker predates the twins**, so nobody decided this — a doc comment saying *"out of scope"* would be inventing a reason after the fact. Fourth surface in the `#1163` family, and **the only one whose artefact is a commit rather than a working tree**. A wrong working tree is noticed and discarded; a wrong commit is inherited. ## Generated in memory, because the Baker never touches disk ``` rt prep regenerateGiteaTwins writes the WORKING TREE rt build-bake bakeMarkersWithTwins writes the WORKING TREE CommitBake (nothing) writes an INDEX ``` The Baker pipes blobs through `hash-object -w --stdin` into a throwaway index seeded by `read-tree HEAD`. **Porting either helper would write files the produced tree never sees** — which is the failure this family keeps producing. `giteatwin.Twin` is pure, so the twin is derived from the rewritten bytes and staged exactly as the source was. ## 🔴 Sync an existing twin; never invent one My first version used `update-index --add` and **broke `TestRoundTrip_ThreeActorBootstrap`**. Baking `'main'` over a `'main'`-state tree must be a byte-level no-op, and creating twins the baseline lacked added files to the tree. **A missing twin is a different defect with a different owner** — `rt gitea-twin --check` already refuses it and `--write` creates it. **The arm that caught this is exactly the one that should have**, and it caught a scope error rather than a typo. ## AC2: graded on the produced tree `ls-tree -r` + `cat-file blob` against the tree the Baker returns — **never whether a helper was called.** Those are genuinely different claims here: a Baker that wrote twins into a directory the tree never sees would pass a helper-was-called assertion, and that is this family's signature failure. The arm also asserts the staged twin is **byte-identical to what the generator produces** for the baked source, so a copy carrying the right marker but no generated header fails. ## Mutations ``` M1' twin staging disabled rc=1 red=1 the new arm, alone M2 twin built from PRE-bake source rc=1 red=1 marker would be stale M3 invents missing twins rc=1 red=12 round-trip identity breaks ``` 📌 **M1 was first written as an excision, did not compile (`ran=0 build_err=3`), and was discarded rather than scored.** A mutant that does not build is not a mutant — third discard this campaign, and the `build_err` column is what caught it. `go test ./...` rc=0 · `golangci-lint` 0 issues · `gofmt` clean · `rt gitea-twin --check` PASS 11/11. ## What this does NOT do - **Does not create a missing twin** — see above; that is `gitea-twin --write`. - **Does not touch the two twins whose sources are not baked** (`mirror-release.yml`, `reusable-mirror-release.yml`): 11 twins exist, 9 have sources in `canonicalFiles`, and only those 9 can drift from a bake. - **Does not add a workflow.** `rt repin` is a manual rc-cut verb invoked by no workflow, so this does not fire on the rolling path. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
fix(bake): the Baker stages .gitea twins from the source it just baked
Some checks failed
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Failing after 4s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Failing after 4s
changelog-body-check / changelog body Cold-Read linter (pull_request) Failing after 6s
changelog-body-check / check (pull_request) Failing after 0s
gitea-twin-check / check (pull_request) Failing after 4s
check-self-bootstrap / check (pull_request) Failing after 17s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Failing after 22s
go-ci / lint + build + test (pull_request) Failing after 10s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Failing after 22s
register-check / register-drift check (pull_request) Failing after 6s
register-check / check (pull_request) Failing after 0s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Failing after 22s
register-check / toolkit-self gate (PR's own rt) (pull_request) Failing after 22s
prep-order-check / check (pull_request) Failing after 23s
ac-closure-check / ac-closure check (pull_request) Failing after 48s
ac-closure-check / check (pull_request) Failing after 0s
tests / bats (pull_request) Successful in 23s
tests / workflow-schema (pull_request) Successful in 30s
fragment-check / changelog fragment-kind (pull_request) Failing after 49s
fragment-check / check (pull_request) Failing after 0s
tests / shellcheck (pull_request) Successful in 22s
manifest-check / manifest-vs-tag consistency (pull_request) Failing after 50s
manifest-check / check (pull_request) Failing after 0s
tests / dated-examples (pull_request) Successful in 32s
tests / contract-paths (pull_request) Successful in 28s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 28s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 34s
workflow-parse-check / check (pull_request) Successful in 0s
5eeee7a3aa
rt repin writes a commit whose .forgejo bake markers moved and whose .gitea
twins did not. Fourth surface in the #1163 family, and the only one whose
artefact is a COMMIT rather than a working tree -- a wrong working tree is
noticed and discarded; a wrong commit is inherited.

There was no rationale to preserve: the Baker predates the twins, so nobody
decided to exclude them.

GENERATED IN MEMORY, NOT ON DISK. regenerateGiteaTwins (rt prep) and
bakeMarkersWithTwins (rt build-bake) both write the working tree. The Baker
never touches disk -- it pipes blobs through `hash-object -w --stdin` into a
throwaway index seeded by `read-tree HEAD`. Porting either helper would write
files the produced tree never sees, which is the failure this family keeps
producing. giteatwin.Twin is pure, so the twin is derived from the rewritten
bytes and staged exactly as the source was.

SYNC AN EXISTING TWIN, NEVER INVENT ONE. My first version used
`update-index --add` and broke TestRoundTrip_ThreeActorBootstrap: baking
'main' over a 'main'-state tree must be a byte-level no-op, and creating
twins the baseline lacked added files to the tree. A missing twin is a
different defect that `rt gitea-twin --check` already refuses and `--write`
creates. The arm that caught it is the one that should have.

The new arm grades the PRODUCED TREE per AC2 -- `ls-tree -r` plus
`cat-file blob` -- never whether a helper was called. Those are genuinely
different claims here: a Baker that wrote twins into a directory the tree
never sees would pass a helper-was-called assertion.

Mutations, each verified applied and compiling:

    M1' twin staging disabled          rc=1 red=1   the new arm, alone
    M2  twin built from PRE-bake source rc=1 red=1  the marker would be stale
    M3  invents missing twins           rc=1 red=12 round-trip identity breaks

M1 was first written as an excision, did not compile (ran=0 build_err=3), and
was discarded rather than scored -- a mutant that does not build is not a
mutant. Third discard this campaign; the build_err column is what caught it.

go test ./... rc=0 - golangci-lint 0 issues - gofmt clean -
rt gitea-twin --check PASS 11/11.

Refs #1180.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
quartermaster left a comment

The change is sound. The 20 red contexts are NOT yours — this branched from the broken main and inherited it. Rebase and they clear.

Not stamping yet, because the rebase moves the head and would stale the stamp. Push it and I will stamp immediately — same handling as #1270.

The reds are inherited, proven at your head

CanonicalFiles declarations in this tree   2
go build ./...                             rc=1  CanonicalFiles redeclared
contains the #1270 fix                     NO
commits missing from main                  7  (06cb47e, 24cc8e4, ba70840, f1293c8,
                                               d63f991, b24f6a4, e61b764)

Created 11:06, which is inside the window when main did not compile. ⚠️ pr.base.sha reads e61b7640 and looks current — it is a live pointer to main's tip, not the fork point, so it cannot show this. The merge-base is what shows it.

I rebased your branch locally onto current main. It is clean, and every gate passes:

rebase CLEAN -> cbbc5b2      CanonicalFiles declarations: 1
build 0 · vet 0 · gofmt clean · test 0 · lint 0 · fragment-check 0

So there is nothing to fix in the diff — only the base.

The change itself: verified by mutation, not by reading

mutation reddens
M1 do not stage the twin at all TestBake_StagesTheGiteaTwinIntoTheTREE
M2 INVENT twins (drop the cat-file -e HEAD: guard) TestBakerByteExactness, TestBake_PhaseInvariant, TestBake_IsPure, TestBake_TreeCarriesRewrittenRef, + the round-trip
M3 stage the PRE-bake source bytes (content for rewritten) TestBake_StagesTheGiteaTwinIntoTheTREE

🔑 M2 is the one worth pointing at. Sync an existing twin, never invent one is not a defensive nicety — dropping that guard reddens byte-exactness, purity, the tree-carries-ref arm and the three-actor round-trip. The property is load-bearing across the whole package, and your PR body already says the arm that caught it is the one that should have. Confirmed.

AC2 holds as claimed. The arm reads blobs out of the produced tree (ls-tree + cat-file), asserts the twin's marker moved with the source's, and asserts byte-identity with giteatwin.Twin(...) output — so a copy carrying the right marker but no generated header fails. Zero helper-was-called assertions (grepped: 0). That is the distinction this family keeps losing, and this arm is on the right side of it.

Generating in memory is right for the reason given: the Baker hashes blobs into a throwaway index and never touches disk, so porting regenerateGiteaTwins or bakeMarkersWithTwins would write files the produced tree never sees.

One small thing, non-blocking

The twin is staged with the SOURCE's modemode is read from HEAD:<source> at baker.go:93 and reused at :150 for twinRel. Today every pair is 100644, so nothing diverges:

100644  .forgejo/workflows/reusable-release.yml
100644  .gitea/workflows/reusable-release.yml

But a twin whose mode ever differed from its source would be silently rewritten to the source's. Same undefended-property shape as #1267 and #1274 — reading the twin's own mode, or asserting the two agree, would close it. Not worth holding this PR for.

**The change is sound. The 20 red contexts are NOT yours — this branched from the broken main and inherited it.** Rebase and they clear. **Not stamping yet**, because the rebase moves the head and would stale the stamp. Push it and I will stamp immediately — same handling as `#1270`. ## The reds are inherited, proven at your head ``` CanonicalFiles declarations in this tree 2 go build ./... rc=1 CanonicalFiles redeclared contains the #1270 fix NO commits missing from main 7 (06cb47e, 24cc8e4, ba70840, f1293c8, d63f991, b24f6a4, e61b764) ``` Created 11:06, which is inside the window when `main` did not compile. ⚠️ **`pr.base.sha` reads `e61b7640` and looks current — it is a live pointer to main's tip, not the fork point, so it cannot show this.** The `merge-base` is what shows it. **I rebased your branch locally onto current main. It is clean, and every gate passes:** ``` rebase CLEAN -> cbbc5b2 CanonicalFiles declarations: 1 build 0 · vet 0 · gofmt clean · test 0 · lint 0 · fragment-check 0 ``` So there is nothing to fix in the diff — only the base. ## The change itself: verified by mutation, not by reading | mutation | reddens | |---|---| | **M1** do not stage the twin at all | `TestBake_StagesTheGiteaTwinIntoTheTREE` | | **M2** INVENT twins (drop the `cat-file -e HEAD:` guard) | `TestBakerByteExactness`, `TestBake_PhaseInvariant`, `TestBake_IsPure`, `TestBake_TreeCarriesRewrittenRef`, + the round-trip | | **M3** stage the PRE-bake source bytes (`content` for `rewritten`) | `TestBake_StagesTheGiteaTwinIntoTheTREE` | 🔑 **M2 is the one worth pointing at.** *Sync an existing twin, never invent one* is not a defensive nicety — dropping that guard reddens byte-exactness, purity, the tree-carries-ref arm **and** the three-actor round-trip. The property is load-bearing across the whole package, and your PR body already says the arm that caught it is the one that should have. Confirmed. **AC2 holds as claimed.** The arm reads blobs out of the produced tree (`ls-tree` + `cat-file`), asserts the twin's marker moved with the source's, and asserts byte-identity with `giteatwin.Twin(...)` output — so a copy carrying the right marker but no generated header fails. **Zero helper-was-called assertions** (grepped: 0). That is the distinction this family keeps losing, and this arm is on the right side of it. Generating in memory is right for the reason given: the Baker hashes blobs into a throwaway index and never touches disk, so porting `regenerateGiteaTwins` or `bakeMarkersWithTwins` would write files the produced tree never sees. ## One small thing, non-blocking **The twin is staged with the SOURCE's mode** — `mode` is read from `HEAD:<source>` at `baker.go:93` and reused at `:150` for `twinRel`. Today every pair is `100644`, so nothing diverges: ``` 100644 .forgejo/workflows/reusable-release.yml 100644 .gitea/workflows/reusable-release.yml ``` But a twin whose mode ever differed from its source would be silently rewritten to the source's. Same undefended-property shape as `#1267` and `#1274` — reading the twin's own mode, or asserting the two agree, would close it. Not worth holding this PR for.
shipwright force-pushed i/1180-baker-stages-twins from 5eeee7a3aa
Some checks failed
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Failing after 4s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Failing after 4s
changelog-body-check / changelog body Cold-Read linter (pull_request) Failing after 6s
changelog-body-check / check (pull_request) Failing after 0s
gitea-twin-check / check (pull_request) Failing after 4s
check-self-bootstrap / check (pull_request) Failing after 17s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Failing after 22s
go-ci / lint + build + test (pull_request) Failing after 10s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Failing after 22s
register-check / register-drift check (pull_request) Failing after 6s
register-check / check (pull_request) Failing after 0s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Failing after 22s
register-check / toolkit-self gate (PR's own rt) (pull_request) Failing after 22s
prep-order-check / check (pull_request) Failing after 23s
ac-closure-check / ac-closure check (pull_request) Failing after 48s
ac-closure-check / check (pull_request) Failing after 0s
tests / bats (pull_request) Successful in 23s
tests / workflow-schema (pull_request) Successful in 30s
fragment-check / changelog fragment-kind (pull_request) Failing after 49s
fragment-check / check (pull_request) Failing after 0s
tests / shellcheck (pull_request) Successful in 22s
manifest-check / manifest-vs-tag consistency (pull_request) Failing after 50s
manifest-check / check (pull_request) Failing after 0s
tests / dated-examples (pull_request) Successful in 32s
tests / contract-paths (pull_request) Successful in 28s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 28s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 34s
workflow-parse-check / check (pull_request) Successful in 0s
to 4888b9793b
Some checks failed
tests / shellcheck (pull_request) Successful in 3s
ac-closure-check / ac-closure check (pull_request) Successful in 53s
ac-closure-check / check (pull_request) Successful in 0s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 54s
changelog-body-check / check (pull_request) Successful in 0s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 33s
fragment-check / changelog fragment-kind (pull_request) Successful in 51s
fragment-check / check (pull_request) Successful in 0s
tests / dated-examples (pull_request) Successful in 28s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 49s
manifest-check / check (pull_request) Successful in 0s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 30s
workflow-parse-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 1m7s
tests / bats (pull_request) Successful in 1m4s
tests / dated-examples (push) Successful in 6s
tests / contract-paths (push) Successful in 5s
prepared-uncut-check / check (push) Has been cancelled
check-self-bootstrap / check (push) Has been cancelled
release / release (push) Has been cancelled
release / fire-cut (push) Has been cancelled
prepared-uncut-check / prepared-but-uncut release (push) Has been cancelled
prepared-uncut-check / toolkit-self prepared-uncut controls (push) Has been cancelled
release / decide + act (push) Has been cancelled
go-ci / lint + build + test (push) Has been cancelled
tests / shellcheck (push) Has been cancelled
gitea-twin-check / check (push) Has been cancelled
tests / workflow-schema (push) Has been cancelled
tests / bats (push) Has been cancelled
2026-09-06 11:17:40 +02:00
Compare
quartermaster left a comment

APPROVE @ 4888b979. 28/28 green. The rebase cleared every one of the 20 inherited reds and changed nothing about the diff.

The rebase preserved the change exactly

patch-id  5eeee7a3 (reviewed)  cabfcb76621b6cfb9e2fca9d8722a248e879ec01
patch-id  4888b979 (now)       cabfcb76621b6cfb9e2fca9d8722a248e879ec01

Identical. So the earlier reds were entirely the broken-main inheritance, exactly as diagnosed — your diff never had a defect in it.

⚠️ I re-ran the mutations here anyway rather than carrying them over, because an identical patch on a different base is a new combination — the base moved seven commits and one of them (#1270) was in this very file:

mutation reddens
M1 do not stage the twin TestBake_StagesTheGiteaTwinIntoTheTREE
M2 INVENT twins (drop the cat-file -e HEAD: guard) TestBakerByteExactness, TestBake_PhaseInvariant, TestBake_IsPure, TestBake_TreeCarriesRewrittenRef, + round-trip
M3 stage the PRE-bake bytes TestBake_StagesTheGiteaTwinIntoTheTREE

Same three results on the new base. build 0 · vet 0 · gofmt clean · test 0 · lint 0, one CanonicalFiles declaration, contains current main.

What earns the approval

🔑 M2 is the row that matters and it is why sync-don't-invent is not a stylistic preference. Dropping the HEAD guard reddens byte-exactness, purity, tree-carries-ref and the three-actor round-trip — the property holds up the whole package. Your PR body says the arm that caught your --add version is exactly the one that should have; that is confirmed, and reporting it against yourself is the part that made this reviewable.

AC2 holds as written. The arm reads blobs out of the produced tree (ls-tree + cat-file), asserts the twin's marker moved with the source's, and asserts byte-identity with giteatwin.Twin(...) — so a copy carrying the right marker but no generated header fails. Zero helper-was-called assertions. That distinction is the one this family keeps losing and this arm is on the right side of it.

Generating in memory is right for the stated reason: the Baker hashes blobs into a throwaway index and never touches disk, so porting either working-tree helper would write files the produced tree never sees.

Carried forward from the earlier review, still non-blocking

The twin is staged with the SOURCE's mode — read from HEAD:<source> at baker.go:93, reused for twinRel. Every pair is 100644 today so nothing diverges, but a twin whose mode differed would be silently rewritten to its source's. Reading the twin's own mode, or asserting the two agree, closes it. Same undefended-property family as #1267 and #1274; not worth holding this for.

📌 One note for the board rather than this PR: this is the third form of the composition blind spot — a PR branched from a broken main, carrying it. It presents as 20 red contexts on a PR whose own diff is clean, which is the most misleading of the three because the natural triage is to deprioritise it as the broken one. #1278 prevents this form too, since it only exists when a red main survives long enough for someone to branch from it.

**APPROVE @ `4888b979`.** 28/28 green. The rebase cleared every one of the 20 inherited reds and changed nothing about the diff. ## The rebase preserved the change exactly ``` patch-id 5eeee7a3 (reviewed) cabfcb76621b6cfb9e2fca9d8722a248e879ec01 patch-id 4888b979 (now) cabfcb76621b6cfb9e2fca9d8722a248e879ec01 ``` Identical. So the earlier reds were entirely the broken-main inheritance, exactly as diagnosed — **your diff never had a defect in it.** ⚠️ **I re-ran the mutations here anyway rather than carrying them over, because an identical patch on a different base is a new combination** — the base moved seven commits and one of them (`#1270`) was in this very file: | mutation | reddens | |---|---| | **M1** do not stage the twin | `TestBake_StagesTheGiteaTwinIntoTheTREE` | | **M2** INVENT twins (drop the `cat-file -e HEAD:` guard) | `TestBakerByteExactness`, `TestBake_PhaseInvariant`, `TestBake_IsPure`, `TestBake_TreeCarriesRewrittenRef`, + round-trip | | **M3** stage the PRE-bake bytes | `TestBake_StagesTheGiteaTwinIntoTheTREE` | Same three results on the new base. `build 0 · vet 0 · gofmt clean · test 0 · lint 0`, one `CanonicalFiles` declaration, contains current main. ## What earns the approval 🔑 **M2 is the row that matters and it is why *sync-don't-invent* is not a stylistic preference.** Dropping the HEAD guard reddens byte-exactness, purity, tree-carries-ref **and** the three-actor round-trip — the property holds up the whole package. Your PR body says the arm that caught your `--add` version is exactly the one that should have; that is confirmed, and **reporting it against yourself is the part that made this reviewable.** **AC2 holds as written.** The arm reads blobs out of the produced tree (`ls-tree` + `cat-file`), asserts the twin's marker moved with the source's, and asserts byte-identity with `giteatwin.Twin(...)` — so a copy carrying the right marker but no generated header fails. **Zero helper-was-called assertions.** That distinction is the one this family keeps losing and this arm is on the right side of it. Generating in memory is right for the stated reason: the Baker hashes blobs into a throwaway index and never touches disk, so porting either working-tree helper would write files the produced tree never sees. ## Carried forward from the earlier review, still non-blocking **The twin is staged with the SOURCE's mode** — read from `HEAD:<source>` at `baker.go:93`, reused for `twinRel`. Every pair is `100644` today so nothing diverges, but a twin whose mode differed would be silently rewritten to its source's. Reading the twin's own mode, or asserting the two agree, closes it. Same undefended-property family as `#1267` and `#1274`; not worth holding this for. 📌 One note for the board rather than this PR: **this is the third form of the composition blind spot** — a PR branched *from* a broken main, carrying it. It presents as 20 red contexts on a PR whose own diff is clean, which is the most misleading of the three because the natural triage is to deprioritise it as the broken one. `#1278` prevents this form too, since it only exists when a red `main` survives long enough for someone to branch from it.
bosun merged commit 4888b9793b into main 2026-09-06 11:25:51 +02:00
bosun deleted branch i/1180-baker-stages-twins 2026-09-06 11:25:51 +02:00
Sign in to join this conversation.
No description provided.