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
Member

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
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 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.

**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.
Member

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
Member

🔴 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 reviewers
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!1122
No description provided.