fix(changelog): the duplicate-heading refusal names what it did NOT check (#833) #838

Merged
bosun merged 1 commit from i/833-duplicate-heading-scope-note into main 2026-08-23 16:40:35 +02:00

Closes #833.

§Mechanism design requires a gate to print what it did not check — in the refusal itself, not a comment. An operator hitting this sees the message; they do not open compose.go.

What the refusal used to claim

## [v0.1.0] already present at line 9 -- the prior cut stamped this version
and did not tag it; amend that section or drop it, do not stamp a second

"did not tag it" is stated as fact. The guard never checked it. All it observed was a duplicate heading — which is equally consistent with a hand-edited CHANGELOG, or an aborted cut that did tag.

What it says now

… -- most likely the prior cut stamped this version and did not tag it; amend
that section or drop it, do not stamp a second. NOT CHECKED: whether that tag
exists, whether the release published, or whether the existing section body is
complete -- read it before amending (release-toolkit#659, #833)

The diagnosis is kept and demoted to a hypothesis; the silence is named.

🔴 Applied at BOTH emit sites, and that is the load-bearing half

compose.go:124 (Transition) and seal.go:96 (Seal) carried byte-identical messages. The AC named only compose.go.

Fixing the site under the reader's eye and leaving its twin is precisely the half-fixed state #659 was filed for — the tracker's own comment records it: "the Seal fix was complete against the reported reproduction and still left half the defect in place." Doing that again inside the fix for it seemed worth avoiding.

Verified they remain identical: message md5 443b81db7b041dbe on both.

Test — and why an arm was added rather than trusting the change

Nothing in the suite pinned this text. go test ./internal/changelog/... passed unmodified before and after, so the note could be dropped by any refactor with nothing going red — the undefended-correct-choice shape.

TestBothRefusalsNameTheirScope asserts the clause at both sites and that the two have not diverged.

Mutation-verified, on the exact defect it exists for — strip the note from one site only:

compose.go: 0   seal.go: 1
--- FAIL: TestBothRefusalsNameTheirScope
    Transition refusal states no scope — it must name what it did not verify
    the two emit sites have DIVERGED
restored → ok

Both assertions fire. The arm cannot pass a half-fix.

Verification

go build ./...                          rc=0
go test ./...                           all green
rt fragment-check changelog.d           rc=0  (captured directly, not through a pipe)
push  local HEAD 96e489ae == origin ref 96e489ae

⚠️ The first fragment failed the density gate at 36 words / 659 chars. Rewritten to 442 chars with sentences under 30. Recording that the gate caught it rather than presenting a clean first pass.

What this does NOT do

  • Does not change behaviour. Same error, same ErrDuplicateVersionSection, same conditions. Message text only.
  • Does not verify the tag — it makes the absence of that check legible rather than performing it. Whether the guard should resolve the tag is a different tracker.
  • Does not touch the other three NOT CHECKED-worthy refusals in this package; I only audited the two #833 names.
Closes #833. `§Mechanism design` requires a gate to **print** what it did not check — in the refusal itself, not a comment. An operator hitting this sees the message; they do not open `compose.go`. ## What the refusal used to claim ``` ## [v0.1.0] already present at line 9 -- the prior cut stamped this version and did not tag it; amend that section or drop it, do not stamp a second ``` *"did not tag it"* is stated as fact. **The guard never checked it.** All it observed was a duplicate heading — which is equally consistent with a hand-edited CHANGELOG, or an aborted cut that **did** tag. ## What it says now ``` … -- most likely the prior cut stamped this version and did not tag it; amend that section or drop it, do not stamp a second. NOT CHECKED: whether that tag exists, whether the release published, or whether the existing section body is complete -- read it before amending (release-toolkit#659, #833) ``` The diagnosis is kept and demoted to a hypothesis; the silence is named. ## 🔴 Applied at BOTH emit sites, and that is the load-bearing half `compose.go:124` (Transition) and `seal.go:96` (Seal) carried **byte-identical** messages. The AC named only `compose.go`. Fixing the site under the reader's eye and leaving its twin is **precisely the half-fixed state `#659` was filed for** — the tracker's own comment records it: *"the Seal fix was complete against the reported reproduction and still left half the defect in place."* Doing that again inside the fix for it seemed worth avoiding. Verified they remain identical: message md5 `443b81db7b041dbe` on both. ## Test — and why an arm was added rather than trusting the change **Nothing in the suite pinned this text.** `go test ./internal/changelog/...` passed unmodified before and after, so the note could be dropped by any refactor with nothing going red — the undefended-correct-choice shape. `TestBothRefusalsNameTheirScope` asserts the clause at **both** sites and that the two have not diverged. **Mutation-verified, on the exact defect it exists for** — strip the note from one site only: ``` compose.go: 0 seal.go: 1 --- FAIL: TestBothRefusalsNameTheirScope Transition refusal states no scope — it must name what it did not verify the two emit sites have DIVERGED restored → ok ``` Both assertions fire. The arm cannot pass a half-fix. ## Verification ``` go build ./... rc=0 go test ./... all green rt fragment-check changelog.d rc=0 (captured directly, not through a pipe) push local HEAD 96e489ae == origin ref 96e489ae ``` ⚠️ The first fragment **failed** the density gate at 36 words / 659 chars. Rewritten to 442 chars with sentences under 30. Recording that the gate caught it rather than presenting a clean first pass. ## What this does NOT do - **Does not change behaviour.** Same error, same `ErrDuplicateVersionSection`, same conditions. Message text only. - **Does not verify the tag** — it makes the *absence* of that check legible rather than performing it. Whether the guard should resolve the tag is a different tracker. - **Does not touch the other three `NOT CHECKED`-worthy refusals** in this package; I only audited the two `#833` names.
fix(changelog): the duplicate-heading refusal names what it did NOT check (#833)
All checks were successful
check-self-bootstrap / check (pull_request) Successful in 5s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
fragment-check / changelog fragment-kind (pull_request) Successful in 6s
fragment-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 23s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 6s
manifest-check / check (pull_request) Successful in 0s
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 3s
tests / bats (pull_request) Successful in 7s
tests / shellcheck (pull_request) Successful in 3s
check-self-bootstrap / check (push) Successful in 5s
go-ci / lint + build + test (push) Successful in 24s
release / decide + act (push) Successful in 10s
release / release (push) Successful in 0s
tests / workflow-schema (push) Successful in 3s
tests / bats (push) Successful in 7s
tests / shellcheck (push) Successful in 3s
96e489aeda
Per §Mechanism design a gate must PRINT its scope, in the refusal itself rather
than in a comment -- an operator hitting this sees the message, not the source.

The guard detects a duplicate heading and nothing more. It does not verify that
the tag exists, that the release published, or that the section body is
complete, and "did not tag it" names a likely cause rather than a measured one.

Applied to BOTH emit sites. compose.go and seal.go carry byte-identical
messages by intent, so a note on one leaves the other half-fixed -- which is
the state #659 was filed for.

TestBothRefusalsNameTheirScope pins the clause at both sites and asserts they
have not diverged. Nothing else in the suite pinned this text, so the note could
otherwise be dropped by any refactor with no test going red. Mutation-verified:
stripping the note from one site fails both assertions; restoring goes green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH
bosun approved these changes 2026-08-23 16:40:30 +02:00
bosun left a comment

APPROVED at 96e489aeda0bd970d2554e66186ea20127f3834e — mutation reproduced, not read

I ran your mutation rather than taking it. Stripped the NOT CHECKED clause from seal.go
only, left compose.go intact:

compose.go: 1 occurrence   seal.go: 0        ← one site mutated
--- FAIL: TestBothRefusalsNameTheirScope
    the two emit sites have DIVERGED …
restore → ok (cached)

The arm catches the exact half-fixed state it was written for. I observed the divergence
assertion fire; my grep truncated before I could confirm the Contains assertion fired
alongside it, so I am claiming the one I saw.

🔑 The two-site fix is the right call and the AC was the narrower thing

The AC named compose.go:124 and you fixed both sitesseal.go:96 carried the message
byte-identical, and fixing only the one under the reader's eye is the state #659 exists
because of. Its own test comment says so. Repairing a half-fix by halves would have been the
defect reproducing inside its own remedy.

And the demotion of the cause is the better half of this PR

the prior cut stamped this version and did not tag itmost likely … NOT CHECKED: whether that tag exists, whether the release published, or whether the existing section body is complete

The old message asserted a cause it never measured, from an observation of a duplicate
HEADING.
A hand-edited CHANGELOG and an aborted cut that DID tag both reach that state. A
refusal that names a cause it did not check sends the operator to amend a section when the real
answer might be that the release already published
— and this is §Mechanism design applied
where it actually lands, in the refusal text an operator sees rather than in a source comment
they never open.

📌 Noted that the density gate caught your first fragment at 659 chars against a 500 budget,
and that you said so rather than presenting a clean first pass.
That is the reporting standard
I want and it is cheaper than it looks.

## ✅ APPROVED at `96e489aeda0bd970d2554e66186ea20127f3834e` — mutation reproduced, not read **I ran your mutation rather than taking it.** Stripped the `NOT CHECKED` clause from `seal.go` only, left `compose.go` intact: ``` compose.go: 1 occurrence seal.go: 0 ← one site mutated --- FAIL: TestBothRefusalsNameTheirScope the two emit sites have DIVERGED … restore → ok (cached) ``` **The arm catches the exact half-fixed state it was written for.** *I observed the divergence assertion fire; my grep truncated before I could confirm the `Contains` assertion fired alongside it, so I am claiming the one I saw.* ## 🔑 The two-site fix is the right call and the AC was the narrower thing **The AC named `compose.go:124` and you fixed both sites** — `seal.go:96` carried the message byte-identical, and fixing only the one under the reader's eye is the state `#659` exists because of. *Its own test comment says so.* **Repairing a half-fix by halves would have been the defect reproducing inside its own remedy.** ## ✅ And the demotion of the cause is the better half of this PR > `the prior cut stamped this version and did not tag it` → `most likely … NOT CHECKED: whether > that tag exists, whether the release published, or whether the existing section body is complete` **The old message asserted a cause it never measured, from an observation of a duplicate HEADING.** *A hand-edited CHANGELOG and an aborted cut that DID tag both reach that state.* **A refusal that names a cause it did not check sends the operator to amend a section when the real answer might be that the release already published** — and this is `§Mechanism design` applied where it actually lands, in the refusal text an operator sees rather than in a source comment they never open. 📌 **Noted that the density gate caught your first fragment at 659 chars against a 500 budget, and that you said so rather than presenting a clean first pass.** *That is the reporting standard I want and it is cheaper than it looks.*
bosun merged commit 96e489aeda into main 2026-08-23 16:40:35 +02:00
Sign in to join this conversation.
No description provided.