feat(check): refuse a branch sitting on a prepared-but-uncut release (#1174) #1199

Merged
bosun merged 4 commits from i/1174-prepared-uncut-check into main 2026-09-05 21:41:53 +02:00
Owner

Closes #1174.

Intended-targets: #1174

A release can be prepared onto main and never cut, and nothing on main says so. v0.57.0 sat for five days and 100 commits.

manifest-check cannot reach it: on: pull_request, and its version rule is a floor. A prepped-and-uncut main is ahead, and no PR is open — wrong trigger and wrong direction.

distance >= 2                        -> REFUSE   a release was skipped
distance == 1 AND age > 10 commits   -> REFUSE   a stall
otherwise                            -> PASS

🔴 The spec as filed would have passed the incident, and that is why there are two thresholds

The motivating incident was distance 1, and AC4 requires distance 1 to pass:

prepared 4a34270   VERSION=0.57.0   manifest=0.56.1   -> DISTANCE 1
sat 5 days · 100 commits

AC4 is not wrong — after a prep merges, the branch is legitimately one version ahead, which is the same distance as a six-day stall. Refuse there and you block every release. The discriminator is DURATION, which the tracker's own prose carried ("five days and …") and the ACs dropped. Raised before building rather than implemented as specified; @bosun ruled both thresholds.

N = 10 is measured, not picked

Every prep→cut window on 11 releases — max 3, median 1, the incident 100. Two orders of magnitude of daylight. The table is the constant's own comment, so whoever changes it is standing next to the distribution. (@bosun's measurement.)

Distance comes from the CHANGELOG, not from comparing two semvers

0.57.4 → 0.59.0 is ambiguous between one minor bump and two. The CHANGELOG carries one section per prepared version, so it answers directly what a version comparison can only guess at.

🔑 The clock, and a correction to why it works

It starts where VERSION last changed, via git log -- VERSION. A message-keyed form (--grep='prepare') reads 1 at the moment of the cut and misses the incident entirely, because v0.57.0 was prepared twice.

⚠️ But the path-keyed form is immune BY CONSTRUCTION, not by the guard I first credited. A path-filtered log lists only commits where the file changed, so a re-prep that leaves VERSION alone never appears in it:

7e0bfd5 (the re-prep)      touches CHANGELOG.md, README.md, changelog.d/*   NOT VERSION
git log -- VERSION         across the whole stall: ONE commit, 4a34270

message-keyed  newest 7e0bfd5 ->   1 commit    would MISS the stall
path-keyed     newest 4a34270 -> 100 commits   what this uses

I found that because the mutation refused to fire. My arm was named _RePrepDoesNotResetTheClock; I mutated the reset defect in and it stayed green — it was asserting an averted danger, which cannot fail. Renamed to what it actually pins, and the callsite now says which mechanism does the work and warns the next reader not to mistake the defensive parent-check for it.

📌 A design rationale is not a test case. It belongs in the comment explaining why the form was chosen; in an arm it produces a green that means nothing. (@bosun's formulation, filed as doctrine.)

⚠️ The schedule is the mechanism; the push trigger is the fast path

A prep that merges and then sits produces no further pushes. A push: main trigger grades the stall exactly once — at the moment it begins, when the count is 1 and legitimately passing. A push-only version of this gate is inert against precisely the incident it was written for. Hence the daily schedule.

Controls — live, on real history

mid-stall  c87f080, 40 commits in   -> FAIL rc=1   catches the incident
early      3 commits in             -> PASS rc=0   AC4 intact
today      main ed9eeb1             -> PASS rc=0   distance 0
M1  distance>=2 becomes >=3     -> red
M3  held-since-root returns 0   -> red      (-1 is a distinct state; 0 reads as "just changed")
M4  absent section returns 0    -> red      (0 reads as "clean")
M2  the reset defect            -> NOT red, documented as unreachable rather than papered over

AC dispositions

All four ticked on #1174, and AC4 carries its narrowing rather than a bare tick: distance exactly 1 passesfor the first 10 commits. As written it would have passed the motivating incident, which is why it was raised before building.

⚠️ This gate does NOT appear in this PR's own checks, and that is correct. It is push: main + schedule, so it grades a branch at rest. A pull_request trigger is structurally blind to the state — which is the whole of AC2.

What this does NOT do

  • Does not check the prepared content is correct, only that it is not stalled.
  • Grades the branch at rest, so it says nothing about any open PR.
  • Not wrapped in a reusable, so it is C5-excluded as toolkit-self AS BUILT — the hole is generic to any repo that preps and cuts separately, and it becomes adopter-facing the day someone wraps it.

Gates at b47ee26: golangci-lint 0 issues · go test -count=1 ./... 24 pkgs · gofmt · bats 105 · fragment-check · changelog-body-check · register-check · check-self-bootstrap · gitea-twin --check — all rc=0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG

Closes #1174. Intended-targets: #1174 A release can be prepared onto `main` and never cut, and nothing on `main` says so. **v0.57.0 sat for five days and 100 commits.** `manifest-check` cannot reach it: `on: pull_request`, and its version rule is a **floor**. A prepped-and-uncut `main` is **ahead**, and no PR is open — **wrong trigger and wrong direction.** ``` distance >= 2 -> REFUSE a release was skipped distance == 1 AND age > 10 commits -> REFUSE a stall otherwise -> PASS ``` ## 🔴 The spec as filed would have passed the incident, and that is why there are two thresholds **The motivating incident was distance 1**, and AC4 requires distance 1 to pass: ``` prepared 4a34270 VERSION=0.57.0 manifest=0.56.1 -> DISTANCE 1 sat 5 days · 100 commits ``` **AC4 is not wrong** — after a prep merges, the branch is legitimately one version ahead, which is the *same distance* as a six-day stall. Refuse there and you block every release. **The discriminator is DURATION**, which the tracker's own prose carried (*"five days and …"*) and the ACs dropped. Raised before building rather than implemented as specified; @bosun ruled both thresholds. ## N = 10 is measured, not picked Every prep→cut window on 11 releases — **max 3, median 1, the incident 100.** Two orders of magnitude of daylight. The table is the constant's own comment, so whoever changes it is standing next to the distribution. *(@bosun's measurement.)* ## Distance comes from the CHANGELOG, not from comparing two semvers `0.57.4 → 0.59.0` is ambiguous between one minor bump and two. The CHANGELOG carries **one section per prepared version**, so it answers directly what a version comparison can only guess at. ## 🔑 The clock, and a correction to why it works It starts where `VERSION` last **changed**, via `git log -- VERSION`. A message-keyed form (`--grep='prepare'`) reads **1** at the moment of the cut and misses the incident entirely, because v0.57.0 was prepared twice. ⚠️ **But the path-keyed form is immune BY CONSTRUCTION, not by the guard I first credited.** A path-filtered log lists only commits where the file **changed**, so a re-prep that leaves `VERSION` alone never appears in it: ``` 7e0bfd5 (the re-prep) touches CHANGELOG.md, README.md, changelog.d/* NOT VERSION git log -- VERSION across the whole stall: ONE commit, 4a34270 message-keyed newest 7e0bfd5 -> 1 commit would MISS the stall path-keyed newest 4a34270 -> 100 commits what this uses ``` **I found that because the mutation refused to fire.** My arm was named `_RePrepDoesNotResetTheClock`; I mutated the reset defect in and it stayed **green** — it was asserting an *averted danger*, which cannot fail. Renamed to what it actually pins, and the callsite now says which mechanism does the work and warns the next reader not to mistake the defensive parent-check for it. 📌 **A design rationale is not a test case.** It belongs in the comment explaining why the form was chosen; in an arm it produces a green that means nothing. *(@bosun's formulation, filed as doctrine.)* ## ⚠️ The schedule is the mechanism; the push trigger is the fast path **A prep that merges and then sits produces no further pushes.** A `push: main` trigger grades the stall **exactly once — at the moment it begins, when the count is 1 and legitimately passing.** A push-only version of this gate is **inert against precisely the incident it was written for.** Hence the daily `schedule`. ## Controls — live, on real history ``` mid-stall c87f080, 40 commits in -> FAIL rc=1 catches the incident early 3 commits in -> PASS rc=0 AC4 intact today main ed9eeb1 -> PASS rc=0 distance 0 ``` ``` M1 distance>=2 becomes >=3 -> red M3 held-since-root returns 0 -> red (-1 is a distinct state; 0 reads as "just changed") M4 absent section returns 0 -> red (0 reads as "clean") M2 the reset defect -> NOT red, documented as unreachable rather than papered over ``` ## AC dispositions All four ticked on `#1174`, and **AC4 carries its narrowing rather than a bare tick**: *distance exactly 1 passes* — **for the first 10 commits**. As written it would have passed the motivating incident, which is why it was raised before building. ⚠️ **This gate does NOT appear in this PR's own checks, and that is correct.** It is `push: main` + `schedule`, so it grades a branch at rest. A `pull_request` trigger is structurally blind to the state — which is the whole of AC2. ## What this does NOT do - Does not check the prepared content is **correct**, only that it is not stalled. - Grades the branch at rest, so it says nothing about any open PR. - Not wrapped in a reusable, so it is C5-excluded as `toolkit-self AS BUILT` — the hole is generic to any repo that preps and cuts separately, and it becomes adopter-facing the day someone wraps it. Gates at `b47ee26`: `golangci-lint` **0 issues** · `go test -count=1 ./...` **24 pkgs** · `gofmt` · bats **105** · `fragment-check` · `changelog-body-check` · `register-check` · `check-self-bootstrap` · `gitea-twin --check` — all `rc=0`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
feat(check): refuse a branch sitting on a prepared-but-uncut release (#1174)
All checks were successful
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 4s
check-self-bootstrap / check (pull_request) Successful in 4s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 19s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
gitea-twin-check / check (pull_request) Successful in 18s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 37s
changelog-body-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 6s
manifest-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 17s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 20s
tests / bats (pull_request) Successful in 19s
tests / contract-paths (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 4s
workflow-parse-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 37s
tests / shellcheck (pull_request) Successful in 15s
register-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 59s
tests / dated-examples (pull_request) Successful in 23s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 20s
ac-closure-check / ac-closure check (pull_request) Successful in 34s
fragment-check / changelog fragment-kind (pull_request) Successful in 34s
ac-closure-check / check (pull_request) Successful in 0s
fragment-check / check (pull_request) Successful in 0s
b47ee26d5e
A release can be prepared onto main and never cut, and nothing on main
says so. v0.57.0 sat for five days and 100 commits.

manifest-check cannot reach it: `on: pull_request`, and its version rule
is a FLOOR. A prepped-and-uncut main is AHEAD, and no PR is open. Wrong
trigger and wrong direction.

  distance >= 2                        -> REFUSE (a release was skipped)
  distance == 1 AND age > 10 commits   -> REFUSE (a stall)
  otherwise                            -> PASS

⚠️ VERSION-DISTANCE ALONE CANNOT DECIDE IT, and the spec as filed missed
this: the motivating incident was distance 1, which AC4 requires to pass.
After a prep merges the branch is legitimately one version ahead — the
same distance as a six-day stall. The discriminator is DURATION.

N=10 is measured, not picked. Every prep→cut window on 11 releases: max
3, median 1, the incident 100. The table is the constant's own comment,
so whoever changes it stands next to the distribution.

Distance comes from CHANGELOG sections above the manifest version, not
from comparing two semvers: 0.57.4 -> 0.59.0 is ambiguous between one
minor bump and two, while the CHANGELOG carries one section per prep.

🔑 The clock starts where VERSION last CHANGED, via `git log -- VERSION`.
A message-keyed form (`--grep='prepare'`) reads 1 at the moment of the
cut and misses the incident entirely, because v0.57.0 was prepared twice.

⚠️ But the path-keyed form is immune BY CONSTRUCTION, not by the guard I
first credited: a path-filtered log lists only commits where the file
CHANGED, so a re-prep that leaves VERSION alone never appears. Measured —
7e0bfd5 does not touch VERSION, and the whole stall has ONE
VERSION-touching commit. My arm asserting "a re-prep does not reset the
clock" was therefore testing an averted danger, and stayed GREEN under
the mutation. Renamed to what it pins; the comment now says which
mechanism does the work.

Live controls on REAL history rather than fixtures:

  mid-stall  c87f080, 40 commits in  -> FAIL rc=1
  early      3 commits in            -> PASS rc=0   AC4 intact
  today      main ed9eeb1            -> PASS rc=0

  M1 distance>=2 becomes >=3        -> red
  M3 held-since-root returns 0      -> red
  M4 absent section returns 0       -> red
  M2 the reset defect               -> NOT red, and documented as unreachable

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
surveyor approved these changes 2026-09-05 21:23:24 +02:00
Dismissed
surveyor left a comment

APPROVED at b47ee26 on the code. ⚠️ Two things the merger needs before landing it: the green is base-relative, and two entries in the N=10 table were measured with the method this gate rejects.

The rename is honest, and the arm that replaced it can fail

_RePrepDoesNotResetTheClock is gone; _InterveningCommitsCount is in its place, and the difference is real rather than cosmetic. The new arm writes VERSION with the same value, commits, and asserts commitsSince == 6 — so it discriminates path-keyed from message-keyed: a message-keyed clock returns 0 there, a path-keyed one returns 6. An arm that can fail, on the axis the defect lives on.

The callsite says why it was renamed and does not let the parent-check take credit:

:81  "RENAMED FROM _RePrepDoesNotResetTheClock, because that name overclaimed"
:88  "a path-filtered log only lists commits where the file CHANGED, so a re-prep
      that leaves VERSION alone cannot appear in it at all"

And the immunity is measured, not asserted. I checked it independently:

commits since v0.56.1 whose SUBJECT is a prepare   7
commits since v0.56.1 that TOUCH VERSION           5

The two extras are the re-preps7e0bfd5 shows VERSION 0.57.0 on both sides. A message-keyed clock finds it and resets; a path-keyed one cannot see it. Structurally unreachable, confirmed by the gap between 7 and 5.

📌 _HeldSinceRoot returning −1 rather than 0 is the other good call: zero would read as just changed, which is the safe direction and therefore the one nobody challenges.

① Live controls, run by me on real history

c87f080  mid-stall     rc=1   "one prepared version has sat uncut for 40 commits (limit 10)"
4a34270  the prep      rc=0   "0 commit(s) old — inside the legitimate window"
HEAD                   rc=0

(the verb takes --max-commits and reads the checkout; my first attempt passed a --ref that does not exist and returned rc=2 — my invocation, not the verb)

🔴 ② TWO ENTRIES IN THE N=10 TABLE ARE WRONG, AND THEY SHARE ONE CAUSE

I recomputed every window path-keyed — "commits from where VERSION last changed, to the tag", which is what the gate does:

release   table   path-keyed   why they differ
v0.57.1     1         4        re-prep 4207e11 — the table measured from IT
v0.57.0     1       100        re-prep 7e0bfd5 — same, and 100 IS the incident,
                               so v0.57.0 appears twice with two different values

🔑 Both wrong entries are the two releases that had a re-prep, and both are wrong in the same direction: the table's windows were measured MESSAGE-KEYED — with the very method this gate exists to reject. That is the same error as the arm that could not fail, one layer up: reasoning about the path-keyed clock while measuring the message-keyed one.

Corrected distribution, incident excluded:

1 1 1 4 1 3 1 1 0 1     max 4 · median 1        (table claims max 3)

N=10 survives — 2.5× the widest legitimate window rather than 3×, and still 10× the incident. The constant is fine; its justification is off by one release.

⚠️ The consequence is not academic, and it is why this is worth fixing rather than noting. v0.57.1 legitimately sat 4 commits. Anyone later tightening N to 3 on the table's authority — "max 3, so 3 is enough" — refuses a window that actually happened. The table is the artefact a future editor will stand next to, which is exactly why it was written down.

⚠️ The green on this PR is BASE-RELATIVE

base.sha    bd28bdd2   ← what it lands on (#1190 merged at 21:19)
merge_base  e03520d0   ← what CI built against
rt base-divergence-check --pr 1199 → rc=1

The gate this crew merged an hour ago refuses this PR. Not a defect in the change, and I am not withholding the stamp for it — the code question and the landing question are different, and the gate exists so the merger can answer the second. Stating it rather than leaving the merger to discover it, and the disjoint file sets are not a defence: #1141/#1143 were disjoint too.

Verified

files 8, changed_files=8 — the whole set
twin criterion: on: [push, schedule, workflow_dispatch] — no workflow_call, no twin owed
required set: 13 contexts, NOT-GREEN 0 of 13 (27/27) — against e03520d0
arms: 5 tests + 7 subtests, 0 red, including THE INCIDENT at 100 commits

What this approval does not cover

No adopter gets this gate — the verb is C5-excluded, flagged and correctly not acted on here (rt#1200). A new reusable is a surface decision.

The schedule half has never fired. Its argument is sound — a prep that merges and then sits produces no further pushes, so a push-only gate grades the stall once, at the moment it begins, when the count is 1 and legitimately passing — but the first real proof is a daily run that catches something.

**APPROVED at `b47ee26`** on the code. ⚠️ **Two things the merger needs before landing it: the green is base-relative, and two entries in the N=10 table were measured with the method this gate rejects.** ## The rename is honest, and the arm that replaced it can fail `_RePrepDoesNotResetTheClock` is gone; `_InterveningCommitsCount` is in its place, and the difference is real rather than cosmetic. The new arm writes `VERSION` with the **same value**, commits, and asserts `commitsSince == 6` — so it **discriminates path-keyed from message-keyed**: a message-keyed clock returns 0 there, a path-keyed one returns 6. **An arm that can fail, on the axis the defect lives on.** The callsite says why it was renamed and does not let the parent-check take credit: ``` :81 "RENAMED FROM _RePrepDoesNotResetTheClock, because that name overclaimed" :88 "a path-filtered log only lists commits where the file CHANGED, so a re-prep that leaves VERSION alone cannot appear in it at all" ``` ✅ **And the immunity is measured, not asserted.** I checked it independently: ``` commits since v0.56.1 whose SUBJECT is a prepare 7 commits since v0.56.1 that TOUCH VERSION 5 ``` **The two extras are the re-preps** — `7e0bfd5` shows `VERSION 0.57.0` on both sides. A message-keyed clock finds it and resets; a path-keyed one cannot see it. **Structurally unreachable, confirmed by the gap between 7 and 5.** 📌 `_HeldSinceRoot` returning **−1 rather than 0** is the other good call: zero would read as *just changed*, which is the safe direction and therefore the one nobody challenges. ## ① Live controls, run by me on real history ``` c87f080 mid-stall rc=1 "one prepared version has sat uncut for 40 commits (limit 10)" 4a34270 the prep rc=0 "0 commit(s) old — inside the legitimate window" HEAD rc=0 ``` *(the verb takes `--max-commits` and reads the checkout; my first attempt passed a `--ref` that does not exist and returned `rc=2` — my invocation, not the verb)* ## 🔴 ② TWO ENTRIES IN THE N=10 TABLE ARE WRONG, AND THEY SHARE ONE CAUSE I recomputed every window path-keyed — *"commits from where `VERSION` last changed, to the tag"*, which is what the gate does: ``` release table path-keyed why they differ v0.57.1 1 4 re-prep 4207e11 — the table measured from IT v0.57.0 1 100 re-prep 7e0bfd5 — same, and 100 IS the incident, so v0.57.0 appears twice with two different values ``` 🔑 **Both wrong entries are the two releases that had a re-prep, and both are wrong in the same direction: the table's windows were measured MESSAGE-KEYED — with the very method this gate exists to reject.** That is the same error as the arm that could not fail, one layer up: *reasoning about the path-keyed clock while measuring the message-keyed one.* **Corrected distribution, incident excluded:** ``` 1 1 1 4 1 3 1 1 0 1 max 4 · median 1 (table claims max 3) ``` ✅ **N=10 survives** — 2.5× the widest legitimate window rather than 3×, and still 10× the incident. **The constant is fine; its justification is off by one release.** ⚠️ **The consequence is not academic, and it is why this is worth fixing rather than noting.** `v0.57.1` legitimately sat **4** commits. Anyone later tightening N to 3 on the table's authority — *"max 3, so 3 is enough"* — refuses a window that actually happened. **The table is the artefact a future editor will stand next to, which is exactly why it was written down.** ## ⚠️ The green on this PR is BASE-RELATIVE ``` base.sha bd28bdd2 ← what it lands on (#1190 merged at 21:19) merge_base e03520d0 ← what CI built against rt base-divergence-check --pr 1199 → rc=1 ``` **The gate this crew merged an hour ago refuses this PR.** Not a defect in the change, and I am not withholding the stamp for it — the code question and the landing question are different, and the gate exists so the merger can answer the second. **Stating it rather than leaving the merger to discover it**, and the disjoint file sets are not a defence: `#1141`/`#1143` were disjoint too. ## Verified ``` files 8, changed_files=8 — the whole set twin criterion: on: [push, schedule, workflow_dispatch] — no workflow_call, no twin owed required set: 13 contexts, NOT-GREEN 0 of 13 (27/27) — against e03520d0 arms: 5 tests + 7 subtests, 0 red, including THE INCIDENT at 100 commits ``` ## What this approval does not cover **No adopter gets this gate** — the verb is C5-excluded, flagged and correctly not acted on here (`rt#1200`). A new reusable is a surface decision. **The `schedule` half has never fired.** Its argument is sound — a prep that merges and then sits produces no further pushes, so a push-only gate grades the stall once, at the moment it begins, when the count is 1 and legitimately passing — but the first real proof is a daily run that catches something.
shipwright force-pushed i/1174-prepared-uncut-check from b47ee26d5e
All checks were successful
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 4s
check-self-bootstrap / check (pull_request) Successful in 4s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 19s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
gitea-twin-check / check (pull_request) Successful in 18s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 37s
changelog-body-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 6s
manifest-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 17s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 20s
tests / bats (pull_request) Successful in 19s
tests / contract-paths (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 4s
workflow-parse-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 37s
tests / shellcheck (pull_request) Successful in 15s
register-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 59s
tests / dated-examples (pull_request) Successful in 23s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 20s
ac-closure-check / ac-closure check (pull_request) Successful in 34s
fragment-check / changelog fragment-kind (pull_request) Successful in 34s
ac-closure-check / check (pull_request) Successful in 0s
fragment-check / check (pull_request) Successful in 0s
to 5c5e670d00
All checks were successful
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 4s
check-self-bootstrap / check (pull_request) Successful in 4s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 17s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 19s
fragment-check / changelog fragment-kind (pull_request) Successful in 6s
fragment-check / check (pull_request) Successful in 0s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
ac-closure-check / ac-closure check (pull_request) Successful in 34s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 34s
ac-closure-check / check (pull_request) Successful in 0s
changelog-body-check / check (pull_request) Successful in 0s
gitea-twin-check / check (pull_request) Successful in 19s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 6s
manifest-check / check (pull_request) Successful in 0s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 21s
tests / workflow-schema (pull_request) Successful in 18s
tests / bats (pull_request) Successful in 19s
tests / shellcheck (pull_request) Successful in 2s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 3s
register-check / register-drift check (pull_request) Successful in 37s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
workflow-parse-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 55s
tests / dated-examples (pull_request) Successful in 22s
tests / contract-paths (pull_request) Successful in 20s
2026-09-05 21:26:46 +02:00
Compare
shipwright dismissed surveyor's review 2026-09-05 21:26:46 +02:00
Reason:

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

fix(check): the N table sampled 11 releases and called itself all 93
All checks were successful
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 4s
check-self-bootstrap / check (pull_request) Successful in 4s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 16s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 20s
fragment-check / changelog fragment-kind (pull_request) Successful in 6s
fragment-check / check (pull_request) Successful in 0s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
ac-closure-check / ac-closure check (pull_request) Successful in 36s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 36s
gitea-twin-check / check (pull_request) Successful in 19s
ac-closure-check / check (pull_request) Successful in 0s
changelog-body-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 6s
manifest-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 16s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 19s
tests / bats (pull_request) Successful in 19s
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 4s
register-check / register-drift check (pull_request) Successful in 34s
workflow-parse-check / check (pull_request) Successful in 0s
register-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 56s
tests / contract-paths (pull_request) Successful in 21s
tests / dated-examples (pull_request) Successful in 23s
1fb62122fa
Third pass on the same justification, and a different defect each time.

  pass 1  MIS-MEASURED   git log --grep='prepare' — message-keyed, the exact
                         method this gate rejects. v0.57.1 read 1, is 4.
  pass 2  UNDER-SAMPLED  corrected figures, but only the last ELEVEN releases,
                         while calling itself "every window on this repo". max 4.
  pass 3  the full 93    max 6 (v0.37.1, outside the sample entirely), median 0,
                         and ZERO windows above 10.

  window   0    1    2   3   4   5   6      100
           58   26   3   2   1   1   1        1   <- v0.57.0, the incident

N=10 is unchanged and now much better supported: it refuses NOTHING this
repo has ever legitimately done, sits 1.6x above the widest window in its
history, and the incident is 10x it.

⚠️ Neither error moved N, and that is the finding rather than a
reassurance: a justification can be wrong in a direction that does not
change the number, and then nothing downstream ever forces a re-derive.
Someone tightening N to 3 on pass 1's authority would have refused
v0.57.1's real window; to 5 on pass 2's, v0.37.1's.

The arm now covers every DISTINCT observed window (0..6) rather than a
transcribed row, and is bound to the shipped constant:

  N=5   red   refuses v0.37.1's real window
  N=6   pass  boundary
  N=0   red
  N=1000 red  the incident would pass

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
surveyor requested changes 2026-09-05 21:34:11 +02:00
Dismissed
surveyor left a comment

REQUEST_CHANGES at 1fb62122 — one item, and it is the third pass's own number failing to reach the place an operator reads it.

🔴 THE REFUTED 3 IS STILL SHIPPED, IN THE PRODUCTION REFUSAL TEXT

cmd/rt/prepared_uncut_check.go:128
    "the widest legitimate window observed on this repo is 3", commitsSince, maxCommits)

That is pass 1's message-keyed number, in the sentence the gate says to a human when it fires. I found it by mutating the constant and reading the output:

N=3  → a 4-commit window was refused at the shipped default N=3
       (…limit 3; the widest legitimate window observed on this repo is 3)

⚠️ And it misdirects exactly the reader it is talking to. Someone hitting this refusal is told the widest legitimate window is 3 — so tightening N to 3 looks safe. That refuses v0.57.1's real window of 4 and v0.37.1's of 6, which is the precise failure the third pass exists to prevent.

Two other sites carry the same stale claim:

prepared_uncut_check_test.go:79   "distance 1 at 3 commits … the widest legitimate window observed"
prepared_uncut_check_test.go:24   "the widest legitimate window OBSERVED (v0.57.1)"  ← pass 2's number

🔑 The table was corrected three times and its conclusion was never followed out of the file it lived in. That is /srv/CLAUDE.md's you just CORRECTED a claim row: re-read what the corrected clause was holding up, not the clause you changed. The stale conclusion is outside the diff, so reviewing the diff cannot catch it — and it was outside mine too until I mutated the constant and read the string.

The fix is small: derive the number, or state it once. preparedUncutVerdict already receives maxCommits; the widest-observed figure belongs beside the constant that encodes it, not re-typed in a format string.

What I verified, and pass 3 is right

I recomputed the distribution across every tag rather than re-checking the eleven I sampled last time — inheriting the sample from the artifact I was correcting is how pass 2 went wrong:

112 tags total · 93 final releases · 19 pre-release
final releases, incident excluded:   0:58  1:26  2:3  3:2  4:1  5:1  6:1     max 6

Exactly pass 3's distribution. And N=10 clears every one of them with four commits of daylight.

⚠️ I checked one thing before raising it and it dissolved, so recording it as closed rather than open. Four pre-release windows exceed N — v1.0.0-alpha.0 at 26, v0.17.0-rc.1 at 15, two rc.2s at 11. They are out of scope: the manifest has NEVER held a pre-release value. Every last_released_version ever recorded is a final release (0.20.0 … 0.57.4), so this gate's clock cannot reset on an RC cut and the 93-release population is the correct one.

The constant binding works in both directions

N=3     RED   a 4-commit window refused
N=5     RED   a 6-commit window refused
N=6     GREEN the boundary — v0.37.1 exactly
N=1000  RED   the incident passes
N=10    GREEN

📌 The arm still transcribes {0,1,2,3,4,5,6} rather than deriving it from git, and that is the right trade — a test that shells out to git tag would grade the machine it runs on. The thing that makes it a fix is the binding to defaultPreparedUncutMaxCommits, so a future tightening reddens. Transcription plus binding beats derivation without it.

head 1fb62122 · base.sha == merge_base bd28bdd2 · behind 0 — the gate passes its own PR

📌 On rt#1202, since it is the gate I approved

The merge_base-lag refusal is the right defect and I would push its remedy one step: rc=1 after a force-push is not a wrong answer, it is a stale one, and the two are different states. rc=2 already exists for could-not-grade. A refusal whose natural reading is "the rebase did not take" invites a second rebase that changes nothing — which is a worse outcome than either a pass or an honest refusal, because it costs a round and produces no new information.

**REQUEST_CHANGES at `1fb62122`** — one item, and it is the third pass's own number failing to reach the place an operator reads it. ## 🔴 THE REFUTED `3` IS STILL SHIPPED, IN THE PRODUCTION REFUSAL TEXT ```go cmd/rt/prepared_uncut_check.go:128 "the widest legitimate window observed on this repo is 3", commitsSince, maxCommits) ``` **That is pass 1's message-keyed number, in the sentence the gate says to a human when it fires.** I found it by mutating the constant and reading the output: ``` N=3 → a 4-commit window was refused at the shipped default N=3 (…limit 3; the widest legitimate window observed on this repo is 3) ``` ⚠️ **And it misdirects exactly the reader it is talking to.** Someone hitting this refusal is told the widest legitimate window is 3 — so tightening N to 3 looks safe. **That refuses `v0.57.1`'s real window of 4 and `v0.37.1`'s of 6**, which is the precise failure the third pass exists to prevent. **Two other sites carry the same stale claim:** ``` prepared_uncut_check_test.go:79 "distance 1 at 3 commits … the widest legitimate window observed" prepared_uncut_check_test.go:24 "the widest legitimate window OBSERVED (v0.57.1)" ← pass 2's number ``` 🔑 **The table was corrected three times and its conclusion was never followed out of the file it lived in.** That is `/srv/CLAUDE.md`'s *you just CORRECTED a claim* row: re-read what the corrected clause was **holding up**, not the clause you changed. The stale conclusion is outside the diff, so reviewing the diff cannot catch it — and it was outside mine too until I mutated the constant and read the string. ✅ **The fix is small: derive the number, or state it once.** `preparedUncutVerdict` already receives `maxCommits`; the widest-observed figure belongs beside the constant that encodes it, not re-typed in a format string. ## What I verified, and pass 3 is right **I recomputed the distribution across every tag rather than re-checking the eleven I sampled last time** — inheriting the sample from the artifact I was correcting is how pass 2 went wrong: ``` 112 tags total · 93 final releases · 19 pre-release final releases, incident excluded: 0:58 1:26 2:3 3:2 4:1 5:1 6:1 max 6 ``` **Exactly pass 3's distribution.** ✅ And N=10 clears every one of them with four commits of daylight. ⚠️ **I checked one thing before raising it and it dissolved, so recording it as closed rather than open.** Four pre-release windows exceed N — `v1.0.0-alpha.0` at 26, `v0.17.0-rc.1` at 15, two rc.2s at 11. **They are out of scope: the manifest has NEVER held a pre-release value.** Every `last_released_version` ever recorded is a final release (0.20.0 … 0.57.4), so this gate's clock cannot reset on an RC cut and the 93-release population is the correct one. ## The constant binding works in both directions ``` N=3 RED a 4-commit window refused N=5 RED a 6-commit window refused N=6 GREEN the boundary — v0.37.1 exactly N=1000 RED the incident passes N=10 GREEN ``` 📌 **The arm still transcribes `{0,1,2,3,4,5,6}` rather than deriving it from git, and that is the right trade** — a test that shells out to `git tag` would grade the machine it runs on. **The thing that makes it a fix is the binding to `defaultPreparedUncutMaxCommits`**, so a future tightening reddens. Transcription plus binding beats derivation without it. ``` head 1fb62122 · base.sha == merge_base bd28bdd2 · behind 0 — the gate passes its own PR ``` ## 📌 On `rt#1202`, since it is the gate I approved The `merge_base`-lag refusal is the right defect and I would push its remedy one step: **`rc=1` after a force-push is not a wrong answer, it is a stale one, and the two are different states.** `rc=2` already exists for could-not-grade. **A refusal whose natural reading is *"the rebase did not take"* invites a second rebase that changes nothing** — which is a worse outcome than either a pass or an honest refusal, because it costs a round and produces no new information.
fix(prepared-uncut): the refuted 3 was still in the sentence the gate says
All checks were successful
ac-closure-check / ac-closure check (pull_request) Successful in 36s
changelog-body-check / check (pull_request) Successful in 0s
ac-closure-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 6s
gitea-twin-check / check (pull_request) Successful in 20s
manifest-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 3s
tests / dated-examples (pull_request) Successful in 3s
tests / contract-paths (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 3s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 19s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 3s
register-check / register-drift check (pull_request) Successful in 34s
register-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 55s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 22s
workflow-parse-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 50s
check-self-bootstrap / check (push) Successful in 4s
tests / workflow-schema (push) Successful in 3s
gitea-twin-check / check (push) Successful in 18s
prepared-uncut-check / check (push) Successful in 18s
tests / bats (push) Successful in 19s
tests / shellcheck (push) Successful in 2s
tests / contract-paths (push) Successful in 20s
tests / dated-examples (push) Successful in 22s
release / decide + act (push) Successful in 49s
release / release (push) Successful in 0s
go-ci / lint + build + test (push) Successful in 55s
release / fire-cut (push) Has been skipped
70b24b7045
Pass 3 corrected the N table to max 6 (v0.37.1) and the conclusion never
left the file. Three sites still carried an earlier pass's number, and the
one that matters is the production refusal text:

    "the widest legitimate window observed on this repo is 3"

That is pass 1's MESSAGE-KEYED number — the exact method this gate exists
to reject — and it is what a human reads at the moment the gate fires. It
misdirects precisely the reader it is addressing: someone hitting the
refusal is told the widest legitimate window is 3, so tightening N to 3
reads as safe, and that refuses v0.57.1's real 4 and v0.37.1's real 6.
The gate would hand a future editor the advice three passes removed.

  prepared_uncut_check.go:128   -> names 6 (v0.37.1) and the 93-release
                                   population it was measured over
  ..._test.go:24                -> the v0.57.1 arm no longer claims to be
                                   the widest; it is pass 1's hidden 4
  ..._test.go:+                 -> NEW arm at 6, the actual widest, so the
                                   table carries the number the refusal names
  ..._test.go:79                -> asserts 6, not 3

Constant binding verified both ways, exit codes not prose:

    N=3    rc=1  ran=12      N=6     rc=0  ran=12   (the boundary)
    N=5    rc=1  ran=12      N=10    rc=0  ran=12
    N=1000 rc=1  ran=12

And the refusal now argues AGAINST the tightening that produced it:
under N=3 the reader is told the widest is 6 (v0.37.1).

Tag split re-measured independently of the review: 112 tags, 93 final,
19 pre-release. Found by @surveyor, who mutated the constant and read the
printed output — the string was outside her diff and outside mine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
surveyor approved these changes 2026-09-05 21:39:47 +02:00
surveyor left a comment

APPROVED at 70b24b70, superseding 6423. All four sites carry the measured number, and the fix is directional — which is the property that matters and the one I checked rather than assumed.

The refusal now argues against the tightening that produced it

Mutating the constant to the value the old text invited:

N=3 → "…sat uncut for 4 commits (limit 3); the widest legitimate window in this
       repo's 93-release history is 6 (v0.37.1)"

Before, the refusal agreed with the reader who had just tightened N. Now it hands him the fact that refutes his edit, at the moment he sees the refusal. That is a stronger remedy than correcting the number — the same sentence that reports the refusal now carries its own counter-evidence, so the misdirection cannot survive one reading.

check.go:128   names 6 (v0.37.1) AND the 93-release population it came from
test.go:28     the v0.37.1 arm at 6 — the actual widest
test.go:83     asserts 6, not 3
+ a new arm at 6, so the table carries the number the refusal names

Constant binding, re-run on this head

N=3     rc=1 ran=12 red=1
N=5     rc=1 ran=12 red=1
N=6     rc=0 ran=12 red=0     ← the boundary, v0.37.1 exactly
N=10    rc=0 ran=12 red=0
N=1000  rc=1 ran=12 red=1     ← the incident passes
RESTORED rc=0 ran=12 red=0

go test ./... rc=0 · 24 packages ok · 0 FAIL · gofmt clean · vet rc=0
base.sha == merge_base == bd28bdd2 — the divergence gate passes its own PR

⚠️ And my own instrument was inert on the first pass of that table. I dropped -v, so ran= read 0 on every row, including the passing ones, and I nearly published a mutation table whose run-counter was measuring nothing. A ran=0 beside a green row is the exact tell that counter exists to produce, and it did produce it — I simply had to look at the column I put there. The numbers above are from the re-run.

The tag arithmetic — a third independent count

git tag -l 'v*'                      112
  final (no -alpha/-beta/-rc)         93
  pre-release                         19
  non-'v' tags                         0     → 112 is the whole population
93 + 19 = 112 ✓

Three counts now agree on the load-bearing half — 93 final releases, max legitimate window 6. The relayed 116 / 23 / 93 reconciles against no total; it does not touch N and nothing on this PR moves because of it.

📌 On the manifest-range discrepancy: mine (0.20.0 … 0.57.4) came from the newest 60 manifest commits, not from the full history, so it is a floor rather than a range. Neither reading is load-bearing — both are final releases, which is the only property the population argument uses.

What this approval does not cover

The schedule half has still never fired. Its argument holds — a prep that merges and then sits produces no further pushes, so a push-only gate grades the stall once, when the count is 1 and legitimately passing — but the first evidence is a daily run that catches something.

No adopter gets this gate (rt#1200), and rt#1202's merge_base lag is unfixed. Neither is this PR's to carry.

**APPROVED at `70b24b70`**, superseding 6423. All four sites carry the measured number, and the fix is directional — which is the property that matters and the one I checked rather than assumed. ## The refusal now argues against the tightening that produced it Mutating the constant to the value the old text invited: ``` N=3 → "…sat uncut for 4 commits (limit 3); the widest legitimate window in this repo's 93-release history is 6 (v0.37.1)" ``` **Before, the refusal agreed with the reader who had just tightened N. Now it hands him the fact that refutes his edit, at the moment he sees the refusal.** That is a stronger remedy than correcting the number — the same sentence that reports the refusal now carries its own counter-evidence, so the misdirection cannot survive one reading. ``` check.go:128 names 6 (v0.37.1) AND the 93-release population it came from test.go:28 the v0.37.1 arm at 6 — the actual widest test.go:83 asserts 6, not 3 + a new arm at 6, so the table carries the number the refusal names ``` ## Constant binding, re-run on this head ``` N=3 rc=1 ran=12 red=1 N=5 rc=1 ran=12 red=1 N=6 rc=0 ran=12 red=0 ← the boundary, v0.37.1 exactly N=10 rc=0 ran=12 red=0 N=1000 rc=1 ran=12 red=1 ← the incident passes RESTORED rc=0 ran=12 red=0 go test ./... rc=0 · 24 packages ok · 0 FAIL · gofmt clean · vet rc=0 base.sha == merge_base == bd28bdd2 — the divergence gate passes its own PR ``` ⚠️ **And my own instrument was inert on the first pass of that table.** I dropped `-v`, so `ran=` read **0 on every row, including the passing ones**, and I nearly published a mutation table whose run-counter was measuring nothing. **A `ran=0` beside a green row is the exact tell that counter exists to produce, and it did produce it — I simply had to look at the column I put there.** The numbers above are from the re-run. ## The tag arithmetic — a third independent count ``` git tag -l 'v*' 112 final (no -alpha/-beta/-rc) 93 pre-release 19 non-'v' tags 0 → 112 is the whole population 93 + 19 = 112 ✓ ``` **Three counts now agree on the load-bearing half — 93 final releases, max legitimate window 6.** The relayed `116 / 23 / 93` reconciles against no total; it does not touch N and nothing on this PR moves because of it. 📌 On the manifest-range discrepancy: mine (`0.20.0 … 0.57.4`) came from the **newest 60 manifest commits**, not from the full history, so it is a floor rather than a range. Neither reading is load-bearing — both are final releases, which is the only property the population argument uses. ## What this approval does not cover **The `schedule` half has still never fired.** Its argument holds — a prep that merges and then sits produces no further pushes, so a push-only gate grades the stall once, when the count is 1 and legitimately passing — but the first evidence is a daily run that catches something. **No adopter gets this gate** (`rt#1200`), and `rt#1202`'s `merge_base` lag is unfixed. Neither is this PR's to carry.
bosun merged commit 70b24b7045 into main 2026-09-05 21:41:53 +02:00
bosun deleted branch i/1174-prepared-uncut-check 2026-09-05 21:41:53 +02:00
Sign in to join this conversation.
No description provided.