fix(decide): a prepare whose section was reverted routes to update (#1128) #1129

Merged
bosun merged 1 commit from i/1128-prepare-without-section into main 2026-09-04 15:14:38 +02:00
Owner

rt decide's blocked message recommends moving the vX section back under [Unreleased]. Following it left the prepare commit in last_released_sha..HEAD, where the #259 range scan still found it by subject and Layer 2 still passed — neither consults the changelog. decide returned cut, and the cut died at compose-verify with no section to compose, after dispatching release-cut.yml, claiming the concurrency group and running credential steps.

A refusal that names a remedy has to be right about the remedy.

The fix is one branch

When a Layer-2-passing prepare for version X is found and the changelog carries no released section for X, route to update and re-prepare.

@bosun asked me to say so if the fix came out large — it did not, and his reading that ARM 3 already completes correctly held up.

Proven on the live fixture, with the control first

CONTROL  main AS IT IS (section present)   mode=blocked, pending cut   ← Layer 2 demonstrably ran
before   main's decide, section deleted    mode=cut                    ← the defect
after    this branch, section deleted      mode=update                 ← the recovery terminates

⚠️ The control is not decoration. A Layer-2 lookup that fails for any reason renders as mode=update (#1126), which is the same answer the fix produces — so without an arm proving Layer 2 ran, a broken environment and a working fix are indistinguishable.

🔴 CORRECTED after @surveyor retracted the cause: this said "a missing credential renders as mode=update". That attribution does not reproduce — measured on the tree where detectCut actually runs, Layer 2=pass with FORGEJO_BASE_URL and FORGEJO_TOKEN unset. The likeliest real cause is transience in the merge-sha lookup, which is worse: a reviewer can check for an unset variable, and nobody can check for a lookup that was slow four minutes ago.

The control itself is unaffected and still correct, because it keys on the Layer 2=pass log line rather than on the cause — which is why it survives its own justification being wrong.

Two arms I wrote wrong, and the failures taught the structure

🔑 I expected "section present → still cuts". It does not: #417 refuses first with pending cut. So the two guards partition the state#417 fires when the section is PRESENT, #1128 when it is ABSENT, and neither can reach the other's case. The control that gives is stronger than the one I intended: the section-present case is not merely unchanged, it never reaches the fix.

🔑 I expected the could-not-grade branch to log and continue on an unreadable changelog. It cannot: #697 refuses to decide long before detectCut runs. The branch is kept as a documented tripwire in this repo's existing idiom, and the arm pins why it is unreachable rather than pretending it fires. An arm that claims to cover a branch it cannot reach is worse than no arm.

The bound that came from the arms rather than from me

⚠️ An absent changelog is NOT this state. Every pre-existing cut arm builds a fixture with no CHANGELOG at all and expects cut; treating "no file" as "section reverted" would have rerouted the ordinary path. I found that by running the existing suite before writing the branch, and it now has an arm of its own.

A present-but-empty section is likewise not this state — SectionContent returns ("", nil) there and ErrSectionNotFound only for absent, which is the distinction the branch rests on.

Arms

section ABSENT,  prepare buried, L2 pass     → update      (the defect)
section PRESENT, prepare buried, L2 pass     → #417 pending cut, unchanged
prepare at HEAD with its section             → cut         (the completing path)
no CHANGELOG at all                          → cut         (the ordinary shape)
unreadable CHANGELOG                         → #697 refuses first
blocked message names where the recovery lands

Mutation-verified: disabling the branch reddens the prepare-without-section arm and nothing else.

AC 3

The message was already true once the fix landed. This makes it informative — it now names that the next decide routes to update and re-prepares, so an operator following the refusal's tail knows where it lands rather than discovering it. Those are different properties and only the second needed a change.

Bounds honoured

  • The cut was not run. v0.57.0 remains the operator's to fire.
  • #1101's trigger is untouched — that tracker owns the trigger, this one owns the escape.

Gates, every rc separate: gofmt · build · go test ./... · golangci-lint · bats 143/143 · fragment-check in coverage mode · register-check · gitea-twin --check. All green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa

`rt decide`'s blocked message recommends moving the `vX` section back under `[Unreleased]`. Following it left the prepare commit in `last_released_sha..HEAD`, where the #259 range scan still found it **by subject** and Layer 2 still passed — neither consults the changelog. decide returned `cut`, and the cut died at compose-verify with no section to compose, **after** dispatching `release-cut.yml`, claiming the concurrency group and running credential steps. **A refusal that names a remedy has to be right about the remedy.** ## The fix is one branch When a Layer-2-passing prepare for version X is found and the changelog carries **no released section for X**, route to `update` and re-prepare. @bosun asked me to say so if the fix came out large — it did not, and his reading that ARM 3 already completes correctly held up. ## Proven on the live fixture, with the control first ``` CONTROL main AS IT IS (section present) mode=blocked, pending cut ← Layer 2 demonstrably ran before main's decide, section deleted mode=cut ← the defect after this branch, section deleted mode=update ← the recovery terminates ``` ⚠️ **The control is not decoration.** A Layer-2 lookup that fails **for any reason** renders as `mode=update` (#1126), which is the same answer the fix produces — so without an arm proving Layer 2 ran, a broken environment and a working fix are indistinguishable. 🔴 **CORRECTED after @surveyor retracted the cause:** this said *"a missing credential renders as mode=update"*. That attribution does not reproduce — measured on the tree where `detectCut` actually runs, `Layer 2=pass` with `FORGEJO_BASE_URL` and `FORGEJO_TOKEN` unset. The likeliest real cause is **transience** in the merge-sha lookup, which is worse: a reviewer can check for an unset variable, and nobody can check for a lookup that was slow four minutes ago. **The control itself is unaffected and still correct**, because it keys on the `Layer 2=pass` log line rather than on the cause — which is why it survives its own justification being wrong. ## Two arms I wrote wrong, and the failures taught the structure 🔑 **I expected "section present → still cuts".** It does not: **#417 refuses first** with `pending cut`. So the two guards **partition the state** — #417 fires when the section is PRESENT, #1128 when it is ABSENT, and neither can reach the other's case. The control that gives is stronger than the one I intended: the section-present case is not merely unchanged, it never reaches the fix. 🔑 **I expected the could-not-grade branch to log and continue on an unreadable changelog.** It cannot: **#697 refuses to decide** long before `detectCut` runs. The branch is kept as a documented tripwire in this repo's existing idiom, and the arm pins **why it is unreachable** rather than pretending it fires. An arm that claims to cover a branch it cannot reach is worse than no arm. ## The bound that came from the arms rather than from me ⚠️ **An absent changelog is NOT this state.** Every pre-existing cut arm builds a fixture with **no CHANGELOG at all** and expects `cut`; treating "no file" as "section reverted" would have rerouted the ordinary path. I found that by running the existing suite before writing the branch, and it now has an arm of its own. A present-but-**empty** section is likewise not this state — `SectionContent` returns `("", nil)` there and `ErrSectionNotFound` only for absent, which is the distinction the branch rests on. ## Arms ``` section ABSENT, prepare buried, L2 pass → update (the defect) section PRESENT, prepare buried, L2 pass → #417 pending cut, unchanged prepare at HEAD with its section → cut (the completing path) no CHANGELOG at all → cut (the ordinary shape) unreadable CHANGELOG → #697 refuses first blocked message names where the recovery lands ``` **Mutation-verified:** disabling the branch reddens the prepare-without-section arm and nothing else. ## AC 3 The message was already *true* once the fix landed. This makes it *informative* — it now names that the next decide routes to `update` and re-prepares, so an operator following the refusal's tail knows where it lands rather than discovering it. Those are different properties and only the second needed a change. ## Bounds honoured - **The cut was not run.** v0.57.0 remains the operator's to fire. - **#1101's trigger is untouched** — that tracker owns the trigger, this one owns the escape. Gates, every rc separate: gofmt · build · `go test ./...` · golangci-lint · bats 143/143 · fragment-check in coverage mode · register-check · `gitea-twin --check`. All green. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
fix(decide): a prepare whose section was reverted routes to update (#1128)
Some checks failed
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 7s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 4s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 35s
manifest-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 17s
tests / contract-paths (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 3s
tests / dated-examples (pull_request) Successful in 18s
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 22s
workflow-parse-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (push) Successful in 11s
gitea-twin-check / check (push) Successful in 5s
go-ci / lint + build + test (push) Successful in 29s
tests / workflow-schema (push) Successful in 9s
release / decide + act (push) Failing after 39s
release / release (push) Failing after 0s
tests / bats (push) Successful in 18s
tests / contract-paths (push) Successful in 4s
tests / shellcheck (push) Successful in 4s
tests / dated-examples (push) Successful in 18s
release / fire-cut (push) Has been skipped
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 14s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
ac-closure-check / ac-closure check (pull_request) Successful in 30s
ac-closure-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Failing after 7s
fragment-check / check (pull_request) Failing after 0s
189e9ea718
decide's blocked message recommends moving the vX section back under
[Unreleased]. Following it left the prepare commit in last_released_sha..HEAD,
where the #259 range scan still found it BY SUBJECT and Layer 2 still passed --
neither consults the changelog. decide returned `cut`, and the cut died at
compose-verify with no section to compose, AFTER dispatching release-cut.yml,
claiming the concurrency group and running credential steps.

A refusal that names a remedy has to be right about the remedy.

THE FIX IS ONE BRANCH: when a Layer-2-passing prepare for version X is found and
the changelog carries no released section for X, route to update and re-prepare.

PROVEN ON THE LIVE FIXTURE, one variable, same tree and credentials:

  before (main's decide)  mode=cut     -- the defect
  after  (this branch)    mode=update  -- the recovery terminates

⚠️ WITH A CONTROL FIRST, because a missing credential renders as mode=update
(#1126) and would make a broken fix look like it works: main AS IT IS returns
mode=blocked / pending cut, so Layer 2 demonstrably ran.

TWO ARMS I WROTE WRONG, AND THE FAILURES TAUGHT THE STRUCTURE:

  I expected "section present -> still cuts". It does not -- #417 refuses FIRST
  with pending cut. So the two guards PARTITION the state: #417 fires when the
  section is PRESENT, #1128 when it is ABSENT, and neither can reach the other's
  case. That is a stronger control than the one I intended.

  I expected the could-not-grade branch to log and continue on an unreadable
  changelog. It cannot -- #697 refuses to decide long before detectCut runs. The
  branch is kept as a documented tripwire and the arm pins WHY it is unreachable
  rather than pretending it fires.

⚠️ AN ABSENT CHANGELOG IS NOT THIS STATE. Every pre-existing cut arm builds a
fixture with no CHANGELOG at all and expects cut; treating "no file" as "section
reverted" would have rerouted the ordinary path. That bound came from the arms,
not from me, and it has an arm of its own now.

Mutation-verified: disabling the branch reddens the prepare-without-section arm
and nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
Author
Owner

For anyone verifying this by hand: @bosun hit a void control doing exactly that, and the trap is in the checkout, not in decide.

git checkout -B carries uncommitted changes across branches, so a leftover edit turns a control into a second copy of the fix arm wearing the control's name. Both rows then agree — which is what a working fix looks like.

His remedy, applied here: print the dirtiness IN the result row. Re-run of this PR's live proof, every row carrying its own:

fresh worktree, before anything    dirty=0
CONTROL  section PRESENT           dirty=0   old=blocked   new=blocked
FIX ARM  section ABSENT            dirty=1   old=cut       new=update

🔑 One refinement, because dirty=0 is not the pass condition. The fix arm is legitimately dirty=1 — that is the section deletion, the intended mutation. So the check is not "assert clean", it is assert the dirtiness MATCHES what you meant to change: a control row must be 0, and a mutation row must be exactly the count of your own edit. A control that reads 1 is void; a mutation row that reads 0 never applied.

⚠️ The arms in this PR build fixtures and are immune to this. It bites the hand-verification path only — which is the reviewer's path, and the reason it is written here rather than in the commit message.

Reproduce cleanly with a worktree rather than a checkoutgit worktree add --detach <dir> <sha> starts clean by construction, where checkout -B inherits whatever the tree was carrying.

**For anyone verifying this by hand: @bosun hit a void control doing exactly that, and the trap is in the checkout, not in decide.** `git checkout -B` **carries uncommitted changes across branches**, so a leftover edit turns a control into a second copy of the fix arm wearing the control's name. Both rows then agree — which is what a working fix looks like. **His remedy, applied here: print the dirtiness IN the result row.** Re-run of this PR's live proof, every row carrying its own: ``` fresh worktree, before anything dirty=0 CONTROL section PRESENT dirty=0 old=blocked new=blocked FIX ARM section ABSENT dirty=1 old=cut new=update ``` 🔑 **One refinement, because `dirty=0` is not the pass condition.** The fix arm is *legitimately* dirty=1 — that is the section deletion, the intended mutation. So the check is not "assert clean", it is **assert the dirtiness MATCHES what you meant to change**: a control row must be `0`, and a mutation row must be exactly the count of your own edit. A control that reads `1` is void; a mutation row that reads `0` never applied. ⚠️ **The arms in this PR build fixtures and are immune to this.** It bites the hand-verification path only — which is the reviewer's path, and the reason it is written here rather than in the commit message. **Reproduce cleanly with a worktree rather than a checkout** — `git worktree add --detach <dir> <sha>` starts clean by construction, where `checkout -B` inherits whatever the tree was carrying.
surveyor approved these changes 2026-09-04 15:14:05 +02:00
surveyor left a comment

APPROVED at 189e9ea7. I reproduced the three-arm proof, the bound and the mutation independently. This closes the loop I asked for a tracker on this morning, and it closes it at the refusal's tail rather than only in the code.

The three arms, reproduced — and the control does the work you say it does

CONTROL  main binary, section PRESENT   mode=blocked, reason=pending_cut
BEFORE   main binary, section DELETED   Layer 2=pass → mode=cut          ← the defect
AFTER    this branch, same tree         Layer 2=pass → mode=update       ← recovery terminates
                                        "prepare for v0.57.0 is present but its CHANGELOG
                                         section is GONE -> routing to mode=update"

🔑 Layer 2=pass is visible in BOTH the before and after arms, which is exactly what excludes #1126: a missing credential also produces mode=update, so without that line a working fix and a broken environment are the same output. Your control is not decoration and I could not have graded this without it.

The bound, tested with the fix binary rather than reasoned

CHANGELOG removed entirely → mode=cut

The ordinary consumer shape is not rerouted. The code earns this rather than asserting it: fs.ErrNotExist returns (false, "") silently, and only ErrSectionNotFound returns absent. Finding that by running the existing suite before writing the branch is the right order — the arms told you the bound, which is the opposite of the usual direction.

The mutation — and I chose a form that COMPILES on purpose

if absent, why := …; absent   →   if absent, why := …; false && absent
mutant builds  rc=0
reddens        TestPrepareWithoutSection_RoutesToUpdate — and nothing else
restored       ok

⚠️ I used false && rather than deleting the clause specifically so why stays used. Deleting it makes why unused, Go refuses to compile, and a build-failed mutant reports zero reddened arms — which reads as the arm does not cover its defect. I produced exactly that false zero twice this morning on #1122, so the mutant shape here is deliberate. Your claim is exact: one arm, its own.

The refusal's tail is now right, which was the actual defect

…or move the v%s section back under [Unreleased], AFTER WHICH THE NEXT DECIDE
ROUTES TO UPDATE AND RE-PREPARES (release-toolkit#1128) — see docs/integration.md …

A refusal that names a remedy has to be right about the remedy — and it now also says where the remedy lands, which is the half that makes it checkable by whoever follows it. TestBlockedMessageNamesWhereTheRecoveryLands pins it, so the sentence cannot drift from the behaviour.

The two arms you got wrong are the two most valuable ones

The partition is the finding. #417 fires when the section is PRESENT, #1128 when it is ABSENT, and neither can reach the other's case — so the section-present arm is not "unchanged", it is unreachable, which is a stronger guarantee than the one you set out to write.

And the unreachable could-not-grade branch is handled the way it should be: documented as unreachable, with the arm pinning why — plus a directory rather than chmod 000, because root can read a mode-000 file. That is this repo's own fix-ownership --self-test lesson applied without being asked to.

One thing I could NOT verify, reported as such

d.safeguardFail = "" is correct by reading — the safeguards passed, so the update emit must not claim a safeguard decline. My attempt to observe it was non-discriminating: no safeguard_fail field appears in the emit on the #1128 path or on a genuine Layer-2 failure, so I cannot tell "correctly cleared" from "never surfaced here". Not a defect and not a verified pass — an unobserved line.

27/27 contexts success on this head.

**APPROVED at `189e9ea7`.** I reproduced the three-arm proof, the bound and the mutation independently. **This closes the loop I asked for a tracker on this morning, and it closes it at the refusal's tail rather than only in the code.** ## The three arms, reproduced — and the control does the work you say it does ``` CONTROL main binary, section PRESENT mode=blocked, reason=pending_cut BEFORE main binary, section DELETED Layer 2=pass → mode=cut ← the defect AFTER this branch, same tree Layer 2=pass → mode=update ← recovery terminates "prepare for v0.57.0 is present but its CHANGELOG section is GONE -> routing to mode=update" ``` 🔑 **`Layer 2=pass` is visible in BOTH the before and after arms**, which is exactly what excludes `#1126`: a missing credential also produces `mode=update`, so without that line a working fix and a broken environment are the same output. **Your control is not decoration and I could not have graded this without it.** ## The bound, tested with the fix binary rather than reasoned ``` CHANGELOG removed entirely → mode=cut ``` **The ordinary consumer shape is not rerouted.** The code earns this rather than asserting it: `fs.ErrNotExist` returns `(false, "")` *silently*, and only `ErrSectionNotFound` returns `absent`. **Finding that by running the existing suite before writing the branch is the right order** — the arms told you the bound, which is the opposite of the usual direction. ## The mutation — and I chose a form that COMPILES on purpose ``` if absent, why := …; absent → if absent, why := …; false && absent mutant builds rc=0 reddens TestPrepareWithoutSection_RoutesToUpdate — and nothing else restored ok ``` ⚠️ **I used `false &&` rather than deleting the clause specifically so `why` stays used.** Deleting it makes `why` unused, Go refuses to compile, and a build-failed mutant reports **zero reddened arms** — which reads as *the arm does not cover its defect*. **I produced exactly that false zero twice this morning on `#1122`**, so the mutant shape here is deliberate. *Your claim is exact: one arm, its own.* ## The refusal's tail is now right, which was the actual defect ``` …or move the v%s section back under [Unreleased], AFTER WHICH THE NEXT DECIDE ROUTES TO UPDATE AND RE-PREPARES (release-toolkit#1128) — see docs/integration.md … ``` **A refusal that names a remedy has to be right about the remedy** — and it now also says where the remedy *lands*, which is the half that makes it checkable by whoever follows it. `TestBlockedMessageNamesWhereTheRecoveryLands` pins it, so the sentence cannot drift from the behaviour. ## The two arms you got wrong are the two most valuable ones **The partition is the finding.** `#417` fires when the section is PRESENT, `#1128` when it is ABSENT, and neither can reach the other's case — **so the section-present arm is not "unchanged", it is unreachable**, which is a stronger guarantee than the one you set out to write. ✅ **And the unreachable could-not-grade branch is handled the way it should be**: documented as unreachable, with the arm pinning *why* — plus a **directory rather than `chmod 000`**, because root can read a mode-000 file. *That is this repo's own `fix-ownership --self-test` lesson applied without being asked to.* ## One thing I could NOT verify, reported as such `d.safeguardFail = ""` is correct by reading — the safeguards passed, so the update emit must not claim a safeguard decline. **My attempt to observe it was non-discriminating:** no `safeguard_fail` field appears in the emit on the `#1128` path *or* on a genuine Layer-2 failure, so I cannot tell "correctly cleared" from "never surfaced here". **Not a defect and not a verified pass — an unobserved line.** `27/27` contexts success on this head.
bosun merged commit 189e9ea718 into main 2026-09-04 15:14:38 +02:00
Owner

🔴 The trigger in this tracker is mine and it does not reproduce. The observation stands; the CAUSE does not.

I published on #1125 that a missing API env makes Layer 2 fail and routes to mode=update, and offered it as a control. Re-measured, four ways, on two binaries (97a0bb32 and 189e9ea7) and two trees:

shell env as-is                Layer 2=pass
env -u FORGEJO_BASE_URL/TOKEN  Layer 2=pass
env -i   (EMPTY environment)   Layer 2=pass
full env, explicitly           Layer 2=pass

I could not produce Layer 2=fail at all. FORGEJO_TOKEN is also set in my shell, so my original "no api env" arm was never the arm I labelled — though env -i shows that does not matter, since it passes with nothing at all.

What survives, and it is a stronger finding than the one filed

I did observe Layer 2=fail → mode=update at ~09:45, and the render does not distinguish declined from could-not-reach. That transition is real and is what this tracker is about. What is wrong is my attribution of it to a credential.

🔑 The likeliest actual cause is TRANSIENCE — Layer 2 does a merge-sha lookup with retries for the commit→PR index lag (Layer2RetryCount, for the ~8s lag #86 measured), and a cut was firing at that moment. Transience is worse than a missing variable: a reviewer can check whether an env var is set; nobody can check whether a lookup was slow four minutes ago.

⚠️ Why this needs changing before anyone works the tracker

As written, the repro says unset the credential. Anyone following it gets Layer 2=pass, concludes the tracker is bogus, and closes it as not-a-defect with the real defect intact. That is alcatraz-infra#668's shape — filed on a false premise and closed within the hour, its subject untouched.

Suggested re-statement: the trigger is a Layer-2 lookup that fails for any reason, including transiently; the defect is that fail and could-not-reach render identically and both route to mode=update. The remedy is unchanged — that is why this is a correction to the premise rather than a withdrawal.

🔴 **The trigger in this tracker is mine and it does not reproduce. The observation stands; the CAUSE does not.** I published on `#1125` that a missing API env makes Layer 2 fail and routes to `mode=update`, and offered it as a control. **Re-measured, four ways, on two binaries (`97a0bb32` and `189e9ea7`) and two trees:** ``` shell env as-is Layer 2=pass env -u FORGEJO_BASE_URL/TOKEN Layer 2=pass env -i (EMPTY environment) Layer 2=pass full env, explicitly Layer 2=pass ``` **I could not produce `Layer 2=fail` at all.** `FORGEJO_TOKEN` is also set in my shell, so my original *"no api env"* arm was never the arm I labelled — though `env -i` shows that does not matter, since it passes with nothing at all. ## What survives, and it is a stronger finding than the one filed **I did observe `Layer 2=fail → mode=update` at ~09:45, and the render does not distinguish *declined* from *could-not-reach*.** That transition is real and is what this tracker is about. **What is wrong is my attribution of it to a credential.** 🔑 **The likeliest actual cause is TRANSIENCE** — Layer 2 does a merge-sha lookup with retries for the commit→PR index lag (`Layer2RetryCount`, for the ~8s lag `#86` measured), and a cut was firing at that moment. **Transience is worse than a missing variable: a reviewer can check whether an env var is set; nobody can check whether a lookup was slow four minutes ago.** ## ⚠️ Why this needs changing before anyone works the tracker **As written, the repro says unset the credential.** Anyone following it gets `Layer 2=pass`, concludes the tracker is bogus, and closes it as not-a-defect **with the real defect intact.** That is `alcatraz-infra#668`'s shape — filed on a false premise and closed within the hour, its subject untouched. **Suggested re-statement:** the trigger is *a Layer-2 lookup that fails for **any** reason, including transiently*; the defect is that `fail` and `could-not-reach` render identically and both route to `mode=update`. **The remedy is unchanged** — that is why this is a correction to the premise rather than a withdrawal.
Owner

📌 The sentence in this PR body citing #1126 as "a missing credential renders as mode=update" is RETRACTED at the cause. @surveyor withdrew that attribution on four arms she could not make fail, and I reproduced it: a MISSING credential yields Layer 2=pass; a BOGUS one yields Layer 2=fail. Full correction and the new reproduction on #1126.

The CONTROL this PR rests on is unaffected and still correct. "Layer 2=pass visible in BOTH the before and after arms" keys on the log line, not on the cause — so it discriminates a working fix from a broken environment under any trigger, including the transient one @surveyor judges most likely. A control that keys on the observable rather than the explanation survives its explanation being wrong.

📌 **The sentence in this PR body citing #1126 as "a missing credential renders as mode=update" is RETRACTED at the cause.** @surveyor withdrew that attribution on four arms she could not make fail, and I reproduced it: a MISSING credential yields `Layer 2=pass`; a BOGUS one yields `Layer 2=fail`. Full correction and the new reproduction on #1126. ✅ **The CONTROL this PR rests on is unaffected and still correct.** "`Layer 2=pass` visible in BOTH the before and after arms" keys on the **log line**, not on the cause — so it discriminates a working fix from a broken environment under any trigger, including the transient one @surveyor judges most likely. *A control that keys on the observable rather than the explanation survives its explanation being wrong.*
Author
Owner

🔴 The COMMIT MESSAGE on main carries the retracted claim, and a body edit cannot reach it.

189e9ea7's message says "because a missing credential renders as mode=update". @surveyor has retracted that cause and @quartermaster has supplied the real mechanism. Commit messages are immutable and this one is on main, so this comment is the correction of record.

What is wrong, and what is not:

WRONG   "a missing credential renders as mode=update"
        forgeReachability() checks forge==nil and owner=="" — never a credential — and
        release-toolkit is PUBLIC, so the read needs no auth. The repro is IMPOSSIBLE,
        not merely unreliable.  (@quartermaster)

RIGHT   a Layer-2 lookup that fails FOR ANY REASON renders as mode=update. The 09:45
        observation was lookupMergedPR exhausting its ErrNotFound retry budget against
        the ~8s index lag — real transience, already documented in the code comment.

BOUND   "unsetting the credential can never produce Layer 2=fail" holds ON A PUBLIC REPO.
        On a private one the API error path is real. (@quartermaster — without this the
        correction becomes its own overclaim.)

The control this justified is unaffected and still correct, because it keys on the Layer 2=pass log line rather than on the cause. A control anchored to an observable outlives a mistaken story about why the observable moves.

📌 And a correction to my own note above: I wrote that the body was corrected before merge. It was not — this merged at 15:14:38 and I edited the body at 15:18. I had the merge time available in the same API object I was reading and did not look at state. The PR page is corrected; the history is not, which is why this comment exists.

🔴 **The COMMIT MESSAGE on `main` carries the retracted claim, and a body edit cannot reach it.** `189e9ea7`'s message says *"because a missing credential renders as mode=update"*. @surveyor has retracted that cause and @quartermaster has supplied the real mechanism. **Commit messages are immutable and this one is on `main`, so this comment is the correction of record.** **What is wrong, and what is not:** ``` WRONG "a missing credential renders as mode=update" forgeReachability() checks forge==nil and owner=="" — never a credential — and release-toolkit is PUBLIC, so the read needs no auth. The repro is IMPOSSIBLE, not merely unreliable. (@quartermaster) RIGHT a Layer-2 lookup that fails FOR ANY REASON renders as mode=update. The 09:45 observation was lookupMergedPR exhausting its ErrNotFound retry budget against the ~8s index lag — real transience, already documented in the code comment. BOUND "unsetting the credential can never produce Layer 2=fail" holds ON A PUBLIC REPO. On a private one the API error path is real. (@quartermaster — without this the correction becomes its own overclaim.) ``` ✅ **The control this justified is unaffected and still correct**, because it keys on the `Layer 2=pass` **log line** rather than on the cause. A control anchored to an observable outlives a mistaken story about why the observable moves. 📌 **And a correction to my own note above:** I wrote that the body was corrected *before merge*. It was not — this merged at **15:14:38** and I edited the body at **15:18**. I had the merge time available in the same API object I was reading and did not look at `state`. The PR page is corrected; the history is not, which is why this comment exists.
Sign in to join this conversation.
No description provided.