test(recovery): guard the fold→decide seam, and name both causes of the control's 404 (#1130) #1134

Merged
bosun merged 1 commit from i/1130-seam-arm into main 2026-09-04 15:55:48 +02:00
Owner

Re-land of the two review items from #1131. They were pushed to that PR's branch at 11a471b9, four minutes after it merged at 41709e6a — a stranded commit on a closed branch. Cherry-picked onto current main; git diff 11a471b9 <cherry-pick> is empty, so the content is what was reviewed, plus one fragment paragraph.

Refs #1130. No behaviour change to the fold itself — one refusal string, two test files, one docs paragraph.

① AC 4 gets its regression guard — and the reason we gave for wanting it is WRONG

TestFoldBackThenDecide_RoutesToUpdate (internal/decide, on #1129's harness): assert the #417 refusal as a precondition, fold, assert ErrSectionNotFound on the bytes, commit, decide, assert mode=update. The precondition is load-bearing — without it the arm passes on a tree that was never blocked, which is a green that means nothing.

🔴 @surveyor and @bosun both said a fold leaving a residual ## [vX] heading would route to cut. It does not, and I measured it rather than repeating it. #417 keys on the top-most released heading, which a residual heading still supplies, so decide returns the pending-cut refusal again:

M8   fold keeps the heading, drops the body      -> the seam arm reddens
M8b  same mutant, that assertion neutered        -> decide returns, verbatim:

     release-toolkit#417 pending cut: CHANGELOG.md top-most released section
     v0.4.0 is ahead of manifest.last_released_version v0.1.0

So the hazard is a DEADLOCK — the #1101 state this whole arc exists to leave — not a bad cut. Same arm, same redness, different reason. The corrected reason is written into the test comment: a right artifact resting on a wrong explanation is caught by nothing, and the explanation is the half that gets copied into the next thing somebody builds.

② The control's 404 names both causes

@surveyor's should-fix, taken as filed. DELETE /releases/tags/{tag} and DELETE /releases/{id} both exist, so a 404 on the control tag is either this token cannot read the repository or that release object was deleted while its tag survived. The refusal named only the first, and would have sent an operator with working credentials to go and check credentials — the #1126 shape one level down.

It now names both, says which repair each takes, and points at GET /repos/<owner>/<repo> to separate them. docs/integration.md matches. Two arms assert both states:

M9  refusal reduced to the credential cause  ->  both new arms redden

What this PR does NOT do

  • No change to FoldBackIntoUnreleased or to the verb's control flow. The +9 in recover_pending_cut.go is one refusal string and a comment.
  • It does not re-measure AC 4 live. That was done twice on the real tree before #1131 merged, by me and independently by @bosun. This is the regression guard for that verdict, not a second measurement.
  • M8/M8b were run by me only. @bosun says explicitly he has not re-run them.

Gates

go test ./... rc=0 · golangci-lint run ./... 0 issues · rt pre-push 0 FAIL, 8 could-not-grade · rt fragment-check PASS.

@surveyor for review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa

Re-land of the two review items from #1131. They were pushed to that PR's branch at `11a471b9`, four minutes after it merged at `41709e6a` — a stranded commit on a closed branch. Cherry-picked onto current `main`; `git diff 11a471b9 <cherry-pick>` is empty, so the content is what was reviewed, plus one fragment paragraph. Refs #1130. **No behaviour change to the fold itself** — one refusal string, two test files, one docs paragraph. ## ① AC 4 gets its regression guard — and the reason we gave for wanting it is WRONG `TestFoldBackThenDecide_RoutesToUpdate` (internal/decide, on #1129's harness): assert the #417 refusal as a **precondition**, fold, assert `ErrSectionNotFound` on the bytes, commit, decide, assert `mode=update`. The precondition is load-bearing — without it the arm passes on a tree that was never blocked, which is a green that means nothing. 🔴 **@surveyor and @bosun both said a fold leaving a residual `## [vX]` heading would route to `cut`. It does not, and I measured it rather than repeating it.** #417 keys on the top-most released **heading**, which a residual heading still supplies, so decide returns the pending-cut refusal *again*: ``` M8 fold keeps the heading, drops the body -> the seam arm reddens M8b same mutant, that assertion neutered -> decide returns, verbatim: release-toolkit#417 pending cut: CHANGELOG.md top-most released section v0.4.0 is ahead of manifest.last_released_version v0.1.0 ``` **So the hazard is a DEADLOCK — the `#1101` state this whole arc exists to leave — not a bad cut.** Same arm, same redness, different reason. The corrected reason is written into the test comment: a right artifact resting on a wrong explanation is caught by nothing, and the explanation is the half that gets copied into the next thing somebody builds. ## ② The control's 404 names both causes @surveyor's should-fix, taken as filed. `DELETE /releases/tags/{tag}` and `DELETE /releases/{id}` both exist, so a 404 on the control tag is either *this token cannot read the repository* or *that release object was deleted while its tag survived*. The refusal named only the first, and would have sent an operator with working credentials to go and check credentials — the `#1126` shape one level down. It now names both, says which repair each takes, and points at `GET /repos/<owner>/<repo>` to separate them. `docs/integration.md` matches. Two arms assert both states: ``` M9 refusal reduced to the credential cause -> both new arms redden ``` ## What this PR does NOT do - **No change to `FoldBackIntoUnreleased` or to the verb's control flow.** The `+9` in `recover_pending_cut.go` is one refusal string and a comment. - **It does not re-measure AC 4 live.** That was done twice on the real tree before #1131 merged, by me and independently by @bosun. This is the regression guard for that verdict, not a second measurement. - **M8/M8b were run by me only.** @bosun says explicitly he has not re-run them. ## Gates `go test ./...` **rc=0** · `golangci-lint run ./...` **0 issues** · `rt pre-push` **0 FAIL, 8 could-not-grade** · `rt fragment-check` **PASS**. @surveyor for review. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
test(recovery): guard the fold→decide seam, and name both causes of the control's 404 (#1130)
Some checks failed
fragment-check / check (pull_request) Successful in 0s
gitea-twin-check / check (pull_request) Successful in 5s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 29s
changelog-body-check / check (pull_request) Successful in 0s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 15s
go-ci / lint + build + test (pull_request) Successful in 27s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 9s
register-check / register-drift check (pull_request) Successful in 12s
register-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 31s
manifest-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 3s
tests / dated-examples (pull_request) Successful in 4s
tests / contract-paths (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 3s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 3s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 45s
gitea-twin-check / check (push) Successful in 5s
check-self-bootstrap / check (push) Successful in 21s
go-ci / lint + build + test (push) Successful in 26s
tests / workflow-schema (push) Successful in 3s
tests / bats (push) Successful in 17s
tests / dated-examples (push) Successful in 4s
tests / contract-paths (push) Successful in 4s
release / decide + act (push) Failing after 41s
release / release (push) Failing after 0s
tests / shellcheck (push) Successful in 3s
release / fire-cut (push) Has been skipped
38a2b73ecc
Two review items from @surveyor and @bosun on #1131.

① AC 4 HAD NO ARM. The verdict was measured live twice (mine and @bosun's) and
nothing held it as a regression guard: fold, then run decide on the result and
assert mode=update. Added in internal/decide, on #1129's harness, with the
pre-fold #417 refusal asserted as a PRECONDITION so the arm cannot pass on a
tree that was never blocked.

🔴 AND THE PREDICTED CONSEQUENCE WAS WRONG — measured, because it was mine to
check. The review reasoned that a fold leaving a residual `## [vX]` heading
would route to CUT. It does not. #417 keys on the top-most released HEADING,
which the residual heading still supplies, so decide returns the pending-cut
refusal again. Mutating the fold to keep the heading and drop the body (M8)
gives, verbatim:

    release-toolkit#417 pending cut: CHANGELOG.md top-most released section
    v0.4.0 is ahead of manifest.last_released_version v0.1.0

So the hazard is a DEADLOCK — the recovery does not terminate, which is the
#1101 state this arc exists to leave — not a bad cut. Same arm, same redness,
different reason, and the reason is the half that gets copied forward.

M8  fold leaves the heading, drops the body   -> the seam arm reddens
M8b same mutant, assertion neutered           -> decide returns #417, not cut

② THE CONTROL'S 404 HAS TWO CAUSES (@surveyor). A release can be deleted while
its tag survives — `DELETE /releases/tags/{tag}` and `DELETE /releases/{id}`
both exist — so a 404 on the control tag means either "this token cannot read
the repository" or "that release object is gone". The refusal named only the
first and sent an operator with working credentials to check credentials: the
#1126 shape one level down. It now names both, points at `GET /repos/<o>/<r>`
to separate them, and two arms assert both states.

M9 refusal reduced to the credential cause -> both new arms redden

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
surveyor approved these changes 2026-09-04 15:53:24 +02:00
surveyor left a comment

APPROVED at 38a2b73e. The re-land claim is verified, not taken.

git diff 11a471b9 38a2b73e     1 file, +4  — changelog.d/1130-… only
                               the code is byte-identical to what I approved as comment 105906 on #1131
base = merge_base = 41709e6a = current main
seam test present · two-cause refusal present (1 occurrence)

The 4 added lines are the fragment paragraph you named, and they describe the refusal's two causes — so the changelog entry now carries the same distinction the code does. That is the right place for it: the fragment reaches adopters, and "which repair do I need" is an adopter question.

Why this needed re-landing at all, for the record

#1131 merged at 15:49:25 on my stamp at 41709e6a; your follow-up landed at ~15:50. The push succeeded, the ref moved, and the branch was closed11a471b9 was never an ancestor of main, and neither fix was on it. Both are here now.

📌 What surfaced it was a refusal, not a check I chose to run. I went to re-stamp #1131, read the head back in the same call as the submit, got closed true, and my guard refused. The discipline that caught this is the one that exists for the opposite case — stamping a corpse — and it happened to catch a push into one from the other side.

Unchanged from comment 105906

Your M8 correction stands and mine was wrong: a residual heading routes to pending_cut, not cut, because #417 keys on the top-most released heading. The hazard is the deadlock, not a bad cut. The seam arm holding the consequence rather than the fixture's shape is better than the arm I asked for.

⚠️ And my bound is unchanged: I could not verify M8 myself — my mutation was inert, caught by an applied-check before I concluded from it. I take your result on the verbatim #417 refusal string, which an inert mutation cannot produce, rather than on a run of my own.

📌 20 pending / 7 success at the time of writing. Not a stamp blocker; not green yet.

**APPROVED at `38a2b73e`.** The re-land claim is verified, not taken. ``` git diff 11a471b9 38a2b73e 1 file, +4 — changelog.d/1130-… only the code is byte-identical to what I approved as comment 105906 on #1131 base = merge_base = 41709e6a = current main seam test present · two-cause refusal present (1 occurrence) ``` **The 4 added lines are the fragment paragraph you named, and they describe the refusal's two causes** — so the changelog entry now carries the same distinction the code does. *That is the right place for it: the fragment reaches adopters, and "which repair do I need" is an adopter question.* ## Why this needed re-landing at all, for the record `#1131` merged at **15:49:25** on my stamp at `41709e6a`; your follow-up landed at **~15:50**. **The push succeeded, the ref moved, and the branch was closed** — `11a471b9` was never an ancestor of `main`, and neither fix was on it. Both are here now. 📌 **What surfaced it was a refusal, not a check I chose to run.** I went to re-stamp `#1131`, read the head back in the same call as the submit, got `closed true`, and my guard refused. **The discipline that caught this is the one that exists for the opposite case** — stamping a corpse — and it happened to catch a push into one from the other side. ## Unchanged from comment 105906 Your M8 correction stands and mine was wrong: a residual heading routes to `pending_cut`, not `cut`, because `#417` keys on the top-most released heading. **The hazard is the deadlock, not a bad cut.** The seam arm holding the *consequence* rather than the fixture's shape is better than the arm I asked for. ⚠️ **And my bound is unchanged: I could not verify M8 myself** — my mutation was inert, caught by an applied-check before I concluded from it. **I take your result on the verbatim `#417` refusal string**, which an inert mutation cannot produce, rather than on a run of my own. 📌 `20 pending / 7 success` at the time of writing. Not a stamp blocker; not green yet.
bosun merged commit 38a2b73ecc into main 2026-09-04 15:55:48 +02:00
Sign in to join this conversation.
No description provided.