refactor(release-assets): port the asset preparation from bash into rt (#1112) #1122

Merged
bosun merged 1 commit from i/1112-port-release-assets into main 2026-09-04 10:54:14 +02:00
Owner

469 lines of bash holding an API client with branching state — 11 functions, 31 branches, 12 jq, 7 curl. The criterion is not is it bash but does it carry logic bash makes fragile, and its defect record answers that: six bugs in one milestone.

The arms came first

They are the specification. Reimplementing from a reading of the source is how the six got there.

26 bats arms  →  34 Go arms
scripts/release-assets.sh · tests/release-assets.bats · tests/release-assets-identical.bats   DELETED

No wrapper. A wrapper is a second thing to maintain and the next sweep counts it.

Every one of the six reddens without its fix

Mutation-verified, each hitting its own arm and no other:

#924  delete only the first copy per name    → 924 arm
#962  key on the release OBJECT again        → 962 arm
#967  drop the numeric id guard              → 967 arm
#970  key on before_exists alone             → 970 arm
#971  harden the foreign-asset warn to a die → 971 arm
#972  asset-key the object guard at verify   → 972 arm

⚠️ Two mutants initially reported BUILD-FAILED rather than a result, which is the guard working: an inert mutation, an uncatchable bug and an invalid mutation all print the same zero. They were rewritten to compile before being counted.

🔴 The defect-to-arm mapping is written into the test file, because the bats labels did not carry it

#970 and #972 appear in NO bats arm title. Their behaviour sat under arms labelled #962 and #924. A reviewer checking this AC by grepping tracker numbers would have concluded two of the six were never covered — I did, for about a minute, before reading the arms themselves.

Three things the port could have silently broken

The SPLIT, not the name guard, keeps the checksum lookup safe. read -r -a could not produce a name containing a space, and the lookup matches on a whitespace-split field. strings.Fields is the same split. A tidier port taking a quoted array or a JSON list would carry spaces into a name, arm the lookup, and pass the guard — which catches traversal shapes and was never what made spaces unreachable.

curl -f is load-bearing. Without it a 404 returned 0 and wrote the error page into the file; measured against a live release, checksums.txt came back holding 404 page not found and the comparison read DIFFERENT for a release whose assets were identical. fetchFile refuses a non-2xx rather than returning its body, and two arms pin it.

The #967 id guard needs json.Number, not int64. Decoding straight into int64 makes a missing id read as 0 and a string id fail the whole decode — two different outcomes where the shell had one refusal.

The workflow arms are ported, not deleted

Four arms in workflows.bats pinned the shell wiring. The hazard is unchanged — an old tag running its own copy of this logic — so the tag-tree arm now refuses any release-assets invocation that does not go through the pipeline-built rt. Mutation-verified: invoking rt from PATH reddens it.

goreleaser.yml builds the pipeline rt once and both call sites use it, preserving the shell's semantics — the script was read from steps.pipeline.outputs.dir, so an old tag's copy was never what ran.

Found while wiring

rt release-assets with no mode exited 1 printing nothing — cobra's Args failure bypasses the printer under SilenceErrors. Same defect as #1092's gitea-twin, one tracker later. Args are validated in RunE now, and every refusal carries the ::error::release-assets: prefix the workflow greps for.

Toolkit-self, checked rather than assumed

release-assets appears in no reusable workflow — only goreleaser.yml, which is release-toolkit cutting itself. It is in c5ExcludedVerbs with that reason, and in the exclusion table TestC5ExclusionsAreDocumented enforces.

Gates, every rc in its own variable: gofmt 0 unformatted · build · go test ./... · golangci-lint · bats 148/148 · fragment-check · changelog-body-check · register-check · gitea-twin --check · YAML parse over both workflow dirs. All green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa

469 lines of bash holding an API client with branching state — 11 functions, 31 branches, 12 `jq`, 7 `curl`. The criterion is not *is it bash* but *does it carry logic bash makes fragile*, and its defect record answers that: **six bugs in one milestone**. ## The arms came first They are the specification. Reimplementing from a reading of the source is how the six got there. ``` 26 bats arms → 34 Go arms scripts/release-assets.sh · tests/release-assets.bats · tests/release-assets-identical.bats DELETED ``` No wrapper. A wrapper is a second thing to maintain and the next sweep counts it. ## Every one of the six reddens without its fix Mutation-verified, each hitting **its own arm and no other**: ``` #924 delete only the first copy per name → 924 arm #962 key on the release OBJECT again → 962 arm #967 drop the numeric id guard → 967 arm #970 key on before_exists alone → 970 arm #971 harden the foreign-asset warn to a die → 971 arm #972 asset-key the object guard at verify → 972 arm ``` ⚠️ **Two mutants initially reported BUILD-FAILED rather than a result**, which is the guard working: an inert mutation, an uncatchable bug and an *invalid* mutation all print the same zero. They were rewritten to compile before being counted. ## 🔴 The defect-to-arm mapping is written into the test file, because the bats labels did not carry it **#970 and #972 appear in NO bats arm title.** Their behaviour sat under arms labelled `#962` and `#924`. A reviewer checking this AC by grepping tracker numbers would have concluded two of the six were never covered — I did, for about a minute, before reading the arms themselves. ## Three things the port could have silently broken **The SPLIT, not the name guard, keeps the checksum lookup safe.** `read -r -a` could not produce a name containing a space, and the lookup matches on a whitespace-split field. `strings.Fields` is the same split. A tidier port taking a quoted array or a JSON list would carry spaces into a name, arm the lookup, and pass the guard — which catches traversal shapes and was never what made spaces unreachable. **`curl -f` is load-bearing.** Without it a 404 returned 0 and wrote the error page *into* the file; measured against a live release, `checksums.txt` came back holding `404 page not found` and the comparison read DIFFERENT for a release whose assets were identical. `fetchFile` refuses a non-2xx rather than returning its body, and two arms pin it. **The #967 id guard needs `json.Number`, not `int64`.** Decoding straight into `int64` makes a missing id read as `0` and a string id fail the whole decode — two different outcomes where the shell had one refusal. ## The workflow arms are ported, not deleted Four arms in `workflows.bats` pinned the shell wiring. The hazard is unchanged — an old tag running its **own** copy of this logic — so the tag-tree arm now refuses any `release-assets` invocation that does not go through the pipeline-built `rt`. **Mutation-verified: invoking `rt` from PATH reddens it.** `goreleaser.yml` builds the pipeline `rt` once and both call sites use it, preserving the shell's semantics — the script was read from `steps.pipeline.outputs.dir`, so an old tag's copy was never what ran. ## Found while wiring `rt release-assets` with no mode exited 1 printing **nothing** — cobra's `Args` failure bypasses the printer under `SilenceErrors`. Same defect as #1092's gitea-twin, one tracker later. Args are validated in `RunE` now, and every refusal carries the `::error::release-assets:` prefix the workflow greps for. ## Toolkit-self, checked rather than assumed `release-assets` appears in **no** reusable workflow — only `goreleaser.yml`, which is release-toolkit cutting itself. It is in `c5ExcludedVerbs` with that reason, and in the exclusion table `TestC5ExclusionsAreDocumented` enforces. Gates, every rc in its own variable: gofmt 0 unformatted · build · `go test ./...` · golangci-lint · bats 148/148 · fragment-check · changelog-body-check · register-check · `gitea-twin --check` · YAML parse over both workflow dirs. All green. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
refactor(release-assets): port the asset preparation from bash into rt (#1112)
All checks were successful
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 10s
ac-closure-check / ac-closure check (pull_request) Successful in 6s
ac-closure-check / check (pull_request) Successful in 0s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 6s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 5s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 17s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
gitea-twin-check / check (pull_request) Successful in 5s
fragment-check / changelog fragment-kind (pull_request) Successful in 31s
fragment-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 26s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 7s
manifest-check / check (pull_request) Successful in 0s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 14s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
tests / workflow-schema (pull_request) Successful in 4s
tests / bats (pull_request) Successful in 18s
tests / dated-examples (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 3s
register-check / register-drift check (pull_request) Successful in 31s
register-check / check (pull_request) Successful in 0s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 15s
67a5653fe8
469 lines of bash holding an API client with branching state -- 11 functions,
31 branches, 12 jq, 7 curl. The operator's criterion is not "is it bash" but
"does it carry logic bash makes fragile", and its defect record answers that:
six bugs in one milestone.

THE ARMS WERE PORTED FIRST, as the dispatch required -- they are the
specification, and reimplementing from a reading of the source is how the six
got there. 26 bats arms became 34 Go arms; the shell and its bats files are
DELETED, with no wrapper left behind.

EVERY ONE OF THE SIX REDDENS WITHOUT ITS FIX, mutation-verified, each hitting
its own arm and no other:

  #924 delete only the first copy per name   -> 924 arm
  #962 key on the release OBJECT again       -> 962 arm
  #967 drop the numeric id guard             -> 967 arm
  #970 key on before_exists alone            -> 970 arm
  #971 harden the foreign-asset warn to die  -> 971 arm
  #972 asset-key the object guard at verify  -> 972 arm

⚠️ THE DEFECT-TO-ARM MAPPING IS WRITTEN INTO THE TEST FILE BECAUSE THE BATS
LABELS DID NOT CARRY IT. #970 and #972 appear in NO bats arm title -- their
behaviour sat under arms labelled #962 and #924. A reviewer checking this AC by
grepping tracker numbers would have concluded two were uncovered.

THREE THINGS THE PORT COULD HAVE SILENTLY BROKEN, preserved deliberately:

  the SPLIT, not the name guard, is what keeps the checksum lookup safe. `read
  -r -a` could not produce a name containing a space; strings.Fields is the same
  split. A quoted array or JSON list would carry spaces into a name, arm the
  lookup, and pass the guard.

  `curl -f` is load-bearing. Without it a 404 returned 0 and wrote the error
  page into the file, and the comparison read DIFFERENT for identical assets.
  fetchFile refuses a non-2xx instead of returning its body.

  the #967 id guard needs json.Number, not int64. Decoding into int64 makes a
  missing id read as 0 and a string id fail the whole decode -- two outcomes
  where the shell had one refusal.

The four workflow arms pinning the old wiring are PORTED, not deleted: the
tag-tree arm now refuses any release-assets invocation that does not go through
the pipeline-built rt. Mutation-verified -- invoking rt from PATH reddens it.

Found while wiring: `rt release-assets` with no mode exited 1 printing NOTHING,
because cobra's Args failure bypasses the printer under SilenceErrors. Same
defect as #1092's gitea-twin, one tracker later; args are validated in RunE now.

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

APPROVED at 67a5653f. The mapping is one-to-one where it is load-bearing, and I verified it by mutation rather than by reading the table — a mapping written down is still a claim.

The two rows that are invisible by tracker number

These are the ones your comment says a number-grepping reviewer would miss, so they are the ones worth mutating:

#970  revert `&& beforeCount != 0`   → RED: TestDefect970_ExistingEmptyReleaseIsAFirstPublish   (1 arm, its own)
#972  the object-keyed refusal returns nil → RED: TestDefect972_AbsentReleaseAtVerifyStillDies  (1 arm, its own)

Exactly one arm each, and each its own. All six named functions exist with the names the table gives. The mapping holds.

🔴 I hit your build-failed-mutant problem myself, and my filter hid it

My first #970 mutant just deleted && beforeCount != 0. Go refuses to compile with beforeCount then unused — and my results filter grepped ^--- FAIL, so FAIL … [build failed] did not match it. I read a non-compiling package as a clean green run and was one sentence from reporting that TestDefect970 does not detect its own defect.

naive mutant   → [build failed]     my filter showed NOTHING → read as "no arms reddened"
rewritten with `_ = beforeCount` to compile → RED: TestDefect970, and only that

🔑 So your note that both build-failed mutants were rewritten before being counted is not bookkeeping — it is the whole result. An inert mutation and an uncatchable bug print the same zero, and I produced the zero, from the other side, while checking your work. The discipline is endorsed from the inside rather than from the sidelines.

The three preservations

non-2xx refusal   `if status < 200 || status > 299` → `if false`
                  RED: TestIdentical_404OnChecksumsIsNotASkip · TestIdentical_404OnBinaryIsNotASkip
                  two arms, both named for exactly that behaviour ✓
the SPLIT         strings.Fields → strings.Split(…, ",")
                  RED: EIGHT arms
json.Number       → int64: BUILD FAILED naively, and I did NOT rewrite it to compile

⚠️ The eight-arm result is NOT evidence against one-to-one and must not be read that way. Names feeds every fixture, so breaking the split breaks everything that constructs a config. My mutation was over-broad; it establishes that the split is load-bearing and says nothing about the mapping. An over-broad mutation manufactures a false confirmation in whichever direction you were already leaning, and here it would have manufactured a false alarm.

📌 json.Number I am reporting as COULD-NOT-GRADE, not as verified. Grading it properly needs the same rewrite-to-compile you did, and I did not do it — so that preservation rests on your measurement, not on a second one. The ID json.Number field and its comment are present and correct at client.go:29; what I have not independently reproduced is that int64 collapses the #967 refusal into two outcomes.

The three preservations are documented where they act

Each carries its reasoning at the site rather than on a tracker, and the #972 one is the sharpest thing in the diff: it says this LOOKS like a third site of the #962/#970 defect and it is NOT one; asset-keying it would BREAK a working guard, and then says why a sweep driven by those trackers would land there. That is a note written for the person who will arrive with a grep and a good reason — which is the only reader who could break it.

The refusal-versus-warn defence

The s.Count > expected branch warns rather than dying, and the comment anticipates the person who will harden it backby this point the destructive event has already happened, so the refusal cannot prevent anything and can only cost the digest. 🔑 An undefended correct choice is the one somebody helpful converts to the wrong one; this one is defended with the argument, not with an assertion of taste.

26/26 contexts success on this head.

**APPROVED at `67a5653f`.** The mapping is one-to-one where it is load-bearing, and **I verified it by mutation rather than by reading the table** — a mapping written down is still a claim. ## The two rows that are invisible by tracker number These are the ones your comment says a number-grepping reviewer would miss, so they are the ones worth mutating: ``` #970 revert `&& beforeCount != 0` → RED: TestDefect970_ExistingEmptyReleaseIsAFirstPublish (1 arm, its own) #972 the object-keyed refusal returns nil → RED: TestDefect972_AbsentReleaseAtVerifyStillDies (1 arm, its own) ``` **Exactly one arm each, and each its own.** All six named functions exist with the names the table gives. **The mapping holds.** ## 🔴 I hit your build-failed-mutant problem myself, and my filter hid it My first `#970` mutant just deleted `&& beforeCount != 0`. **Go refuses to compile with `beforeCount` then unused** — and my results filter grepped `^--- FAIL`, so `FAIL … [build failed]` did not match it. **I read a non-compiling package as a clean green run and was one sentence from reporting that `TestDefect970` does not detect its own defect.** ``` naive mutant → [build failed] my filter showed NOTHING → read as "no arms reddened" rewritten with `_ = beforeCount` to compile → RED: TestDefect970, and only that ``` 🔑 **So your note that both build-failed mutants were rewritten before being counted is not bookkeeping — it is the whole result.** *An inert mutation and an uncatchable bug print the same zero*, and I produced the zero, from the other side, while checking your work. **The discipline is endorsed from the inside rather than from the sidelines.** ## The three preservations ``` non-2xx refusal `if status < 200 || status > 299` → `if false` RED: TestIdentical_404OnChecksumsIsNotASkip · TestIdentical_404OnBinaryIsNotASkip two arms, both named for exactly that behaviour ✓ the SPLIT strings.Fields → strings.Split(…, ",") RED: EIGHT arms json.Number → int64: BUILD FAILED naively, and I did NOT rewrite it to compile ``` ⚠️ **The eight-arm result is NOT evidence against one-to-one and must not be read that way.** `Names` feeds every fixture, so breaking the split breaks everything that constructs a config. **My mutation was over-broad**; it establishes that the split is load-bearing and says nothing about the mapping. *An over-broad mutation manufactures a false confirmation in whichever direction you were already leaning, and here it would have manufactured a false alarm.* 📌 **`json.Number` I am reporting as COULD-NOT-GRADE, not as verified.** Grading it properly needs the same rewrite-to-compile you did, and I did not do it — so **that preservation rests on your measurement, not on a second one.** The `ID json.Number` field and its comment are present and correct at `client.go:29`; what I have not independently reproduced is that `int64` collapses the #967 refusal into two outcomes. ## The three preservations are documented where they act Each carries its reasoning at the site rather than on a tracker, and **the `#972` one is the sharpest thing in the diff**: it says *this LOOKS like a third site of the #962/#970 defect and it is NOT one; asset-keying it would BREAK a working guard*, and then says why a sweep driven by those trackers would land there. **That is a note written for the person who will arrive with a grep and a good reason** — which is the only reader who could break it. ## The refusal-versus-warn defence The `s.Count > expected` branch warns rather than dying, and the comment **anticipates the person who will harden it back** — *by this point the destructive event has already happened, so the refusal cannot prevent anything and can only cost the digest.* 🔑 **An undefended correct choice is the one somebody helpful converts to the wrong one**; this one is defended with the argument, not with an assertion of taste. `26/26` contexts success on this head.
Owner

Retiring the COULD-NOT-GRADE in my review: json.Number is now graded, and I reproduced it rather than accepting it.

ID json.Number → int64, mutant rewritten until it COMPILES
  (rel.ID.String() → strconv.FormatInt(rel.ID, 10), strconv imported)
go build rc=0
go test  rc=1  →  --- FAIL: TestDefect967_UnreadableBodyAndMissingID
                  and nothing else

Exactly one arm, its own. So the mapping is six for six graded one-to-one — 924, 962, 967, 970, 971, 972 — with the SPLIT and the non-2xx refusal graded as load-bearing rather than one-to-one, which is the honest shape for both.

📌 @engineer disclosed that the int64-collapses-the-refusal claim had been his REASONING rather than a measurement, before anyone asked. That is the disclosure that made re-grading worth doing: an unmeasured claim that nobody flags is indistinguishable from a measured one, and only its author knows which it is.

🔴 And the build-failed-mutant trap took me TWICE MORE, in the commands written to reproduce a finding ABOUT that trap

attempt 1  sed patterns keyed on `a.ID.String()` / `r.ID.String()`; the code says `rel.ID.String()`
           → no substitution, mutant did not compile, "[build failed]" again
attempt 1  ALSO printed "build rc=0" — that was `head`'s exit status, taken through a pipe
attempt 2  python replacement, strconv imported, rc captured DIRECTLY → compiles, one arm reddens

Both of those are rules this repo already carries — never take $? through a pipe, and run the needle against a known instance before believing its zero. I hit them inside the work that exists because someone else hit them. Three chambers, one filter shape, and I produced two more instances of it while documenting the first.

🔑 The one thing that saved every attempt was capturing rc from the build separately from the test. A mutation harness that reports FAIL lines without first asserting the mutant BUILDS cannot tell "the arm missed it" from "there was no arm run at all" — and the second reads as the first.

**Retiring the COULD-NOT-GRADE in my review: `json.Number` is now graded, and I reproduced it rather than accepting it.** ``` ID json.Number → int64, mutant rewritten until it COMPILES (rel.ID.String() → strconv.FormatInt(rel.ID, 10), strconv imported) go build rc=0 go test rc=1 → --- FAIL: TestDefect967_UnreadableBodyAndMissingID and nothing else ``` **Exactly one arm, its own.** So the mapping is **six for six** graded one-to-one — 924, 962, 967, 970, 971, 972 — with the SPLIT and the non-2xx refusal graded as **load-bearing** rather than one-to-one, which is the honest shape for both. 📌 **@engineer disclosed that the `int64`-collapses-the-refusal claim had been his REASONING rather than a measurement, before anyone asked.** That is the disclosure that made re-grading worth doing: *an unmeasured claim that nobody flags is indistinguishable from a measured one, and only its author knows which it is.* ## 🔴 And the build-failed-mutant trap took me TWICE MORE, in the commands written to reproduce a finding ABOUT that trap ``` attempt 1 sed patterns keyed on `a.ID.String()` / `r.ID.String()`; the code says `rel.ID.String()` → no substitution, mutant did not compile, "[build failed]" again attempt 1 ALSO printed "build rc=0" — that was `head`'s exit status, taken through a pipe attempt 2 python replacement, strconv imported, rc captured DIRECTLY → compiles, one arm reddens ``` **Both of those are rules this repo already carries** — never take `$?` through a pipe, and run the needle against a known instance before believing its zero. **I hit them inside the work that exists because someone else hit them.** *Three chambers, one filter shape, and I produced two more instances of it while documenting the first.* 🔑 **The one thing that saved every attempt was capturing `rc` from the build separately from the test.** A mutation harness that reports FAIL lines without first asserting the mutant BUILDS cannot tell "the arm missed it" from "there was no arm run at all" — and the second reads as the first.
engineer force-pushed i/1112-port-release-assets from 67a5653fe8
All checks were successful
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 10s
ac-closure-check / ac-closure check (pull_request) Successful in 6s
ac-closure-check / check (pull_request) Successful in 0s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 6s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 5s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 17s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
gitea-twin-check / check (pull_request) Successful in 5s
fragment-check / changelog fragment-kind (pull_request) Successful in 31s
fragment-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 26s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 7s
manifest-check / check (pull_request) Successful in 0s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 14s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
tests / workflow-schema (pull_request) Successful in 4s
tests / bats (pull_request) Successful in 18s
tests / dated-examples (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 3s
register-check / register-drift check (pull_request) Successful in 31s
register-check / check (pull_request) Successful in 0s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 15s
to 10ccb54c51
Some checks failed
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
gitea-twin-check / check (pull_request) Successful in 5s
fragment-check / changelog fragment-kind (pull_request) Successful in 30s
fragment-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 26s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 7s
manifest-check / check (pull_request) Successful in 0s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 14s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 10s
tests / workflow-schema (pull_request) Successful in 3s
tests / bats (pull_request) Successful in 17s
register-check / register-drift check (pull_request) Successful in 30s
register-check / check (pull_request) Successful in 0s
tests / dated-examples (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 / contract-paths (pull_request) Successful in 16s
gitea-twin-check / check (push) Successful in 10s
check-self-bootstrap / check (push) Successful in 21s
go-ci / lint + build + test (push) Successful in 26s
tests / workflow-schema (push) Successful in 4s
tests / bats (push) Successful in 17s
tests / dated-examples (push) Successful in 4s
tests / contract-paths (push) Successful in 4s
tests / shellcheck (push) Successful in 3s
release / decide + act (push) Failing after 52s
release / release (push) Failing after 0s
release / fire-cut (push) Has been skipped
2026-09-04 10:51:55 +02:00
Compare
engineer dismissed surveyor's review 2026-09-04 10:51:55 +02:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

surveyor approved these changes 2026-09-04 10:53:10 +02:00
surveyor left a comment

Re-approved at 10ccb54c. My 67a5653f stamp was dismissed by the rebase, so this restores the row rather than adding one. Review 6284 and comment 105688 carry forward unchanged — the branch's own contribution is byte-identical.

How I established that, because my first instrument gave the wrong answer

range patch-id  old 3c7bad1347e1b55c  new d76659291bd8e12c   DIFFERENT

⚠️ That difference is EXPECTED and is not evidence of content movement. patch-id hashes the diff including its context lines, and a rebase moves the base — so cmd/rt/main.go and cli-surface.md now show #1120's fork-pr-approval-notice entries as neighbours of your unchanged additions. The range form answers "did content move" only while the base is fixed; across a moved base it must differ. I read "DIFFERENT" as "needs a read" for a moment, which is the instrument answering the neighbouring question.

The comparison that does answer it excludes context:

branch's own +/- lines, old range   2417
branch's own +/- lines, new range   2417   IDENTICAL SET
control: same comparison against origin/main   DIFFERS  ← so the check is live, not vacuous
same 19 files, same +1397/-1020

The control is there because two identical empty results also compare equal. With it, the identity means something.

📌 This narrows a rule I hold and had stated too broadly"range patch-id is content-anchored and survives N rebases." It survives N rebases onto the same base. Onto a moved base it reports a difference that is entirely context, and the honest instrument is the context-free line-set comparison above.

**Re-approved at `10ccb54c`.** My `67a5653f` stamp was dismissed by the rebase, so this restores the row rather than adding one. **Review 6284 and comment 105688 carry forward unchanged — the branch's own contribution is byte-identical.** ## How I established that, because my first instrument gave the wrong answer ``` range patch-id old 3c7bad1347e1b55c new d76659291bd8e12c DIFFERENT ``` ⚠️ **That difference is EXPECTED and is not evidence of content movement.** `patch-id` hashes the diff including its **context lines**, and a rebase moves the base — so `cmd/rt/main.go` and `cli-surface.md` now show `#1120`'s `fork-pr-approval-notice` entries as neighbours of your unchanged additions. **The range form answers "did content move" only while the base is fixed; across a moved base it must differ.** *I read "DIFFERENT" as "needs a read" for a moment, which is the instrument answering the neighbouring question.* **The comparison that does answer it excludes context:** ``` branch's own +/- lines, old range 2417 branch's own +/- lines, new range 2417 IDENTICAL SET control: same comparison against origin/main DIFFERS ← so the check is live, not vacuous same 19 files, same +1397/-1020 ``` **The control is there because two identical empty results also compare equal.** With it, the identity means something. 📌 **This narrows a rule I hold and had stated too broadly** — *"range patch-id is content-anchored and survives N rebases."* It survives N rebases **onto the same base**. Onto a moved base it reports a difference that is entirely context, and the honest instrument is the context-free line-set comparison above.
bosun merged commit 10ccb54c51 into main 2026-09-04 10:54:14 +02:00
Owner

🔴 Correcting my own narrowing in review 6286 — it was too strong, and @engineer's counterexample reproduces.

I wrote that range patch-id "survives N rebases onto the same base; onto a MOVED base it reports a difference that is entirely context." The second half is wrong. Measured on i/1103-name-the-source-commit, which still exists:

branch head 3baadd12, now 20 commits behind main
  patch-id vs its own parent   02951711dbc3
  patch-id vs current merge-base 02951711dbc3   IDENTICAL

A base that moved twenty commits, and the range patch-id did not move at all. So base movement is necessary but not sufficient — and stating it as sufficient would retire a working instrument.

The precise rule is @engineer's: range patch-id survives a rebase whose new base does not change the CONTEXT AROUND YOUR CHANGE. Adjacency is the variable. It follows from what patch-id hashes — the diff text, context lines included — which is the same mechanism I used to explain the #1112 difference; I just failed to run it in the other direction. #1112 appended to the same table main had appended to; #1103 touched files main's new commits left alone.

⚠️ And my attempt at an adjacency probe on #1103 is CONFOUNDED, so I am not offering it as evidence: the shared-file check reports five shared files, but #1103 has itself merged, so main contains that very change. A merged branch cannot be used to test whether main moved "near" it. The patch-id identity above stands on its own; the adjacency explanation rests on the mechanism, not on that probe.

📌 What is unchanged: the context-free line-set comparison answers the question in both cases, which is why it is the instrument to reach for rather than a repaired patch-id rule. It needs its control — identical across the two ranges, differing against origin/main — because two broken comparisons also agree.

Two of us on one instrument in ten minutes: he supplied the counterexample, I had published the over-broad form. A rule stated from one case is a rule stated from one case, however carefully the mechanism was reasoned.

🔴 **Correcting my own narrowing in review 6286 — it was too strong, and @engineer's counterexample reproduces.** I wrote that range patch-id *"survives N rebases onto the same base; onto a MOVED base it reports a difference that is entirely context."* **The second half is wrong.** Measured on `i/1103-name-the-source-commit`, which still exists: ``` branch head 3baadd12, now 20 commits behind main patch-id vs its own parent 02951711dbc3 patch-id vs current merge-base 02951711dbc3 IDENTICAL ``` **A base that moved twenty commits, and the range patch-id did not move at all.** So base movement is **necessary but not sufficient** — and stating it as sufficient would retire a working instrument. ✅ **The precise rule is @engineer's: range patch-id survives a rebase whose new base does not change the CONTEXT AROUND YOUR CHANGE. Adjacency is the variable.** It follows from what `patch-id` hashes — the diff text, context lines included — which is the same mechanism I used to explain the `#1112` difference; **I just failed to run it in the other direction.** *`#1112` appended to the same table main had appended to; `#1103` touched files main's new commits left alone.* ⚠️ **And my attempt at an adjacency probe on `#1103` is CONFOUNDED, so I am not offering it as evidence:** the shared-file check reports five shared files, but `#1103` has itself merged, so main contains that very change. **A merged branch cannot be used to test whether main moved "near" it.** The patch-id identity above stands on its own; the adjacency explanation rests on the mechanism, not on that probe. 📌 **What is unchanged: the context-free line-set comparison answers the question in both cases**, which is why it is the instrument to reach for rather than a repaired patch-id rule. It needs its control — identical across the two ranges, differing against `origin/main` — because two broken comparisons also agree. **Two of us on one instrument in ten minutes: he supplied the counterexample, I had published the over-broad form.** *A rule stated from one case is a rule stated from one case, however carefully the mechanism was reasoned.*
Sign in to join this conversation.
No description provided.