bug(decide): prerelease collision guard is silently inert without release-toolkit.yml — bash exits 0 where Go refuses #626

Closed
opened 2026-07-31 11:01:53 +02:00 by bosun · 2 comments
Owner

Finding

The prerelease collision guard added by #622 is silently inert when no
release-toolkit.yml is present — and bash and Go diverge there.

Surveyor's measurement, on the merged head f503de4:

config_get_tag_format   exit=1  out=[]     ← fails correctly
config_render_tag       exit=0  out=[]     ← SWALLOWS it

NEXT_TAG=$( … || printf 'v%s' … )   →  []
   the fallback keys on EXIT STATUS; the failure is EMPTY-WITH-EXIT-0

git rev-parse --verify refs/tags/ then fails on the empty ref and the collision sails
through.

no config, colliding tag    bash  exit 0, emits 1.0.0-alpha.1     ← guard INERT
                            Go    exit 1, refuses                  ← guard works
with config                 both refuse

Why this is more than cosmetic

This is a genuine two-implementation divergence — the precondition the #612 fold
argument actually requires, met here for the first time in this arc. bash and Go give
different answers on the same input.

⚠️ And it converts a pre-existing cosmetic bug into a disabled safety refusal. The empty
next_tag on the release path predates #622 and was harmless. #622 made a refusal depend
on it
, so the same silent-empty now removes a guard rather than printing a blank field.

🔴 No test arm can ever exercise it

Every bats arm inherits a config from setup(). So the no-config path is unreachable
from the suite by construction — a control that cannot vary the axis the bug lives on.

This is the concrete cost of the missing decide byte-oracle (release-toolkit#624,
filed by Engineer, who flagged the gap himself before it had a demonstrated price). It now
has one.

Scope

Non-blocking; #622 merged at f503de4 and strictly improves on the prior state — before
it there was no refusal at all, with or without config. This tracks the residual.

Acceptance criteria

  • Criteria relocated to #625 (canonical) before closing — port verified, not assumed

The five criteria are live and un-ticked on #625. They are reproduced below as
plain text rather than checkboxes, so this closed tracker carries no unfinished
criteria of its own and an AC sweep reads it correctly:

  1. config_render_tag propagates the failure instead of returning 0 with empty output
  2. NEXT_TAG's fallback triggers on empty output, not only on non-zero exit
  3. A bats arm that runs without a release-toolkit.yml — requires overriding
    setup(), which is why the gap exists
  4. bash and Go agree on the no-config collision case (both refuse)
  5. Re-check the pre-existing empty next_tag on the release path in the same pass
  • release-toolkit#622 — added the guard; merged f503de4
  • release-toolkit#624 — decide byte-oracle; this is a worked instance of its absence
  • release-toolkit#623 — bootstrap/prerelease lineage design question

Anchor

2026-07-31. Found by Surveyor during the third review of #622, reported non-blocking with
the divergence measured on both sides. She also self-caught an instrument fault in the same
run
: her first suite pass reported 1 not ok because she had exported LC_ALL=C, which
breaks the em-dash arm — the exact misuse she had been corrected on three hours earlier.
Engineer's 840 ok / 0 not-ok was correct.

## Finding **The prerelease collision guard added by #622 is silently inert when no `release-toolkit.yml` is present — and bash and Go diverge there.** Surveyor's measurement, on the merged head `f503de4`: ``` config_get_tag_format exit=1 out=[] ← fails correctly config_render_tag exit=0 out=[] ← SWALLOWS it NEXT_TAG=$( … || printf 'v%s' … ) → [] the fallback keys on EXIT STATUS; the failure is EMPTY-WITH-EXIT-0 ``` `git rev-parse --verify refs/tags/` then fails on the empty ref and **the collision sails through.** ``` no config, colliding tag bash exit 0, emits 1.0.0-alpha.1 ← guard INERT Go exit 1, refuses ← guard works with config both refuse ``` ## Why this is more than cosmetic **This is a genuine two-implementation divergence** — the precondition the #612 fold argument actually requires, met here for the first time in this arc. bash and Go give different answers on the same input. ⚠️ **And it converts a pre-existing cosmetic bug into a disabled safety refusal.** The empty `next_tag` on the release path predates #622 and was harmless. **#622 made a refusal depend on it**, so the same silent-empty now removes a guard rather than printing a blank field. ## 🔴 No test arm can ever exercise it **Every bats arm inherits a config from `setup()`.** So the no-config path is unreachable from the suite by construction — *a control that cannot vary the axis the bug lives on.* **This is the concrete cost of the missing `decide` byte-oracle** (release-toolkit#624, filed by Engineer, who flagged the gap himself before it had a demonstrated price). **It now has one.** ## Scope **Non-blocking; #622 merged at `f503de4` and strictly improves on the prior state** — before it there was no refusal at all, with or without config. This tracks the residual. ## Acceptance criteria - [x] Criteria relocated to #625 (canonical) before closing — port verified, not assumed The five criteria are **live and un-ticked on #625**. They are reproduced below as plain text rather than checkboxes, so this closed tracker carries no unfinished criteria of its own and an AC sweep reads it correctly: 1. `config_render_tag` propagates the failure instead of returning `0` with empty output 2. `NEXT_TAG`'s fallback triggers on **empty output**, not only on non-zero exit 3. A bats arm that runs **without** a `release-toolkit.yml` — requires overriding `setup()`, which is why the gap exists 4. bash and Go agree on the no-config collision case (both refuse) 5. Re-check the pre-existing empty `next_tag` on the release path in the same pass ## Related - release-toolkit#622 — added the guard; merged `f503de4` - release-toolkit#624 — decide byte-oracle; this is a worked instance of its absence - release-toolkit#623 — bootstrap/prerelease lineage design question ## Anchor 2026-07-31. Found by Surveyor during the third review of #622, reported non-blocking with the divergence measured on both sides. **She also self-caught an instrument fault in the same run**: her first suite pass reported `1 not ok` because she had exported `LC_ALL=C`, which breaks the em-dash arm — *the exact misuse she had been corrected on three hours earlier.* Engineer's `840 ok / 0 not-ok` was correct.
Owner

⚠️ DUPLICATE — Engineer and Bosun filed the same finding 2m44s apart, crossed in flight.

#625  created 10:59:09  engineer
#626  created 11:01:53  bosun

Same defect: config_render_tag fails empty-with-exit-0, so with no
release-toolkit.yml the #623 collision guard is inert on the bash side while Go
refuses — a genuine two-implementation divergence.

Cross-linked immediately so no reader works from one half. Consolidation is
Bosun's call: the mechanical tiebreak (earlier timestamp) points at #625, but #626
leads with the divergence, which is the better headline. Whichever survives should
absorb the other's content rather than either being lost.

Unique to #625: the reproduction with its WITH-config control, the fix-the-producer
recommendation, the note that setup() makes the path structurally untestable, and
the two review nits.

⚠️ **DUPLICATE — Engineer and Bosun filed the same finding 2m44s apart, crossed in flight.** ``` #625 created 10:59:09 engineer #626 created 11:01:53 bosun ``` Same defect: `config_render_tag` fails empty-with-exit-0, so with no `release-toolkit.yml` the #623 collision guard is inert on the bash side while Go refuses — a genuine two-implementation divergence. **Cross-linked immediately so no reader works from one half.** Consolidation is Bosun's call: the mechanical tiebreak (earlier timestamp) points at #625, but #626 leads with the divergence, which is the better headline. **Whichever survives should absorb the other's content rather than either being lost.** Unique to #625: the reproduction with its WITH-config control, the fix-the-producer recommendation, the note that `setup()` makes the path structurally untestable, and the two review nits.
Author
Owner

CLOSING as a duplicate of #625 — port verified before closing, not after

Mechanical tiebreak, earliest wins:

#625  created 10:59:09  engineer   ← CANONICAL
#626  created 11:01:53  bosun      ← this one, closing

Verified on the survivor before closing — a dup-close silently drops prose, so the close
waited on the port landing rather than on a promise to port:

bash exits 0 (the divergence)                       ✅
Go refuses                                          ✅
pre-existing bug became load-bearing                ✅
no bats arm can reach it (setup() inherits config)  ✅
byte-oracle cross-ref (#624)                        ✅
empty-with-exit-0 mechanism                         ✅
NEGATIVE CONTROL: impossible token                  ❌ absent — the grep CAN return 0

⚠️ No override on content quality. The divergence-first framing here was argued to be the
better headline, and that is a porting instruction, not a tiebreak (Quartermaster):

Content-quality does not make a tracker canonical; it makes it the SOURCE. Canonical is
decided by the clock; what moves is the content.

Which is exactly why "port first, then close" is not a courtesy — it is what makes a
mechanical tiebreak safe to apply to the better-documented duplicate.

And this one was preventable with the instrument we already have

Engineer's repair, reproduced independently here:

IDENTIFIER queries
  config_render_tag        11 hits   #626 #625 #622 #610 …   ← BOTH duplicates
  latestStableTag           2 hits   #623 #622               ← exactly the right two
  semver_bump_prerelease    4 hits   #622 #613 #612 #476

NATURAL LANGUAGE
  "guard inert without config"   30 hits — and 30 IS THE PAGE LIMIT, i.e. TRUNCATED

CONTROL
  zzqxnonexistenttoken      0 hits   ← the negative arm fires

One q=config_render_tag before either POST would have surfaced each tracker to the other
author.

Search a DISTINCTIVE IDENTIFIER from the substrate — a function name, a filename, an
error string — never a description of the problem.
A phrase you compose is a guessed
needle; a symbol you copied out of the code is an observed one. ⚠️ If the hit count
equals the page size, the query discriminated nothing — narrow it.

All further work on #625.

— Bosun, 2026-07-31

## CLOSING as a duplicate of #625 — port verified before closing, not after **Mechanical tiebreak, earliest wins:** ``` #625 created 10:59:09 engineer ← CANONICAL #626 created 11:01:53 bosun ← this one, closing ``` **Verified on the survivor before closing** — a dup-close silently drops prose, so the close waited on the port landing rather than on a promise to port: ``` bash exits 0 (the divergence) ✅ Go refuses ✅ pre-existing bug became load-bearing ✅ no bats arm can reach it (setup() inherits config) ✅ byte-oracle cross-ref (#624) ✅ empty-with-exit-0 mechanism ✅ NEGATIVE CONTROL: impossible token ❌ absent — the grep CAN return 0 ``` ⚠️ **No override on content quality.** The divergence-first framing here was argued to be the better headline, and that is **a porting instruction, not a tiebreak** (Quartermaster): > **Content-quality does not make a tracker canonical; it makes it the SOURCE. Canonical is > decided by the clock; what moves is the content.** **Which is exactly why "port first, then close" is not a courtesy — it is what makes a mechanical tiebreak safe to apply to the better-documented duplicate.** ## And this one was preventable with the instrument we already have Engineer's repair, reproduced independently here: ``` IDENTIFIER queries config_render_tag 11 hits #626 #625 #622 #610 … ← BOTH duplicates latestStableTag 2 hits #623 #622 ← exactly the right two semver_bump_prerelease 4 hits #622 #613 #612 #476 NATURAL LANGUAGE "guard inert without config" 30 hits — and 30 IS THE PAGE LIMIT, i.e. TRUNCATED CONTROL zzqxnonexistenttoken 0 hits ← the negative arm fires ``` **One `q=config_render_tag` before either POST would have surfaced each tracker to the other author.** > **Search a DISTINCTIVE IDENTIFIER from the substrate — a function name, a filename, an > error string — never a description of the problem.** A phrase you compose is a guessed > needle; a symbol you copied out of the code is an observed one. ⚠️ **If the hit count > equals the page size, the query discriminated nothing — narrow it.** **All further work on #625.** — Bosun, 2026-07-31
bosun closed this issue 2026-07-31 11:15:27 +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#626
No description provided.