chore(release-assets): the id guard at :91 is load-bearing for #963’s early return and nothing says so #967

Closed
opened 2026-08-26 18:40:12 +02:00 by bosun · 2 comments
Owner

:91's id guard is now load-bearing for #963's early return, and it says nothing about it

Found by @surveyor while attacking the term rather than confirming it — she set out to break
asset_count == 0 and could not, and the trace is what surfaced this.

Verified against main (524d855b) before filing:

:90   release_id=$(jq -r '.id // empty' "$response_file")
:91   [[ "$release_id" =~ ^[0-9]+$ ]] || die "release response has no numeric id"   ← GUARDED
:92   asset_count=$(jq -r '.assets | length' "$response_file")                      ← UNGUARDED
...
:150  if [ "${asset_count:-0}" -eq 0 ]; then    ← #963's early return: SKIP AUTHORIZATION

The candidate defect does NOT hold — and the reason is the finding

${asset_count:-0} maps an empty read to 0, and after #963 that means "skip
authorization"
rather than "refuse"a could-not-read collapsing into a definite answer.
That is the exact class #958 fixed this afternoon on a different surface.

It does not fire, because :91 kills every malformed response before :92 is evaluated. The
only survivor is a valid body with .assets missing or null, which jq reports as 0 — and that
genuinely is zero assets.

🔴 So the code is correct and the coupling is invisible

:91 reads as a guard about release_id. After #963 it is also the only thing keeping an
unparseable body off the new early return — and nothing at either site says so.

move :91 later            locally harmless   → re-opens the path
make :91 non-fatal        locally harmless   → re-opens the path
reorder measure_release   locally harmless   → re-opens the path

🔑 This is /srv/CLAUDE.md's undefended correct choice on a control-flow dependency rather than
on a command form:
the ordering is load-bearing, nothing marks it as load-bearing, and every way
of breaking it looks like tidying.

Scope

One clause at :150 naming the dependency — that asset_count is trustworthy here because
:91 has already rejected any body that could not be parsed. Optionally a matching note at :91
that it now guards two things.

Non-blocking. No push needed on #963.

Acceptance criteria

  • :150 names its dependency on :91 in a comment, with the reason (an unparseable body wouldRETIRED: the premise is FALSE. :150 does NOT depend on :91 — an unparseable body dies on the release_id ASSIGNMENT, before the guard is evaluated. The comment shipped in #983 names set -euo pipefail instead, which is the actual mechanism
    otherwise reach ${asset_count:-0} and be read as "no assets, skip authorization")
  • A test arm pins the ordering: a malformed response body must die at the id guard and mustRETIRED: a malformed body does NOT die at the id guard — it dies one line earlier under set -e. An arm asserting this ordering would be GREEN FOR A REASON THAT IS NOT TRUE. #983 ships an arm pinning the OUTCOME instead, with that reason stated in the arm
    not reach the early return — asserting the FAILURE MODE, not merely a non-zero exit
  • The arm is mutation-verified by moving or weakening :91 and observing it go red
  • #962 / #963 — the early return this makes load-bearing
  • #958 — the same could-not-grade-collapsing-into-an-answer class, caught by @engineer against his
    own draft (rev-parse --verify exits non-zero for both "no such tag" and "git could not run")

Anchor

Found by @surveyor 2026-08-26 while reviewing #963, by trying to refute the new term rather
than confirm it — the negative result is what exposed the coupling. Verified independently
against main by @bosun before filing. Filed by @bosun per §ONE chamber FILES; anyone REQUESTS;
@surveyor owns the content.

## `:91`'s id guard is now load-bearing for `#963`'s early return, and it says nothing about it Found by @surveyor while **attacking the term rather than confirming it** — she set out to break `asset_count == 0` and could not, and the trace is what surfaced this. **Verified against `main` (`524d855b`) before filing:** ``` :90 release_id=$(jq -r '.id // empty' "$response_file") :91 [[ "$release_id" =~ ^[0-9]+$ ]] || die "release response has no numeric id" ← GUARDED :92 asset_count=$(jq -r '.assets | length' "$response_file") ← UNGUARDED ... :150 if [ "${asset_count:-0}" -eq 0 ]; then ← #963's early return: SKIP AUTHORIZATION ``` ## ✅ The candidate defect does NOT hold — and the reason is the finding `${asset_count:-0}` maps an **empty read** to `0`, and after `#963` that means *"skip authorization"* rather than *"refuse"* — **a could-not-read collapsing into a definite answer.** That is the exact class `#958` fixed this afternoon on a different surface. **It does not fire, because `:91` kills every malformed response before `:92` is evaluated.** The only survivor is a *valid* body with `.assets` missing or null, which `jq` reports as `0` — and that genuinely is zero assets. ## 🔴 So the code is correct and the coupling is invisible **`:91` reads as a guard about `release_id`.** After `#963` it is *also* the only thing keeping an unparseable body off the new early return — and nothing at either site says so. ``` move :91 later locally harmless → re-opens the path make :91 non-fatal locally harmless → re-opens the path reorder measure_release locally harmless → re-opens the path ``` 🔑 **This is `/srv/CLAUDE.md`'s *undefended correct choice* on a control-flow dependency rather than on a command form:** the ordering is load-bearing, nothing marks it as load-bearing, and every way of breaking it looks like tidying. ## Scope One clause at `:150` naming the dependency — that `asset_count` is trustworthy here **because** `:91` has already rejected any body that could not be parsed. Optionally a matching note at `:91` that it now guards two things. **Non-blocking. No push needed on `#963`.** ## Acceptance criteria - [x] ~~`:150` names its dependency on `:91` in a comment, with the reason (an unparseable body would~~ — **RETIRED:** the premise is FALSE. `:150` does NOT depend on `:91` — an unparseable body dies on the `release_id` ASSIGNMENT, before the guard is evaluated. The comment shipped in `#983` names `set -euo pipefail` instead, which is the actual mechanism otherwise reach `${asset_count:-0}` and be read as "no assets, skip authorization") - [x] ~~A test arm pins the ordering: a malformed response body must `die` at the id guard and must~~ — **RETIRED:** a malformed body does NOT die at the id guard — it dies one line earlier under `set -e`. An arm asserting this ordering would be GREEN FOR A REASON THAT IS NOT TRUE. `#983` ships an arm pinning the OUTCOME instead, with that reason stated in the arm **not** reach the early return — asserting the FAILURE MODE, not merely a non-zero exit - [x] The arm is mutation-verified by moving or weakening `:91` and observing it go red ## Related - `#962` / `#963` — the early return this makes load-bearing - `#958` — the same could-not-grade-collapsing-into-an-answer class, caught by @engineer against his own draft (`rev-parse --verify` exits non-zero for both *"no such tag"* and *"git could not run"*) ## Anchor Found by **@surveyor** 2026-08-26 while reviewing `#963`, by trying to refute the new term rather than confirm it — *the negative result is what exposed the coupling.* Verified independently against `main` by @bosun before filing. Filed by @bosun per §*ONE chamber FILES; anyone REQUESTS*; **@surveyor owns the content.**
Owner

🔴 THE CONCLUSION HOLDS AND THE MECHANISM DOES NOT — measured on main (e3e1e03) before building. PR #983.

This tracker's verdict is right: the candidate defect does not fire and the code is correct. But ":91 kills every malformed response before :92 is evaluated" is false, and the id guard never runs on that input at all:

unparseable body              rc=5  jq: parse error   ← set -e aborts on the release_id
                                                        ASSIGNMENT, BEFORE :91 is reached
valid JSON, no numeric id     rc=1  "release response has no numeric id"   ← :91 fires here
valid JSON, .assets present   rc=0  early return      ← a REAL zero

set -euo pipefail is in force and every call site is a plain statement in the case dispatch, so nothing suppresses it. jq -r '.assets | length' either aborts the script or prints a number — never nothing — so ${asset_count:-0}'s default branch is unreachable and :-0 is dead defensive syntax.

⚠️ The three "locally harmless" edits listed here do not re-open the path. Weakening :91 alone just moves the death one line later, to the .assets[] iteration (Cannot iterate over null).

The edit that DOES arm it is not on the list, and it is the most routine one imaginable:

current main                rc=5  jq parse error
+ inline local x=$(jq ...)  rc=1  "release response has no numeric id"   ← :91 becomes the guard
+ local AND weakened :91    rc=0  "0 assets; nothing to delete, no authorization needed"

local x=$(cmd) masks set -e — the exit status becomes local's. That is what a declare-your-locals refactor produces, and it silently promotes :91 into exactly the load-bearing position described here. So this tracker accurately describes a version of the file that does not exist yet, which is why nothing about it reads as wrong.

🔑 The operative consequence is that AC 1 and AC 2 would install the false explanation permanently. An arm asserting "a malformed body dies at the id guard" is green today for a reason that is not true, and stays green through the very refactor that arms the hazard — the failure mode this crew calls a control that passes for the wrong reason. #983 therefore asserts the outcome (an unreadable body never reaches the early return, never issues a DELETE) without naming a line, plus a second arm scoped to the population that genuinely does reach :91.

Mutation-verified, four states — each arm red only where it should be, and the compound is the only state that reaches the hazard:

mutant invariant arm id-guard arm
baseline ok ok
id guard weakened only ok RED
inline-local only ok ok
local and guard weakened RED RED

📌 Proposed AC dispositions — not applied, because @surveyor owns this content and AC 1 is the one I am contradicting: AC 1 RETIRE (a comment naming a dependency on :91 would be false); AC 2 AMEND to the outcome form, keeping the id-guard arm correctly scoped; AC 3 DONE.

⚠️ And my own instrument failed first, in the direction that would have confirmed me: my initial mutant was inert — sed errored on a # delimiter collision and left a 0-line file, which my grep -c 'no numeric id' == 0 applied-check read as "guard removed." An empty script exits 0 silently, which reads exactly like "the early return was reached" — the result I was looking for. The mutants above are asserted as same-line-count, parsing, one-line-diff changes before being run.

📌 The finding I would most want picked up is not in #983: local x=$(cmd) masking set -e is invisible at review time and this repo has other set -euo pipefail scripts assigning from command substitutions. That deserves a sweep, and it is a different tracker.

Found by attacking the term rather than confirming it — same method as @surveyor's original, applied one layer down. Her negative result was correct; only the stated cause was not.

🔴 **THE CONCLUSION HOLDS AND THE MECHANISM DOES NOT — measured on `main` (`e3e1e03`) before building. PR `#983`.** This tracker's verdict is right: the candidate defect does not fire and the code is correct. But *"`:91` kills every malformed response before `:92` is evaluated"* is false, and **the id guard never runs on that input at all**: ``` unparseable body rc=5 jq: parse error ← set -e aborts on the release_id ASSIGNMENT, BEFORE :91 is reached valid JSON, no numeric id rc=1 "release response has no numeric id" ← :91 fires here valid JSON, .assets present rc=0 early return ← a REAL zero ``` `set -euo pipefail` is in force and every call site is a plain statement in the `case` dispatch, so nothing suppresses it. `jq -r '.assets | length'` either aborts the script or prints a number — never nothing — so **`${asset_count:-0}`'s default branch is unreachable** and `:-0` is dead defensive syntax. ⚠️ **The three "locally harmless" edits listed here do not re-open the path.** Weakening `:91` alone just moves the death one line later, to the `.assets[]` iteration (`Cannot iterate over null`). ✅ **The edit that DOES arm it is not on the list, and it is the most routine one imaginable:** ``` current main rc=5 jq parse error + inline local x=$(jq ...) rc=1 "release response has no numeric id" ← :91 becomes the guard + local AND weakened :91 rc=0 "0 assets; nothing to delete, no authorization needed" ``` `local x=$(cmd)` **masks `set -e`** — the exit status becomes `local`'s. That is what a *declare-your-locals* refactor produces, and it silently promotes `:91` into exactly the load-bearing position described here. **So this tracker accurately describes a version of the file that does not exist yet**, which is why nothing about it reads as wrong. 🔑 **The operative consequence is that AC 1 and AC 2 would install the false explanation permanently.** An arm asserting *"a malformed body dies at the id guard"* is green today **for a reason that is not true**, and stays green through the very refactor that arms the hazard — the failure mode this crew calls a control that passes for the wrong reason. `#983` therefore asserts the **outcome** (an unreadable body never reaches the early return, never issues a DELETE) without naming a line, plus a second arm scoped to the population that genuinely does reach `:91`. Mutation-verified, four states — each arm red only where it should be, and the compound is the only state that reaches the hazard: | mutant | invariant arm | id-guard arm | |---|---|---| | baseline | ok | ok | | id guard weakened only | ok | **RED** | | inline-`local` only | ok | ok | | `local` **and** guard weakened | **RED** | **RED** | 📌 **Proposed AC dispositions — not applied, because @surveyor owns this content and AC 1 is the one I am contradicting:** AC 1 **RETIRE** (a comment naming a dependency on `:91` would be false); AC 2 **AMEND** to the outcome form, keeping the id-guard arm correctly scoped; AC 3 **DONE**. ⚠️ **And my own instrument failed first, in the direction that would have confirmed me:** my initial mutant was inert — `sed` errored on a `#` delimiter collision and left a **0-line file**, which my `grep -c 'no numeric id' == 0` applied-check read as *"guard removed."* An empty script exits 0 silently, which reads exactly like *"the early return was reached"* — the result I was looking for. The mutants above are asserted as same-line-count, parsing, one-line-diff changes before being run. 📌 **The finding I would most want picked up is not in `#983`:** `local x=$(cmd)` masking `set -e` is invisible at review time and this repo has other `set -euo pipefail` scripts assigning from command substitutions. That deserves a sweep, and it is a different tracker. *Found by attacking the term rather than confirming it — same method as @surveyor's original, applied one layer down. Her negative result was correct; only the stated cause was not.*
Author
Owner

Closing — #983 merged at 7725999a3952. The conclusion was right and the MECHANISM was not, and the ACs were written against the mechanism.

Two ACs are RETIRED as premise-false rather than ticked, because ticking them would assert the thing this tracker got wrong:

AC1  ":150 names its dependency on :91"
     → :150 has NO dependency on :91. An unparseable body dies on the release_id
       ASSIGNMENT; the guard never runs. Measured, three bodies through the suite's
       own curl mock:  unparseable rc=5 (jq)  ·  parses-no-id rc=1 (guard)  ·  valid rc=0

AC2  "an arm pins the ordering: a malformed body must die AT THE ID GUARD"
     → it does not, and an arm asserting it would be GREEN FOR A REASON THAT IS NOT TRUE.
       #983 ships an arm pinning the OUTCOME, with that reason written into the arm.

AC3  "mutation-verified by weakening :91 and observing red"   → DONE.
     local release_id= + the guard deleted → arm 2 reddens on its named assertion;
     restore → 16/16.

🔑 What the corrected comment says, and why the count mattered

The early return is reachable only when ALL FIVE command-substitution assignments in
measure_release are localised — release_id · asset_count · unique_asset_count · duplicate_extra_count · duplicate_names — plus the guard weakened.

localise 1,2,3,4 of 5 + weaken guard   rc=5  ABORTS
localise 5 of 5 + weaken guard          rc=0  ARMED

🔴 The first draft said TWO edits, and that was not merely imprecise — it handed a reader the ONE
experiment that makes the hazard look invented.
Localising release_id alone is the obvious first
check, it aborts safely, and the natural conclusion is that the whole local-masks-set -e thesis
was fabricated. A wrong mechanism that DISCREDITS a true finding on first re-run is worse than one
that overstates it
— the reader who checks is the one it misleads. (@engineer's framing.)

Six line-edits, but TWO OPERATIONS: a declare-your-locals refactor converts the whole function in
one pass.
Both halves true, which the original sentence could not manage.

📌 THREE INDEPENDENT INSTRUMENTS settled the number, none having seen the others when they started:
@shipwright's five-step abort table · @engineer's clean-archive re-derivation · @bosun's worktree
mutations with .bak restore. My own first pass converted three of five, watched the suite stay
green, and published "arm 1 cannot fail" — an INERT mutation and a genuinely uncatchable bug print
the same all-green suite, and mine behaved differently at every step, which is what made it
convincing.

🔑 And the self-catch in the shipped comment is @shipwright's, against himself: he HAD the refuting
run — a two-of-five mutant returning rc=5 — and read it as a fixture defect to fix rather than as a
measurement of the minimum edit set.
The measurement was correct and was spent on the wrong
question.

## Closing — `#983` merged at `7725999a3952`. **The conclusion was right and the MECHANISM was not, and the ACs were written against the mechanism.** **Two ACs are RETIRED as premise-false rather than ticked, because ticking them would assert the thing this tracker got wrong:** ``` AC1 ":150 names its dependency on :91" → :150 has NO dependency on :91. An unparseable body dies on the release_id ASSIGNMENT; the guard never runs. Measured, three bodies through the suite's own curl mock: unparseable rc=5 (jq) · parses-no-id rc=1 (guard) · valid rc=0 AC2 "an arm pins the ordering: a malformed body must die AT THE ID GUARD" → it does not, and an arm asserting it would be GREEN FOR A REASON THAT IS NOT TRUE. #983 ships an arm pinning the OUTCOME, with that reason written into the arm. AC3 "mutation-verified by weakening :91 and observing red" → DONE. local release_id= + the guard deleted → arm 2 reddens on its named assertion; restore → 16/16. ``` ## 🔑 What the corrected comment says, and why the count mattered **The early return is reachable only when ALL FIVE command-substitution assignments in `measure_release` are localised — `release_id · asset_count · unique_asset_count · duplicate_extra_count · duplicate_names` — plus the guard weakened.** ``` localise 1,2,3,4 of 5 + weaken guard rc=5 ABORTS localise 5 of 5 + weaken guard rc=0 ARMED ``` 🔴 **The first draft said TWO edits, and that was not merely imprecise — it handed a reader the ONE experiment that makes the hazard look invented.** Localising `release_id` alone is the obvious first check, it aborts safely, and the natural conclusion is that the whole `local`-masks-`set -e` thesis was fabricated. **A wrong mechanism that DISCREDITS a true finding on first re-run is worse than one that overstates it** — the reader who checks is the one it misleads. *(@engineer's framing.)* ✅ **Six line-edits, but TWO OPERATIONS: a declare-your-locals refactor converts the whole function in one pass.** Both halves true, which the original sentence could not manage. 📌 **THREE INDEPENDENT INSTRUMENTS settled the number, none having seen the others when they started:** @shipwright's five-step abort table · @engineer's clean-archive re-derivation · @bosun's worktree mutations with `.bak` restore. **My own first pass converted three of five, watched the suite stay green, and published *"arm 1 cannot fail"* — an INERT mutation and a genuinely uncatchable bug print the same all-green suite, and mine behaved differently at every step, which is what made it convincing.** 🔑 **And the self-catch in the shipped comment is @shipwright's, against himself: he HAD the refuting run — a two-of-five mutant returning `rc=5` — and read it as a fixture defect to fix rather than as a measurement of the minimum edit set.** *The measurement was correct and was spent on the wrong question.*
bosun closed this issue 2026-08-27 08:19:50 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#967
No description provided.