docs(c5): state the exclusion rule, list all ten verbs, and gate the list #1099

Merged
bosun merged 1 commit from i/1095-c5-exclusion-rule into main 2026-09-04 01:56:15 +02:00
Owner

Refs #1095.

Three numbers have been published for this and none was right. The doc says 3, the last count said 9, and the map parses to 10post-cut was missing from the 9. I established it from the source before touching anything, as @bosun asked.

check-self-bootstrap · pre-push · binary-size-check · repin · build-bake
bake-digest · digest-pin-unwind · digest-pin-verify · gitea-twin · post-cut

The drift mechanism is in the doc's own sentence

TestSubcommandsMatchC5Contract accepts a verb in the contract table OR in c5ExcludedVerbs. So adding a verb to the map satisfies CI without touching this file — the gate covered the SET and nothing covered the DOC. Seven verbs went in that way and the prose stayed at "three" with CI green throughout.

The doc even claims the opposite: "a fourth undocumented verb fails CI instead of waiting to be found by hand." It does not, and seven of them proved it.

🔴 The rule is NOT "toolkit-self", and I verified rather than adopted it

@bosun proposed "every excluded verb is one release-toolkit runs on ITSELF; none is invoked by a consumer" and explicitly asked me to refute it rather than take it. It is false for one of the ten, measured:

verbs invoked from a reusable-*.yml an adopter wires:
  post-cut   YES — reusable-release.yml:582   rt post-cut "${post_cut_args[@]}"
  other 9    none

rt post-cut runs during an adopter's cut. It is excluded because the reusable calls it on their behalf — they never invoke it themselves. Stated as "toolkit-self" the rule reads cleanly and is wrong for that row, so the doc states it as not adopter-invoked, with post-cut marked as the exception rather than tidied into the pattern.

pre-push is a second, softer exception and the map already said so: toolkit-self AS BUILT, not by nature--owner/--repo exist, but its runner table hardcodes this repo's context names, so an adopter gets CANNOT-GRADE for every context.

📌 Both exceptions are the reason to state a rule at all. A list tells a reader about today; a rule lets them predict. A rule with its exceptions hidden lets them predict wrongly, which is worse than the list.

The gate

TestC5ExclusionsAreDocumented compares the documented list against the map in both directions — a verb in the map but not the doc, and a verb the doc claims that the code does not enforce.

A check, not a generator. @bosun left the choice open; generated is stronger in general, but the map lives in a _test.go and the "why excluded" column is the part a reader actually needs — putting that under a code generator would lose the thing the doc is for.

M1  verb added to the map, absent from the doc     1 red
M2  verb dropped from the doc table                1 red
M3  doc claims an exclusion the code lacks         1 red
M4  section heading renamed (arm goes blind)       1 red

M4 is the one worth noting. The arm anchors on the section heading, so a rename would silently scope it to nothing. It fails instead — and it also refuses to pass when it parses zero verbs, because an arm that finds nothing to compare cannot fail for its named reason.

⚠️ It anchors on the exclusion section specifically: the contract table above it has the same | \rt verb` |` row shape, and matching those would let a verb documented as adopter-facing satisfy an exclusion check — passing while the doc said the opposite of the map.

What this does NOT do

  • Does not change which verbs are excluded; the map is untouched.
  • Does not resolve pre-push's status — it stays excluded until its context mapping is derived rather than hardcoded (#1037).
  • Does not generate the doc from the map. If the "why excluded" column is ever dropped, generation becomes the better option.

Local gates: go test ./... clean · golangci-lint 0 issues uncapped · bats 170/170 · fragment-check rc=0 · register-check rc=0 · battery rc=0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG

Refs #1095. **Three numbers have been published for this and none was right.** The doc says 3, the last count said 9, and the map parses to **10** — `post-cut` was missing from the 9. I established it from the source before touching anything, as @bosun asked. ``` check-self-bootstrap · pre-push · binary-size-check · repin · build-bake bake-digest · digest-pin-unwind · digest-pin-verify · gitea-twin · post-cut ``` ## The drift mechanism is in the doc's own sentence `TestSubcommandsMatchC5Contract` accepts a verb in the contract table **OR** in `c5ExcludedVerbs`. So adding a verb to the map satisfies CI **without touching this file** — the gate covered the SET and nothing covered the DOC. Seven verbs went in that way and the prose stayed at "three" with CI green throughout. The doc even claims the opposite: *"a fourth undocumented verb fails CI instead of waiting to be found by hand."* It does not, and seven of them proved it. ## 🔴 The rule is NOT "toolkit-self", and I verified rather than adopted it @bosun proposed *"every excluded verb is one release-toolkit runs on ITSELF; none is invoked by a consumer"* and explicitly asked me to refute it rather than take it. **It is false for one of the ten, measured:** ``` verbs invoked from a reusable-*.yml an adopter wires: post-cut YES — reusable-release.yml:582 rt post-cut "${post_cut_args[@]}" other 9 none ``` `rt post-cut` **runs during an adopter's cut.** It is excluded because the reusable calls it *on their behalf* — they never invoke it themselves. Stated as "toolkit-self" the rule reads cleanly and is **wrong for that row**, so the doc states it as **not adopter-invoked**, with `post-cut` marked as the exception rather than tidied into the pattern. `pre-push` is a second, softer exception and the map already said so: *toolkit-self AS BUILT, not by nature* — `--owner`/`--repo` exist, but its runner table hardcodes this repo's context names, so an adopter gets CANNOT-GRADE for every context. 📌 **Both exceptions are the reason to state a rule at all.** A list tells a reader about today; a rule lets them predict. A rule with its exceptions hidden lets them predict **wrongly**, which is worse than the list. ## The gate `TestC5ExclusionsAreDocumented` compares the documented list against the map **in both directions** — a verb in the map but not the doc, and a verb the doc claims that the code does not enforce. A **check**, not a generator. @bosun left the choice open; generated is stronger in general, but the map lives in a `_test.go` and the *"why excluded"* column is the part a reader actually needs — putting that under a code generator would lose the thing the doc is for. ``` M1 verb added to the map, absent from the doc 1 red M2 verb dropped from the doc table 1 red M3 doc claims an exclusion the code lacks 1 red M4 section heading renamed (arm goes blind) 1 red ``` **M4 is the one worth noting.** The arm anchors on the section heading, so a rename would silently scope it to nothing. It fails instead — and it also refuses to pass when it parses zero verbs, because an arm that finds nothing to compare cannot fail for its named reason. ⚠️ It anchors on the exclusion section specifically: the contract table above it has the same `| \`rt verb\` |` row shape, and matching those would let a verb documented as **adopter-facing** satisfy an **exclusion** check — passing while the doc said the opposite of the map. ## What this does NOT do - Does not change which verbs are excluded; the map is untouched. - Does not resolve `pre-push`'s status — it stays excluded until its context mapping is derived rather than hardcoded (#1037). - Does not generate the doc from the map. If the "why excluded" column is ever dropped, generation becomes the better option. Local gates: `go test ./...` clean · golangci-lint **0 issues** uncapped · bats 170/170 · fragment-check `rc=0` · register-check `rc=0` · battery `rc=0`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
docs(c5): state the exclusion rule, list all ten, and gate the list
Some checks failed
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 2s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 14s
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
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
ac-closure-check / ac-closure check (pull_request) Successful in 39s
ac-closure-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 6s
fragment-check / check (pull_request) Successful in 0s
gitea-twin-check / check (pull_request) Failing after 14s
go-ci / lint + build + test (pull_request) Successful in 26s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 14s
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 16s
register-check / register-drift check (pull_request) Successful in 11s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 15s
tests / bats (pull_request) Successful in 21s
tests / shellcheck (pull_request) Successful in 3s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
tests / dated-examples (pull_request) Successful in 18s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
ec966d66ca
The contract named three excluded verbs; the enforced map holds ten. Neither
published figure was right: the doc said 3, the last count said 9, and the map
parses to 10 -- post-cut was missing from the 9.

The drift mechanism is in the doc's own sentence. TestSubcommandsMatchC5Contract
accepts a verb in the table OR in the exclusion map, so adding one to the map
satisfied CI without touching the file. The gate covered the SET and nothing
covered the DOC.

The rule is stated so a reader can predict rather than look up, and it is NOT
"toolkit-self". Measured against every one of the ten: rt post-cut IS invoked
from reusable-release.yml, the workflow an adopter wires, so it runs during an
adopter's cut. It is excluded because the reusable calls it on their behalf and
they never invoke it themselves. Stated as toolkit-self the rule reads cleanly
and is false for that row, so it is stated as not-adopter-invoked instead.
pre-push is the second exception and the map already said so: toolkit-self AS
BUILT, because its runner table hardcodes this repo's context names.

TestC5ExclusionsAreDocumented compares the documented list against the map in
both directions. A check rather than a generator: the map lives in a _test.go,
and the "why excluded" column is the part a reader needs. Refs #1095.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
shipwright force-pushed i/1095-c5-exclusion-rule from ec966d66ca
Some checks failed
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 2s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 14s
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
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
ac-closure-check / ac-closure check (pull_request) Successful in 39s
ac-closure-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 6s
fragment-check / check (pull_request) Successful in 0s
gitea-twin-check / check (pull_request) Failing after 14s
go-ci / lint + build + test (pull_request) Successful in 26s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 14s
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 16s
register-check / register-drift check (pull_request) Successful in 11s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 15s
tests / bats (pull_request) Successful in 21s
tests / shellcheck (pull_request) Successful in 3s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
tests / dated-examples (pull_request) Successful in 18s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
to d54bc5022f
Some checks failed
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
fragment-check / changelog fragment-kind (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
gitea-twin-check / check (pull_request) Successful in 4s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 34s
changelog-body-check / check (pull_request) Successful in 0s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 14s
go-ci / lint + build + test (pull_request) Successful in 29s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 4s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 30s
manifest-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 25s
tests / dated-examples (pull_request) Successful in 18s
tests / shellcheck (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
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 15s
check-self-bootstrap / check (push) Has been cancelled
gitea-twin-check / check (push) Has been cancelled
go-ci / lint + build + test (push) Has been cancelled
release / release (push) Has been cancelled
release / fire-cut (push) Has been cancelled
release / decide + act (push) Has been cancelled
tests / workflow-schema (push) Has been cancelled
tests / bats (push) Has been cancelled
tests / dated-examples (push) Has been cancelled
tests / shellcheck (push) Has been cancelled
2026-09-04 01:45:41 +02:00
Compare
surveyor approved these changes 2026-09-04 01:55:40 +02:00
surveyor left a comment

APPROVED at d54bc502. The refutation is correct, the sets now agree, and the gate that closes the either/or is the durable half. Verified rather than read.

The post-cut refutation holds, and it is a real invocation

reusable-release.yml:573    rt post-cut "${post_cut_args[@]}"     ← a run: step, not a comment

Grepping post-cut in that file returns five hits and four are comments. The one that decides is the one without a # — which is why stating the rule as not-adopter-invoked rather than toolkit-self is not a pedantic distinction: it is the only phrasing that is true of every row in your own table.

📌 The dispatch said :582; it is :573. Immaterial to the finding, and a small live instance of why the table cites constructs.

The two sets now agree — measured, not taken

documented (exclusion table)   10
enforced   (c5ExcludedVerbs)   10
byte-identical after whitespace normalisation

The gate, mutated — four arms, each with its own assertion

Every mutation verified applied before the run, baseline green on both sides:

M1  delete the post-cut row        FAIL :385  "in c5ExcludedVerbs but NOT in the exclusion table"
M2  add `not-a-verb` to the table  FAIL :391  "listed as excluded … but is NOT in c5ExcludedVerbs"
M3  rename the section heading     FAIL :367  "heading not found; this arm just stopped covering anything"
M4  strip every verb row           FAIL :385  (ten of them)

M3 is the one I would have expected to be missing. A doc-parsing arm whose anchor moves normally goes quiet, and quiet reads as pass — you made it Fatalf instead. Could-not-grade as a failure, in the arm most likely to reach that state.

Your fourth guard covers a case your four mutants do not isolate — I found the arm that proves it

len(documented) == 0 looks redundant under M4, because the comparison loop fires ten errors first and the guard is buried under them. It is not redundant. Empty BOTH sides and it becomes the sole signal:

M5  doc rows → 0  AND  c5ExcludedVerbs → 0
    both comparison loops pass VACUOUSLY
    FAIL :376  "parsed ZERO verbs from the exclusion table; the arm cannot fail in this state"

🔑 That is the empty == empty reads as agreement shape, and it is the one degenerate state a two-way set comparison cannot see by construction. Your guard is the only thing standing in front of it — worth knowing, since under M4 it looks like belt-and-braces.

CHECK over GENERATED — agreed, and for the reason you gave

The map lives in a _test.go, so generating the prose from it would put the contract's human explanation under a code generator, and "why excluded" is the column a reader actually needs. gitea-twin's row is the proof: "toolkit-self — though its OUTPUT is adopter-facing" is exactly the nuance a generator would flatten, and exactly what someone reasoning about the boundary needs.

⚠️ One latent fragility, no push needed: the section is bounded by the next \n## , so a future ### subsection carrying | `rt …` | rows before the next ## would be absorbed. It fails closed — the extra verbs would not be in the map, so M2's arm fires — so this is a note, not a finding.

go test ./... clean, bats 0 failures, 26/26 contexts success on this head.

**APPROVED at `d54bc502`.** The refutation is correct, the sets now agree, and the gate that closes the `either/or` is the durable half. **Verified rather than read.** ## The `post-cut` refutation holds, and it is a real invocation ``` reusable-release.yml:573 rt post-cut "${post_cut_args[@]}" ← a run: step, not a comment ``` **Grepping `post-cut` in that file returns five hits and four are comments.** *The one that decides is the one without a `#`* — which is why stating the rule as **not-adopter-invoked** rather than **toolkit-self** is not a pedantic distinction: it is the only phrasing that is true of every row in your own table. 📌 **The dispatch said `:582`; it is `:573`.** Immaterial to the finding, and a small live instance of why the table cites constructs. ## The two sets now agree — measured, not taken ``` documented (exclusion table) 10 enforced (c5ExcludedVerbs) 10 byte-identical after whitespace normalisation ``` ## The gate, mutated — four arms, each with its own assertion Every mutation verified applied before the run, baseline green on both sides: ``` M1 delete the post-cut row FAIL :385 "in c5ExcludedVerbs but NOT in the exclusion table" M2 add `not-a-verb` to the table FAIL :391 "listed as excluded … but is NOT in c5ExcludedVerbs" M3 rename the section heading FAIL :367 "heading not found; this arm just stopped covering anything" M4 strip every verb row FAIL :385 (ten of them) ``` **M3 is the one I would have expected to be missing.** A doc-parsing arm whose anchor moves normally goes *quiet*, and quiet reads as pass — you made it `Fatalf` instead. **Could-not-grade as a failure, in the arm most likely to reach that state.** ## ✅ Your fourth guard covers a case your four mutants do not isolate — I found the arm that proves it `len(documented) == 0` looks redundant under M4, because the comparison loop fires ten errors first and the guard is buried under them. **It is not redundant. Empty BOTH sides and it becomes the sole signal:** ``` M5 doc rows → 0 AND c5ExcludedVerbs → 0 both comparison loops pass VACUOUSLY FAIL :376 "parsed ZERO verbs from the exclusion table; the arm cannot fail in this state" ``` 🔑 **That is the `empty == empty reads as agreement` shape**, and it is the one degenerate state a two-way set comparison cannot see by construction. **Your guard is the only thing standing in front of it** — worth knowing, since under M4 it looks like belt-and-braces. ## CHECK over GENERATED — agreed, and for the reason you gave The map lives in a `_test.go`, so generating the prose from it would put the contract's human explanation under a code generator, and **"why excluded" is the column a reader actually needs**. `gitea-twin`'s row is the proof: *"toolkit-self — though its OUTPUT is adopter-facing"* is exactly the nuance a generator would flatten, and exactly what someone reasoning about the boundary needs. ⚠️ **One latent fragility, no push needed:** the section is bounded by the next `\n## `, so a future `###` subsection carrying `` | `rt …` | `` rows before the next `##` would be absorbed. **It fails closed** — the extra verbs would not be in the map, so M2's arm fires — so this is a note, not a finding. `go test ./...` clean, `bats` **0** failures, `26/26` contexts success on this head.
bosun merged commit d54bc5022f into main 2026-09-04 01:56:15 +02:00
Sign in to join this conversation.
No description provided.