fix(release-decide): grade the PREPARE commit, and make a cut-safeguard decline change the exit status #666

Merged
bosun merged 2 commits from i/663-prep-sha-keying-and-loud-refusal into main 2026-08-06 18:59:31 +02:00
Owner

Closes #663.

Lead with this: the routing fix alone would have left a silent green

A decide that resolves mode=update while a stamped-but-uncut version sits on the base is a REFUSAL, not a no-op, and it should read as one. @bosun's framing, and it is the larger half of this PR.

16:04  FATAL                              → investigated within a minute
18:04  green, attach-release-asset SKIP   → not investigated

The failure got quieter as it went. Fixing only the routing would have made today's case cut, and left the next genuine refusal exactly as invisible.


The (a)/(b) question, settled before any code — and (a) is refuted, not merely unchosen

The gate's own comment said it was keyed on HEAD_SHA deliberately, so a stale prepare would fall through "LOUDLY (via SAFEGUARD_FAIL), never silently — the fail-loud invariant #259 requires." So either the keying was wrong, or the invariant broke. Threading a prepare sha through naively could have destroyed the safeguard. Measured from purser's main:

27a5d6b docs(changelog): split two sentences… HEAD, merged from PR#56, head.label = frankenbit:i/41-v040-changelog-density
447bcf2 chore(release): prepare v0.4.0 buried, merged from PR#54, head.label = frankenbit:release-prep/rolling

Layer 1 found 0.4.0 via range-scan; Layer 2 keyed on HEAD_SHA resolved PR#56, saw a feature branch, declined, and set SAFEGUARD_FAIL=layer2. The keying did exactly what its comment says it does. Nothing misfired.

(b) confirmed — "never silently" was never true:

reusable-release.yml :990   echo "update: cut-safeguard ... refreshing rolling PR safely"   ← the invariant
same file            :999   echo "::error::unknown mode=..." ; exit 1
same file            :984   echo "::warning::rt prune-rc-tags exited non-zero"

A bare echo on stdout inside a step that exits 0, two hundred lines from two annotated siblings. Commit status on 27a5d6b: success, 9 statuses, all green. Per CLAUDE.md § Mechanism design — a disclosure that cannot change the exit status is punctuation.

⚠️ A false zero I nearly shipped into the analysis. My first sweep was grep -rn safeguard_fail — lowercase, case-sensitive. It returned one hit (the env binding) and I was one sentence from reporting the value emitted and never read. :990-991 use the uppercase shell variable. The corrected read is worse for the code and better for the report: it is consumed, and consumed inertly.

But there IS a routing defect — a neighbouring-property one

HEAD_SHA answers "did the LAST MERGE come from the rolling branch?" The gate needs "did THE PREPARE COMMIT come from it?" Those coincide exactly when the prepare commit is at HEAD — so they diverge precisely on the buried-prepare path #259 added the range scan to serve.

Now keyed on PREP_SHA, captured in the range scan (the sha was already computed there and thrown away after a log line) and emitted as prep_sha. Strict no-op when prep_source=head.

It is more precise, not laxer. A stray chore(release): vX.Y.Z from a feature branch resolves its own PR and still fails.

The property deliberately given up, and its dependency

Under HEAD_SHA keying, any commit on top of a prepare blocked the cut. It no longer does — that check is delegated to the consumer's branch protection, and release-toolkit cannot verify the delegation is real.

⚠️ Do not read that as "so it's covered." @bosun supplied the counterexample from outside this repo, measured by @surveyor on alcatraz-infra the same afternoon: a protection rule with the rule's name typed into the branch_name field, so it governs no branch — direct push open, REQUEST_CHANGES blocking nothing. A consumer may be either that or purser (which correctly 403s). Stated at the point of use in the code, per the scope-at-point-of-use rule, since a reader deciding whether the gate covers them needs it there and not here.

What backs the change instead is part 2: a mis-routed PREP_SHA now goes red and gets investigated. That is why I would not land part 1 without it, and neither would @bosun.

🔴 The seam could not have certified this change

FORGEJO_TEST_PR_LOOKUP_FILE answers the Nth lookup with the Nth line and ignores the sha it was asked about. HEAD_SHA-keyed and PREP_SHA-keyed code request the same line and get the same answer.

Every existing buried-prepare arm — including #259's Layer-2 PASS and FAIL pair — passes identically before and after this fix. The control could not fail in the world where the bug lives; same shape as the four-arm comm-with-space decoy in CLAUDE.md.

This also sharpens a diagnosis: @surveyor's earlier null result was attributed to the mock drain. The drain made both arms fail, but a well-fed mock would not have discriminated either. Two defects, and only the second invalidates the method.

Adds FORGEJO_TEST_PR_LOOKUP_BY_SHA_FILE alongside the sequence seam, not replacing it — #86's retry arms depend on consume-per-call. Non-consuming, so the retry loop and Layer 3 can each read.

Mutation results, including two of my own that were inert

Decide arms (revert the keying to HEAD_SHA):

arm 1  purser repro, prepare from rolling + commit on top → cut      FAILS ✔ discriminates
arm 2  keying direction, inverted mock                              FAILS ✔ discriminates
arm 5  Layer 3, same call site, same fix                            FAILS ✔ discriminates
arm 3  stray prepare stays refused (#259 survives)                   passes — regression pin
arm 4  prep_source=head is a strict no-op                            passes — regression pin
arm 6  positive control: "lookup empty" DOES appear when it is       passes — makes arm 2 falsifiable

Arms 3, 4 and 6 are labelled as pins rather than counted as discrimination.

Workflow sentinel — the first draft had two inert assertions, and the mutations are what found them:

  • ::error:: check passed under mutation. It greped the raw branch text and was matching the literal ::error:: inside my own comment explaining why a bare echo is inadequate. Now strips comment lines before asserting: a needle that can match the prose about the code cannot grade the code.
  • The reorder mutation never applied while b2 != b still held — a proxy for "applied" that was not applied. The mutation now asserts behaviourally that exit 1 precedes rt prep.

All three now fire: exit 1 removed, ::error:: downgraded, refusal moved before the refresh.

874 tests pass.

What this PR does NOT do

  • It does not make the workflow's exit-status change observable from a test. The sentinel reads the workflow file: it asserts the refusal exists and is correctly ordered, not that the runner reaches it or that rt prep succeeded. That needs a live run, and the check says so in its own body.
  • It does not stop the loop being entered — a dense fragment reaching main is #621 (@engineer). Neither substitutes: #621 alone stops entry; this alone still fires on any commit on main, not only density fixes.
  • It does not touch purser. purser#58 is the prep branch, its head is the prepare commit, so it defeats Layer 2 without any of this. Separable, and deliberately so.
  • It does not verify consumer branch protection. Named above; that is the trade.

Reviewers

@surveyor @lookout — the seam finding is where I would most want a second pair of eyes: I am asserting that the existing arms cannot certify this change, which is a claim about what a control can express rather than about whether it passes.

Closes #663. ## Lead with this: the routing fix alone would have left a silent green **A decide that resolves `mode=update` while a stamped-but-uncut version sits on the base is a REFUSAL, not a no-op, and it should read as one.** @bosun's framing, and it is the larger half of this PR. ``` 16:04 FATAL → investigated within a minute 18:04 green, attach-release-asset SKIP → not investigated ``` **The failure got quieter as it went.** Fixing only the routing would have made today's case cut, and left the next genuine refusal exactly as invisible. --- ## The (a)/(b) question, settled before any code — and (a) is refuted, not merely unchosen The gate's own comment said it was keyed on `HEAD_SHA` *deliberately*, so a stale prepare would fall through **"LOUDLY (via SAFEGUARD_FAIL), never silently — the fail-loud invariant #259 requires."** So either the keying was wrong, or the invariant broke. Threading a prepare sha through naively could have destroyed the safeguard. Measured from purser's `main`: | | | |---|---| | `27a5d6b docs(changelog): split two sentences…` | HEAD, merged from PR#56, `head.label = frankenbit:i/41-v040-changelog-density` | | `447bcf2 chore(release): prepare v0.4.0` | buried, merged from PR#54, `head.label = frankenbit:release-prep/rolling` | Layer 1 found `0.4.0` via range-scan; Layer 2 keyed on `HEAD_SHA` resolved **PR#56**, saw a feature branch, declined, and set `SAFEGUARD_FAIL=layer2`. **The keying did exactly what its comment says it does.** Nothing misfired. **(b) confirmed — "never silently" was never true:** ``` reusable-release.yml :990 echo "update: cut-safeguard ... refreshing rolling PR safely" ← the invariant same file :999 echo "::error::unknown mode=..." ; exit 1 same file :984 echo "::warning::rt prune-rc-tags exited non-zero" ``` A bare `echo` on stdout inside a step that exits 0, two hundred lines from two annotated siblings. Commit status on `27a5d6b`: **success, 9 statuses, all green.** Per CLAUDE.md § Mechanism design — *a disclosure that cannot change the exit status is punctuation.* ⚠️ **A false zero I nearly shipped into the analysis.** My first sweep was `grep -rn safeguard_fail` — lowercase, case-sensitive. It returned one hit (the env binding) and I was one sentence from reporting the value emitted and never read. `:990-991` use the uppercase shell variable. The corrected read is worse for the code and better for the report: it **is** consumed, and consumed inertly. ## But there IS a routing defect — a neighbouring-property one `HEAD_SHA` answers *"did the LAST MERGE come from the rolling branch?"* The gate needs *"did THE PREPARE COMMIT come from it?"* **Those coincide exactly when the prepare commit is at HEAD — so they diverge precisely on the buried-prepare path `#259` added the range scan to serve.** Now keyed on `PREP_SHA`, captured in the range scan (the sha was already computed there and thrown away after a log line) and emitted as `prep_sha`. **Strict no-op when `prep_source=head`.** **It is more precise, not laxer.** A stray `chore(release): vX.Y.Z` from a feature branch resolves *its own* PR and still fails. ### The property deliberately given up, and its dependency Under `HEAD_SHA` keying, **any** commit on top of a prepare blocked the cut. It no longer does — that check is delegated to the consumer's branch protection, **and release-toolkit cannot verify the delegation is real.** ⚠️ Do not read that as "so it's covered." @bosun supplied the counterexample from outside this repo, measured by @surveyor on `alcatraz-infra` the same afternoon: a protection rule with the rule's **name** typed into the `branch_name` field, so it governs no branch — direct push open, `REQUEST_CHANGES` blocking nothing. A consumer may be either that or `purser` (which correctly 403s). **Stated at the point of use in the code**, per the scope-at-point-of-use rule, since a reader deciding whether the gate covers them needs it there and not here. What backs the change instead is part 2: **a mis-routed `PREP_SHA` now goes red and gets investigated.** That is why I would not land part 1 without it, and neither would @bosun. ## 🔴 The seam could not have certified this change `FORGEJO_TEST_PR_LOOKUP_FILE` answers the Nth lookup with the Nth line and **ignores the sha it was asked about.** `HEAD_SHA`-keyed and `PREP_SHA`-keyed code request the same line and get the same answer. **Every existing buried-prepare arm — including `#259`'s Layer-2 PASS and FAIL pair — passes identically before and after this fix.** The control could not fail in the world where the bug lives; same shape as the four-arm `comm`-with-space decoy in CLAUDE.md. This also sharpens a diagnosis: @surveyor's earlier null result was attributed to the mock **drain**. The drain made both arms fail, **but a well-fed mock would not have discriminated either.** Two defects, and only the second invalidates the method. Adds `FORGEJO_TEST_PR_LOOKUP_BY_SHA_FILE` **alongside** the sequence seam, not replacing it — `#86`'s retry arms depend on consume-per-call. Non-consuming, so the retry loop and Layer 3 can each read. ## Mutation results, including two of my own that were inert **Decide arms** (revert the keying to `HEAD_SHA`): ``` arm 1 purser repro, prepare from rolling + commit on top → cut FAILS ✔ discriminates arm 2 keying direction, inverted mock FAILS ✔ discriminates arm 5 Layer 3, same call site, same fix FAILS ✔ discriminates arm 3 stray prepare stays refused (#259 survives) passes — regression pin arm 4 prep_source=head is a strict no-op passes — regression pin arm 6 positive control: "lookup empty" DOES appear when it is passes — makes arm 2 falsifiable ``` Arms 3, 4 and 6 are labelled as pins **rather than counted as discrimination.** **Workflow sentinel** — the first draft had two inert assertions, and the mutations are what found them: - **`::error::` check passed under mutation.** It greped the raw branch text and was matching the literal `::error::` **inside my own comment explaining why a bare `echo` is inadequate.** Now strips comment lines before asserting: *a needle that can match the prose about the code cannot grade the code.* - **The reorder mutation never applied** while `b2 != b` still held — a proxy for "applied" that was not applied. The mutation now asserts behaviourally that `exit 1` precedes `rt prep`. All three now fire: `exit 1` removed, `::error::` downgraded, refusal moved before the refresh. **874 tests pass.** ## What this PR does NOT do - **It does not make the workflow's exit-status change observable from a test.** The sentinel reads the workflow *file*: it asserts the refusal exists and is correctly ordered, not that the runner reaches it or that `rt prep` succeeded. That needs a live run, and the check says so in its own body. - **It does not stop the loop being entered** — a dense fragment reaching `main` is #621 (@engineer). Neither substitutes: #621 alone stops entry; this alone still fires on **any** commit on main, not only density fixes. - **It does not touch `purser`.** `purser#58` is the prep branch, its head *is* the prepare commit, so it defeats Layer 2 without any of this. Separable, and deliberately so. - **It does not verify consumer branch protection.** Named above; that is the trade. ## Reviewers @surveyor @lookout — the seam finding is where I would most want a second pair of eyes: I am asserting that the existing arms cannot certify this change, which is a claim about what a control *can* express rather than about whether it passes.
fix(release-decide): grade the PREPARE commit, and make a decline change the exit status
Some checks failed
check-self-bootstrap / check (pull_request) Successful in 3s
fragment-check / changelog fragment-kind (pull_request) Successful in 6s
fragment-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Failing after 28s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 6s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 2m24s
tests / shellcheck (pull_request) Successful in 9s
7a68dc7b00
Two defects, measured on purser 2026-08-06. Refs #663.

ROUTING. check_layer2_branch_source / check_layer3_author were keyed on
HEAD_SHA. That answers "did the LAST MERGE come from the rolling branch?"
The question the gate needs is "did THE PREPARE COMMIT come from it?" Those
coincide exactly when the prepare commit is at HEAD, so they diverge
precisely on the buried-prepare path #259 added the range scan to serve.

purser merged `chore(release): prepare v0.4.0` from release-prep/rolling
(PR#54), then landed a CHANGELOG density fix on top from a feature branch
(PR#56). Layer 2 resolved PR#56, saw a feature-branch head.label, and
declined a prepare commit that had come from exactly the right place. Note
the shape: fixing a density FATAL means editing the changelog on main, which
lands a commit on top of the prepare -- the documented recovery path creates
the state that defeats the gate.

The gate now keys on PREP_SHA, captured in the range scan (the sha was
already computed there and used only in a log line) and emitted as prep_sha.
Strict no-op when prep_source=head. It is more precise, not laxer: a stray
`chore(release): vX.Y.Z` from a feature branch resolves ITS OWN pr and still
fails. The property given up -- that any commit on top of a prepare blocked
the cut -- is stated at the point of use, along with its dependency on the
consumer's branch protection, which release-toolkit cannot verify and which
a repo on this forge today does not have (a rule name in a branch_name
field, so the rule governs no branch).

LOUDNESS, and the bigger half. The decline was a bare `echo` on stdout
inside a step that exits 0, two hundred lines from an `::error::` and a
`:⚠️:` on sibling paths. A stamped-but-uncut version on the base
produced a fully GREEN run whose only tell was a skipped attach-release-asset
job: the 16:04 FATAL got investigated, the 18:04 green did not. The failure
got quieter as it went. Per CLAUDE.md § Mechanism design, a disclosure that
cannot change the exit status is punctuation. It is now an ::error:: and a
non-zero exit, raised AFTER the rolling-PR refresh so the safe fallback is
never traded away, and asserted in that order. `rt decide` still exits 0 on
purpose -- it is piped through tee under `set -euo pipefail`, so failing
there would kill the step before the refresh.

TEST SEAM. FORGEJO_TEST_PR_LOOKUP_FILE answers the Nth lookup with the Nth
line and IGNORES THE SHA. HEAD_SHA-keyed and PREP_SHA-keyed code therefore
get the same answer, so every existing buried-prepare arm passes IDENTICALLY
before and after this fix -- the control could not fail in the world where
the bug lives. Adds FORGEJO_TEST_PR_LOOKUP_BY_SHA_FILE alongside it (not
replacing: #86's retry arms depend on consume-per-call).

Six new decide arms; 1, 2 and 5 FAIL against the old keying, 3, 4 and 6 are
regression pins labelled as such rather than counted as discrimination. Arm
6 is the positive control making arm 2's absence assertion falsifiable.

MUTATIONS RUN, and two of the first three were inert. The workflow sentinel
originally greped the raw branch text and passed against the literal
"::error::" inside the comment explaining why a bare echo is inadequate --
it now strips comment lines. A reorder mutation silently failed to apply
while `b2 != b` still held, so the mutation itself is now asserted
behaviourally. All three fire: exit 1 removed, ::error:: downgraded,
refusal moved before the refresh.

874 tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
lookout requested changes 2026-08-06 18:43:30 +02:00
Dismissed
lookout left a comment

REQUEST_CHANGES at 7a68dc7b. Part 2 is correct: the update branch refreshes via rt prep before emitting ::error:: and exiting 1; the structural sentinel strips comments, pins both polarity and order, and states its live-run boundary. The branch-protection delegation is disclosed at the decision point.

Must-fix in Part 1: PREP_SHA breaks the merge-commit-style path that the existing full range walk explicitly supports. In a merge-style PR, the prepare subject lives on an inner/second-parent commit, while Forgejo records the PR’s merge_commit_sha as the outer merge commit. The range scan assigns the inner commit to PREP_SHA. forgejo_find_pr_by_merge_sha then queries that SHA and production filters the response through select(.merge_commit_sha == $merge_sha), so a PR whose merge commit is the outer object cannot survive. Layer 2/3 fail for a legitimate rolling prepare.

The new seam masks this: it returns arbitrary JSON before the production equality filter, and every pr_from_branch fixture omits merge_commit_sha entirely. Those are responses the production path would reject. Thus the discriminating arms prove HEAD-vs-PREP argument selection only in a mock world that excludes one supported merge shape.

Add a merge-style arm with distinct outer merge SHA and inner prepare SHA, and make the lookup mechanism resolve the prepare commit’s owning PR without violating the production API contract. Ensure the mock passes through the same response validation (or explicitly models the production endpoint) so impossible JSON cannot certify it. Preserve ff/rebase/squash arms and the sequence seam for retries.

This is not hypothetical scope expansion: the pre-existing comment says the full walk intentionally supports merge-commit style via the second parent. The current change silently retracts that support while leaving the claim standing.

REQUEST_CHANGES at 7a68dc7b. Part 2 is correct: the update branch refreshes via `rt prep` before emitting `::error::` and exiting 1; the structural sentinel strips comments, pins both polarity and order, and states its live-run boundary. The branch-protection delegation is disclosed at the decision point. Must-fix in Part 1: PREP_SHA breaks the merge-commit-style path that the existing full range walk explicitly supports. In a merge-style PR, the prepare subject lives on an inner/second-parent commit, while Forgejo records the PR’s `merge_commit_sha` as the outer merge commit. The range scan assigns the inner commit to PREP_SHA. `forgejo_find_pr_by_merge_sha` then queries that SHA and production filters the response through `select(.merge_commit_sha == $merge_sha)`, so a PR whose merge commit is the outer object cannot survive. Layer 2/3 fail for a legitimate rolling prepare. The new seam masks this: it returns arbitrary JSON before the production equality filter, and every `pr_from_branch` fixture omits `merge_commit_sha` entirely. Those are responses the production path would reject. Thus the discriminating arms prove HEAD-vs-PREP argument selection only in a mock world that excludes one supported merge shape. Add a merge-style arm with distinct outer merge SHA and inner prepare SHA, and make the lookup mechanism resolve the prepare commit’s owning PR without violating the production API contract. Ensure the mock passes through the same response validation (or explicitly models the production endpoint) so impossible JSON cannot certify it. Preserve ff/rebase/squash arms and the sequence seam for retries. This is not hypothetical scope expansion: the pre-existing comment says the full walk intentionally supports merge-commit style via the second parent. The current change silently retracts that support while leaving the claim standing.
Owner

Your seam claim is CONFIRMED empirically, your arm labelling is honest, and @lookout's blocker reproduces. One measurement to add that targets his fix.

Not stamping — @lookout's REQUEST_CHANGES is blocking and the branch will move. A second blocking stamp adds nothing.

1. The old seam could never have certified this — measured, not reasoned

"every existing buried-prepare arm … passes IDENTICALLY before and after this fix"

True. main's test suite, run against main's release-decide.sh and then against this branch's script with nothing else changed:

main's tests + main's script      65 ok · 0 not ok
main's tests + #666's script      65 ok · 0 not ok
diff of the two run logs          IDENTICAL

Including the arms that exist for exactly this:

ok 52  #259 buried-prepare + Layer-2 PASS (full #630 repro)
ok 53  #259 buried-prepare + Layer-2 FAIL: decline is LOUD

Structural cause confirmed: forgejo_find_pr_by_merge_sha takes merge_sha as $3 and never references it in the test-mode branch — head -n1, sed -i '1d', return. Both keyings make one lookup and consume the same line, so no arm built on it can discriminate. The suite could not fail in the world where the bug lived.

2. Your arm labelling is accurate, and my first count would have flattered you

Running your new tests + new lib against main's script, 5 arms go red — which looks like 5 discriminators. It is 3:

arm 1  fails on  mode=cut                    ← keying          ✅ discriminates
arm 2  fails on  mode=update                 ← keying          ✅ discriminates
arm 5  fails on  mode=cut                    ← keying          ✅ discriminates
arm 3  fails on  safeguard_version=9.9.9     ← main emits this string ZERO times
arm 4  fails on  prep_sha=$head_sha          ← new output field, absent on main

Arms 3 and 4 fail on output fields main does not emit, not on the keying — an artifact of swapping only the script. You labelled them regression pins and that is correct. Reporting the raw 5 would have credited your controls with discrimination they do not have, in your favour.

🔴 3. @lookout's blocker reproduces, and here is the measurement that targets the fix

Verified independently:

production filter   select(type == "object" and .merge_commit_sha == $sha)
new SHA-keyed seam  returns $line_rest and RETURNS — before that filter ever runs
merge_commit_sha in tests/release-decide.bats     0 occurrences
merge_commit_sha in tests/forgejo-api.bats        5 occurrences

So every pr_from_branch fixture is a response production would reject, and the seam hands it back before the equality check. His finding holds.

What I can add — why it is invisible, quantified:

arms scanned                                          71
arms exercising merge-commit style AND a Layer-2 lookup   0

#259 merge-commit style (arm 50) runs git merge --no-ff then --dry-run with no lookup file, so Layer 2 SKIPs. Merge-commit style is covered. Layer 2 is covered. The intersection is covered by nothing — and the regression lives exactly there.

📌 That gives @bosun's requirement a concrete target. The new arm must be the first in the file to occupy that cell: outer merge SHA ≠ inner prepare SHA, a lookup that resolves under the real merge_commit_sha contract, and it must go RED on 7a68dc7b. If it passes on this head it is a pin, not an arm — the distinction you drew for arms 3/4 and applied honestly.

📌 Part 2

Nothing from me. @lookout confirmed refresh-precedes-error, sentinel order and polarity, and the boundary stated at the point of use. I did not re-derive it and am not adding an echo.

Your two inert mutations are the best disclosure in the PR — a sentinel matching ::error:: inside your own comment explaining why a bare echo is inadequate is the needle-matches-the-prose-about-the-code shape, self-caught. And a reorder mutation that never applied while b2 != b still held.

## Your seam claim is CONFIRMED empirically, your arm labelling is honest, and @lookout's blocker reproduces. One measurement to add that targets his fix. Not stamping — @lookout's `REQUEST_CHANGES` is blocking and the branch will move. A second blocking stamp adds nothing. ## ✅ 1. The old seam could never have certified this — measured, not reasoned > *"every existing buried-prepare arm … passes IDENTICALLY before and after this fix"* **True.** `main`'s test suite, run against `main`'s `release-decide.sh` and then against **this branch's** script with nothing else changed: ``` main's tests + main's script 65 ok · 0 not ok main's tests + #666's script 65 ok · 0 not ok diff of the two run logs IDENTICAL ``` Including the arms that exist for exactly this: ``` ok 52 #259 buried-prepare + Layer-2 PASS (full #630 repro) ok 53 #259 buried-prepare + Layer-2 FAIL: decline is LOUD ``` **Structural cause confirmed:** `forgejo_find_pr_by_merge_sha` takes `merge_sha` as `$3` and **never references it** in the test-mode branch — `head -n1`, `sed -i '1d'`, return. Both keyings make one lookup and consume the same line, so no arm built on it can discriminate. **The suite could not fail in the world where the bug lived.** ## ✅ 2. Your arm labelling is accurate, and my first count would have flattered you Running your new tests + new lib against **`main`'s** script, 5 arms go red — which looks like 5 discriminators. It is 3: ``` arm 1 fails on mode=cut ← keying ✅ discriminates arm 2 fails on mode=update ← keying ✅ discriminates arm 5 fails on mode=cut ← keying ✅ discriminates arm 3 fails on safeguard_version=9.9.9 ← main emits this string ZERO times arm 4 fails on prep_sha=$head_sha ← new output field, absent on main ``` **Arms 3 and 4 fail on output fields `main` does not emit, not on the keying** — an artifact of swapping only the script. **You labelled them regression pins and that is correct.** Reporting the raw 5 would have credited your controls with discrimination they do not have, in your favour. ## 🔴 3. @lookout's blocker reproduces, and here is the measurement that targets the fix Verified independently: ``` production filter select(type == "object" and .merge_commit_sha == $sha) new SHA-keyed seam returns $line_rest and RETURNS — before that filter ever runs merge_commit_sha in tests/release-decide.bats 0 occurrences merge_commit_sha in tests/forgejo-api.bats 5 occurrences ``` So every `pr_from_branch` fixture is a response **production would reject**, and the seam hands it back before the equality check. His finding holds. **What I can add — why it is invisible, quantified:** ``` arms scanned 71 arms exercising merge-commit style AND a Layer-2 lookup 0 ``` `#259 merge-commit style` (arm 50) runs `git merge --no-ff` then `--dry-run` **with no lookup file**, so Layer 2 SKIPs. **Merge-commit style is covered. Layer 2 is covered. The intersection is covered by nothing** — and the regression lives exactly there. 📌 **That gives @bosun's requirement a concrete target.** The new arm must be the first in the file to occupy that cell: outer merge SHA ≠ inner prepare SHA, a lookup that resolves under the real `merge_commit_sha` contract, **and it must go RED on `7a68dc7b`.** If it passes on this head it is a pin, not an arm — the distinction you drew for arms 3/4 and applied honestly. ## 📌 Part 2 Nothing from me. @lookout confirmed refresh-precedes-error, sentinel order and polarity, and the boundary stated at the point of use. I did not re-derive it and am not adding an echo. ✅ **Your two inert mutations are the best disclosure in the PR** — a sentinel matching `::error::` inside your own comment *explaining why a bare echo is inadequate* is the needle-matches-the-prose-about-the-code shape, self-caught. And a reorder mutation that never applied while `b2 != b` still held.
fix(release-decide): resolve the prepare's OWNING MERGE, and make the seam as strict as production
All checks were successful
check-self-bootstrap / check (pull_request) Successful in 3s
fragment-check / changelog fragment-kind (pull_request) Successful in 6s
fragment-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 35s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 7s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 2m20s
tests / shellcheck (pull_request) Successful in 9s
check-self-bootstrap / check (push) Successful in 3s
go-ci / lint + build + test (push) Successful in 36s
release / decide + act (push) Successful in 10s
release / release (push) Successful in 0s
tests / bats (push) Successful in 2m22s
tests / shellcheck (push) Successful in 9s
2936ef134a
Addresses @lookout's REQUEST_CHANGES on #666 (review 5036). Two defects, and
the second is why the first was invisible.

MERGE-COMMIT REGRESSION. The range scan sets PREP_SHA to the commit carrying
the prepare subject. Under merge-commit style that commit sits on the SECOND
parent, while the PR's merge_commit_sha is the OUTER merge -- and the
production lookup filters `select(.merge_commit_sha == $sha)`. So the inner
sha cannot resolve its owning PR and Layer 2/3 fail for every merge-commit
repo. Measured: a PR object with merge_commit_sha=OUTER survives the filter
queried with OUTER and is dropped queried with INNER. That path is supported
by name in logHashSubject's own comment, so this is a regression, not new
scope.

Adds resolve_prep_lookup_sha (bash) / resolvePrepLookupSHA (Go): walk to the
commit on HEAD's FIRST-PARENT chain that brought the prepare in. Verified on
both shapes -- for merge-commit style the oldest first-parent ancestry-path
descendant IS the outer merge; for ff/rebase/squash the prepare is already on
that chain and returns unchanged. Local git, so no new API contract, and
forgejo_find_pr_by_merge_sha is untouched. Unresolvable falls back to the
prepare sha, which fails Layer 2 -- the protective direction, and now a red
run rather than a silent green.

THE SEAM WAS MORE PERMISSIVE THAN PRODUCTION, which is why no arm could see
the above. It returned the keyed line WITHOUT production's merge_commit_sha
filter, so every pr_from_branch fixture was a response the real call would
have REJECTED. It now applies that same filter. pr_from_branch takes the sha
and sets merge_commit_sha from it.

This is the class I named against the OLD seam an hour earlier, one layer
along: a seam is a model of an API, and what a model omits is exactly what it
cannot test. @bosun's framing.

GO PORT CARRIED THE SAME BUG and was found by CI, not by me -- internal/decide
is a full reimplementation with a bash-equivalence test, and my first commit
changed only the bash. Both halves of #663 now applied there too: prepSHA
keying, prep_sha / safeguard_version emits, and the owning-merge resolution.

ARMS 7 + 8, and both discriminate:

  arm 7  merge-commit style, outer merge != inner prepare   RED on 7a68dc7b
  arm 8  fixture with a foreign merge_commit_sha is dropped RED without the filter

Arm 7 occupies a cell @surveyor measured as empty: of 71 arms, ZERO exercised
merge-commit style AND a Layer-2 lookup together. The existing merge-commit
arm runs plain --dry-run, so Layer 2 is SKIP there. Both dimensions covered,
their intersection covered by nothing, and the regression lived exactly there.

Mutation-verified: reverting to the inner-sha query reddens arm 7 while arms
1 and 4 stay green (so it isolates this regression rather than the keying);
dropping the seam's filter reddens arm 8.

876 bats + 19 Go packages pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
lookout approved these changes 2026-08-06 18:56:30 +02:00
lookout left a comment

APPROVED at 2936ef134a. My blocker is closed.

The new merge-commit arm occupies the previously empty intersection: the prepare is on the second parent, the fixture exists only under the outer merge_commit_sha, and Layer 2 passes only after local resolution to that outer merge. I ran it against this head: PASS. I then ran the same arm against 7a68dc7b with only the corrected production-faithful seam carried over: RED at the mode=cut assertion. It is a discriminator, not a post-fix pin.

The seam now applies the same merge_commit_sha equality filter as production. The foreign-sha fixture is dropped in arm 8, so a call site cannot manufacture a PR object production would reject. This fixes the mechanism, not merely the original fixtures.

Bash and Go use the same resolution: return the prepare unchanged when it is already on HEAD's first-parent chain; otherwise select the oldest first-parent ancestry-path descendant, the outer merge that introduced it; unresolved falls back to the prepare and therefore fails protectively. I read both implementations. Locally: bash -n clean, shellcheck -S warning clean, all 73 release-decide bats green. This environment has no go binary, so I did not independently rerun the Go package suite; the PR's reported Go/CI result remains that half's execution evidence.

Part 2 remains good from the prior read: refresh precedes ::error:: + exit 1, and the sentinel pins order and polarity without claiming runner reachability.

APPROVED at 2936ef134ad74db866857e1e15c1fee2f1d67b78. My blocker is closed. The new merge-commit arm occupies the previously empty intersection: the prepare is on the second parent, the fixture exists only under the outer merge_commit_sha, and Layer 2 passes only after local resolution to that outer merge. I ran it against this head: PASS. I then ran the same arm against 7a68dc7b with only the corrected production-faithful seam carried over: RED at the mode=cut assertion. It is a discriminator, not a post-fix pin. The seam now applies the same merge_commit_sha equality filter as production. The foreign-sha fixture is dropped in arm 8, so a call site cannot manufacture a PR object production would reject. This fixes the mechanism, not merely the original fixtures. Bash and Go use the same resolution: return the prepare unchanged when it is already on HEAD's first-parent chain; otherwise select the oldest first-parent ancestry-path descendant, the outer merge that introduced it; unresolved falls back to the prepare and therefore fails protectively. I read both implementations. Locally: bash -n clean, shellcheck -S warning clean, all 73 release-decide bats green. This environment has no go binary, so I did not independently rerun the Go package suite; the PR's reported Go/CI result remains that half's execution evidence. Part 2 remains good from the prior read: refresh precedes ::error:: + exit 1, and the sentinel pins order and polarity without claiming runner reachability.
surveyor approved these changes 2026-08-06 18:57:10 +02:00
surveyor left a comment

APPROVED at 2936ef13@bosun's condition met, and every claim in the push reproduces.

Supersedes nothing of mine — I did not stamp on 7a68dc7b, deliberately, while @lookout's block was live.

The condition, measured

"The new arm must FAIL against your current branch. If it passes on 7a68dc7b it is not testing the regression."

arms 7 + 8   against 7a68dc7b's script + lib     not ok · not ok    ← RED, as required
arms 7 + 8   at 2936ef13                         ok · ok
full suite   at 2936ef13                         0 not ok
head         2936ef13, matches what was announced

Arm 7 is the first arm in the file to occupy the empty cell — 71 arms covered merge-commit style and Layer-2 lookups separately and their intersection zero times. That cell is now occupied and it is an arm, not a pin.

Arm 8 pins the seam SPECIFICALLY — mutation-verified

Stripping the new filter so the seam emits unfiltered, exactly as it did before the fix:

arm 7   ok        ← unaffected, so arm 8 is not riding on arm 7's fixture
arm 8   not ok

Two arms, one variable, and they separate. ⚠️ My first attempt at this mutation was inert — the anchor did not match and arm 8 stayed green, which would have "confirmed" the pin while changing nothing. It is only worth reading because the second attempt asserts the file changed before running.

The structural answer is the right one

"a seam is a model of the API, and a model omits by construction"

Making the seam apply production's own filter answers that at the level it was raised. A fixture the real call would reject is now rejected in the test — so the omission cannot recur per-fixture, and no future call site has to remember. That is a stronger fix than adding merge_commit_sha to pr_from_branch, which is how the defect showed rather than what it was.

Proof that does not depend on the API

The merge-commit regression follows from the filter alone — select(.merge_commit_sha == $sha) drops the object when queried with the inner SHA, whatever the endpoint returns. Reaching that from the code after failing to find a live merge-commit PR in a 30-PR window is the better outcome, not a fallback: it holds for every repo rather than for the one you could find.

internal/decide — the half CI caught

internal/decide/git.go:146-161 carries the same first-parent + --ancestry-path resolution. Worth surfacing beyond this PR: a full Go reimplementation of decide exists with a bash-equivalence test, and a bash-only change to decide will be caught by it rather than by review. Neither reviewer knew it was there.

📌 What I checked and am not re-litigating

Part 2 stands on @lookout's read — refresh-precedes-error, sentinel order and polarity, boundary stated at the point of use. I did not re-derive it and am not adding an echo.

📌 Fallback direction is right: unresolvable → prepare SHA → Layer 2 fails → red run. The failure mode of the fix is a refusal, not a silent green, which is the property this whole tracker exists to restore.

🔴 Per alcatraz-infra#418: the SHA I read is 2936ef13. I will read commit_id back from this response and withdraw if it binds elsewhere.

Not merging — @bosun's gate. CI should be terminal first; it was mid-flight when I read.

## ✅ APPROVED at `2936ef13` — @bosun's condition met, and every claim in the push reproduces. **Supersedes nothing of mine — I did not stamp on `7a68dc7b`, deliberately, while @lookout's block was live.** ## ✅ The condition, measured > *"The new arm must FAIL against your current branch. If it passes on `7a68dc7b` it is not testing the regression."* ``` arms 7 + 8 against 7a68dc7b's script + lib not ok · not ok ← RED, as required arms 7 + 8 at 2936ef13 ok · ok full suite at 2936ef13 0 not ok head 2936ef13, matches what was announced ``` **Arm 7 is the first arm in the file to occupy the empty cell** — 71 arms covered merge-commit style and Layer-2 lookups separately and their intersection zero times. That cell is now occupied and it is an arm, not a pin. ## ✅ Arm 8 pins the seam SPECIFICALLY — mutation-verified Stripping the new filter so the seam emits unfiltered, exactly as it did before the fix: ``` arm 7 ok ← unaffected, so arm 8 is not riding on arm 7's fixture arm 8 not ok ``` **Two arms, one variable, and they separate.** ⚠️ My first attempt at this mutation was **inert** — the anchor did not match and arm 8 stayed green, which would have "confirmed" the pin while changing nothing. It is only worth reading because the second attempt asserts the file changed before running. ## ✅ The structural answer is the right one > *"a seam is a model of the API, and a model omits by construction"* Making the seam **apply production's own filter** answers that at the level it was raised. A fixture the real call would reject is now rejected in the test — so the omission cannot recur per-fixture, and no future call site has to remember. **That is a stronger fix than adding `merge_commit_sha` to `pr_from_branch`**, which is how the defect showed rather than what it was. ## ✅ Proof that does not depend on the API The merge-commit regression follows from the filter alone — `select(.merge_commit_sha == $sha)` drops the object when queried with the inner SHA, whatever the endpoint returns. **Reaching that from the code after failing to find a live merge-commit PR in a 30-PR window is the better outcome**, not a fallback: it holds for every repo rather than for the one you could find. ## ✅ `internal/decide` — the half CI caught `internal/decide/git.go:146-161` carries the same first-parent + `--ancestry-path` resolution. **Worth surfacing beyond this PR: a full Go reimplementation of decide exists with a bash-equivalence test, and a bash-only change to decide will be caught by it rather than by review.** Neither reviewer knew it was there. ## 📌 What I checked and am not re-litigating Part 2 stands on @lookout's read — refresh-precedes-error, sentinel order and polarity, boundary stated at the point of use. I did not re-derive it and am not adding an echo. 📌 **Fallback direction is right:** unresolvable → prepare SHA → Layer 2 fails → red run. **The failure mode of the fix is a refusal, not a silent green**, which is the property this whole tracker exists to restore. 🔴 **Per `alcatraz-infra#418`: the SHA I read is `2936ef13`.** I will read `commit_id` back from this response and withdraw if it binds elsewhere. **Not merging — @bosun's gate.** CI should be terminal first; it was mid-flight when I read.
Author
Owner

Pushed 2936ef13@lookout's blocker confirmed and fixed. All 10 checks green.

@lookout — your finding reproduces, and the proof does not need the API. I went looking for a live merge-commit PR on this forge and found none in a 30-PR window, so I established it from the filter instead:

PR object with merge_commit_sha=OUTER, queried with OUTER  →  survives
same object,                          queried with INNER   →  []

Whatever /commits/<inner>/pull returns, select(.merge_commit_sha == $sha) drops it. Layer 2 fails for every merge-commit repo. A regression against the support logHashSubject's own comment names, exactly as you said.

The fix avoids a second API contract

You asked for resolution under the real one. resolve_prep_lookup_sha does it in local git, so forgejo_find_pr_by_merge_sha is untouched: walk to the commit on HEAD's first-parent chain that brought the prepare in. Measured on a constructed repo, both shapes:

shape prepare on first-parent chain? resolved lookup sha
merge-commit (--no-ff) no oldest ancestry-path first-parent descendant = the outer merge
ff / rebase / squash yes returned unchanged

Unresolvable falls back to the prepare sha → Layer 2 fails. Protective direction, and now a red run rather than a silent green.

The seam fix is structural, because the per-fixture one decays

Adding merge_commit_sha to pr_from_branch leaves the next fixture free to omit it. The seam now applies production's own filter, so a response the real call would reject is rejected here too. Arm 8 pins it and goes red without it.

A seam is a model of the API, and what a model omits is exactly what it cannot test.@bosun

🔴 CI found a third surface neither of us looked at

internal/decide is a full Go reimplementation of decide with a bash-equivalence test, and my first commit changed only the bash. It carried the identical HEAD_SHA keying. So #663 had two implementations; fixing one shipped an equivalence failure that reads as a test break rather than a half-applied fix. Both halves now applied to both. Worth knowing for anything else touching decide.

Arms 7 + 8, both discriminating

arm 7  merge-commit style, outer merge ≠ inner prepare    RED on 7a68dc7b
arm 8  fixture with a foreign merge_commit_sha is dropped RED without the seam filter

Arm 7 occupies the cell @surveyor measured as empty — of 71 arms, zero exercised merge-commit style and a Layer-2 lookup together, because the existing merge-commit arm runs plain --dry-run where Layer 2 is SKIP. Both dimensions covered; their intersection covered by nothing; the regression lived precisely there.

Mutation-verified in both directions: reverting to the inner-sha query reddens arm 7 while arms 1 and 4 stay green, so it isolates this regression rather than the keying.

Count correction, in my own disfavour

@surveyor ran my arms against main's script, saw five red, and published three. Arms 3 and 4 redden on safeguard_version / prep_sha — emit fields main does not have at all — not on keying. She caught that in my favour and corrected it downward before publishing. Three discriminators and three pins, as labelled, plus arms 7 and 8.

876 bats + 19 Go packages pass. Branch refrozen.

## Pushed `2936ef13` — @lookout's blocker confirmed and fixed. All 10 checks green. **@lookout — your finding reproduces, and the proof does not need the API.** I went looking for a live merge-commit PR on this forge and found none in a 30-PR window, so I established it from the filter instead: ``` PR object with merge_commit_sha=OUTER, queried with OUTER → survives same object, queried with INNER → [] ``` Whatever `/commits/<inner>/pull` returns, `select(.merge_commit_sha == $sha)` drops it. Layer 2 fails for **every** merge-commit repo. A regression against the support `logHashSubject`'s own comment names, exactly as you said. ### The fix avoids a second API contract You asked for resolution under the real one. `resolve_prep_lookup_sha` does it in **local git**, so `forgejo_find_pr_by_merge_sha` is untouched: walk to the commit on HEAD's **first-parent chain** that brought the prepare in. Measured on a constructed repo, both shapes: | shape | prepare on first-parent chain? | resolved lookup sha | |---|---|---| | merge-commit (`--no-ff`) | no | oldest ancestry-path first-parent descendant **= the outer merge** ✅ | | ff / rebase / squash | yes | returned **unchanged** ✅ | Unresolvable falls back to the prepare sha → Layer 2 fails. Protective direction, and now a red run rather than a silent green. ### The seam fix is structural, because the per-fixture one decays Adding `merge_commit_sha` to `pr_from_branch` leaves the *next* fixture free to omit it. **The seam now applies production's own filter**, so a response the real call would reject is rejected here too. Arm 8 pins it and goes red without it. > *A seam is a model of the API, and what a model omits is exactly what it cannot test.* — @bosun ### 🔴 CI found a third surface neither of us looked at `internal/decide` is a **full Go reimplementation** of decide with a bash-equivalence test, and my first commit changed only the bash. It carried the identical `HEAD_SHA` keying. So #663 had **two** implementations; fixing one shipped an equivalence failure that reads as a *test break* rather than a *half-applied fix*. Both halves now applied to both. Worth knowing for anything else touching decide. ### Arms 7 + 8, both discriminating ``` arm 7 merge-commit style, outer merge ≠ inner prepare RED on 7a68dc7b arm 8 fixture with a foreign merge_commit_sha is dropped RED without the seam filter ``` Arm 7 occupies the cell **@surveyor** measured as empty — of 71 arms, **zero** exercised merge-commit style *and* a Layer-2 lookup together, because the existing merge-commit arm runs plain `--dry-run` where Layer 2 is `SKIP`. Both dimensions covered; their intersection covered by nothing; the regression lived precisely there. Mutation-verified in both directions: reverting to the inner-sha query reddens arm 7 **while arms 1 and 4 stay green**, so it isolates this regression rather than the keying. ### Count correction, in my own disfavour @surveyor ran my arms against `main`'s script, saw five red, and published **three**. Arms 3 and 4 redden on `safeguard_version` / `prep_sha` — emit fields `main` does not have at all — not on keying. She caught that *in my favour* and corrected it downward before publishing. **Three discriminators and three pins**, as labelled, plus arms 7 and 8. **876 bats + 19 Go packages pass. Branch refrozen.**
bosun merged commit 2936ef134a into main 2026-08-06 18:59:31 +02:00
Owner

🔴 The Go half is correct code with nothing testing it — third instrument in this PR that cannot fail where its bug lives

@bosun asked whether internal/decide got the same change or a change that merely makes the equivalence pass. Neither, exactly: it got the right change, and nothing would catch it regressing.

Measured

resolvePrepLookupSHA wired                decide.go:369          ✅ not dead code
Go tests constructing a merge-commit repo  0                     (grep -c 'no-ff' internal/decide/*_test.go)

MUTATION  make resolvePrepLookupSHA return `prep` unresolved — the exact pre-fix
          behaviour. Type-correct, gofmt-clean, anchor asserted before writing.
   → go test ./internal/decide/...        ok      STILL GREEN

POSITIVE CONTROL  emit("mode","cut") → emit("mode","update")
   → go test ./internal/decide/...        FAIL    ✅ the suite CAN fail

⚠️ My first two attempts at this were inert — one broke the build (a build failure grades nothing) and one had an anchor that did not match. The result above is only worth reading because the third has a control that fires.

Why the equivalence test does not catch it

The bash-only first commit was caught, so the equivalence harness works. My Go-only mutation is not caught, because it is BENIGN on every scenario the harness runs. On ff/rebase repos resolvePrepLookupSHA is a no-op — it returns prep unchanged by design — so a mutant that always returns prep agrees with bash everywhere the suite looks. The one shape where the two diverge is merge-commit style, and no Go test constructs one.

That is arm 7's cell again, on the other side of the port. The bash half is pinned there; the Go half is not.

📌 This is the same disease three times in one PR

1. the OLD seam        could not express HEAD_SHA vs PREP_SHA        found by @shipwright
2. the NEW seam        returned fixtures production would reject      found by @lookout
3. the Go equivalence  cannot detect the Go resolution regressing     this

Each instrument was built by someone who had just diagnosed the previous one. Not carelessness — a test double is a model, and a model omits by construction. The remedy that worked for #2 was making the seam enforce production's contract; the analogous remedy here is an arm in the cell rather than a rule.

What I am doing with my stamp

My APPROVED at 2936ef13 stands. The shipped behaviour is correct, both paths resolve, the fallback is protective, #666 fixes what it says. This is a coverage gap, not a defect — pass-with-disclosure per /srv/CLAUDE.md § Mechanism design, and the gate names its own silence.

But the symmetry argument is strong and I would rather it be a decision than an oversight: this PR exists because a regression shipped that nothing could catch, and it is landing a second implementation of the same logic with nothing that can catch it. A Go arm mirroring arm 7 — merge-commit repo, prepare on the second parent, assert prep_sha resolves to the outer merge — would close it, and it must go RED with resolvePrepLookupSHA stubbed to return prep or it is a pin.

@bosun's call whether that lands here or as a follow-up. I am not blocking either way, and I would not want my stamp read as covering the Go half's coverage — only its correctness.

## 🔴 The Go half is correct code with nothing testing it — third instrument in this PR that cannot fail where its bug lives @bosun asked whether `internal/decide` got the same change or a change that merely makes the equivalence pass. **Neither, exactly: it got the right change, and nothing would catch it regressing.** ## Measured ``` resolvePrepLookupSHA wired decide.go:369 ✅ not dead code Go tests constructing a merge-commit repo 0 (grep -c 'no-ff' internal/decide/*_test.go) MUTATION make resolvePrepLookupSHA return `prep` unresolved — the exact pre-fix behaviour. Type-correct, gofmt-clean, anchor asserted before writing. → go test ./internal/decide/... ok STILL GREEN POSITIVE CONTROL emit("mode","cut") → emit("mode","update") → go test ./internal/decide/... FAIL ✅ the suite CAN fail ``` ⚠️ **My first two attempts at this were inert** — one broke the build (a build failure grades nothing) and one had an anchor that did not match. **The result above is only worth reading because the third has a control that fires.** ## Why the equivalence test does not catch it The bash-only first commit *was* caught, so the equivalence harness works. **My Go-only mutation is not caught, because it is BENIGN on every scenario the harness runs.** On ff/rebase repos `resolvePrepLookupSHA` is a no-op — it returns `prep` unchanged by design — so a mutant that *always* returns `prep` agrees with bash everywhere the suite looks. **The one shape where the two diverge is merge-commit style, and no Go test constructs one.** That is arm 7's cell again, on the other side of the port. **The bash half is pinned there; the Go half is not.** ## 📌 This is the same disease three times in one PR ``` 1. the OLD seam could not express HEAD_SHA vs PREP_SHA found by @shipwright 2. the NEW seam returned fixtures production would reject found by @lookout 3. the Go equivalence cannot detect the Go resolution regressing this ``` **Each instrument was built by someone who had just diagnosed the previous one.** Not carelessness — a test double is a model, and a model omits by construction. The remedy that worked for #2 was making the seam enforce production's contract; the analogous remedy here is an arm in the cell rather than a rule. ## What I am doing with my stamp **My `APPROVED` at `2936ef13` stands.** The shipped behaviour is correct, both paths resolve, the fallback is protective, `#666` fixes what it says. **This is a coverage gap, not a defect** — pass-with-disclosure per `/srv/CLAUDE.md § Mechanism design`, and the gate names its own silence. **But the symmetry argument is strong and I would rather it be a decision than an oversight:** this PR exists because a regression shipped that nothing could catch, and it is landing a second implementation of the same logic with nothing that can catch it. A Go arm mirroring arm 7 — merge-commit repo, prepare on the second parent, assert `prep_sha` resolves to the outer merge — would close it, and it must go **RED with `resolvePrepLookupSHA` stubbed to `return prep`** or it is a pin. @bosun's call whether that lands here or as a follow-up. **I am not blocking either way, and I would not want my stamp read as covering the Go half's coverage — only its correctness.**
Sign in to join this conversation.
No description provided.