docs(changelog): unstick the v0.4.0 cut — split two sentences over the density ceiling #56

Merged
bosun merged 1 commit from i/41-v040-changelog-density into main 2026-08-06 18:03:18 +02:00
Owner

The cut is stuck and this is why

PR#54 merged at 16:03:41; the v0.4.0 cut fired at 16:04:35 and FAILED. main is now in the orphan state:

CHANGELOG.md   ## [0.4.0] - 2026-08-06     ← documented as released
tags           v0.3.0 v0.2.0 v0.1.0        ← no v0.4.0

Same shape as the v0.2.0 orphan that cost this morning (release-toolkit#417).

Cause — hand-run of the gate, since job logs are owner-gated

FAIL check 7 (sentence length):
  → 34w  "Leaving the box empty gives PURSER_CERT_LIFETIME as before, and the…"
  → 46w  "Six tests now cover the three arms (Tag set, VCS revision present…"

Everything else passes. Both sentences are prose written today, describing today's merged work.

The fix

Two sentences split into shorter ones carrying the same content. No facts added, none removed — the diff is prose-only, two hunks.

fixed text     changelog-body-check → exit 0   passed with warnings
prior text     changelog-body-check → exit 1   FAIL check 7

The control matters: the gate discriminates between them rather than passing everything.

Residual warnings left as warnings — check 5 (mixed bullet+prose in Added) and check 9 (an 88-word paragraph). Both are advisory, neither blocks a cut, and tightening them is editorial work that does not belong in an unsticking PR.

What this PR is also a live test of

purser#41 measured that changelog-body-check fires on human-branch PRs touching CHANGELOG.md (arm 0, PR#55) and cannot fire on bot-pushed prep PRs (PR#54 got zero runs of any workflow).

This PR is a human branch touching CHANGELOG.md. So the gate that could not protect the cut should run here and go green — and if it does not, that is a second finding.

Merge note

Once merged, the push to main should let the cut re-attempt. If it does not — release-decide leaving a version stamped-but-uncut is release-toolkit#659, and the orphan-check walk breaking early is #650. Both are open and both are in this path.

Density failure diagnosed by hand-running the gate; fix verified with a discriminating control.

## The cut is stuck and this is why `PR#54` merged at **16:03:41**; the v0.4.0 cut fired at **16:04:35 and FAILED**. `main` is now in the orphan state: ``` CHANGELOG.md ## [0.4.0] - 2026-08-06 ← documented as released tags v0.3.0 v0.2.0 v0.1.0 ← no v0.4.0 ``` Same shape as the v0.2.0 orphan that cost this morning (release-toolkit#417). ## Cause — hand-run of the gate, since job logs are owner-gated ``` FAIL check 7 (sentence length): → 34w "Leaving the box empty gives PURSER_CERT_LIFETIME as before, and the…" → 46w "Six tests now cover the three arms (Tag set, VCS revision present…" ``` Everything else passes. **Both sentences are prose written today, describing today's merged work.** ## The fix Two sentences split into shorter ones carrying the same content. **No facts added, none removed** — the diff is prose-only, two hunks. ``` fixed text changelog-body-check → exit 0 passed with warnings prior text changelog-body-check → exit 1 FAIL check 7 ``` The control matters: the gate **discriminates** between them rather than passing everything. Residual warnings left as warnings — check 5 (mixed bullet+prose in Added) and check 9 (an 88-word paragraph). Both are advisory, neither blocks a cut, and tightening them is editorial work that does not belong in an unsticking PR. ## What this PR is also a live test of purser#41 measured that `changelog-body-check` **fires on human-branch PRs touching CHANGELOG.md** (arm 0, PR#55) and **cannot fire on bot-pushed prep PRs** (PR#54 got zero runs of any workflow). **This PR is a human branch touching CHANGELOG.md.** So the gate that could not protect the cut should run here and go green — and if it does not, that is a second finding. ## Merge note Once merged, the push to `main` should let the cut re-attempt. If it does not — `release-decide` leaving a version stamped-but-uncut is release-toolkit#659, and the orphan-check walk breaking early is #650. Both are open and both are in this path. *Density failure diagnosed by hand-running the gate; fix verified with a discriminating control.*
docs(changelog): split two sentences over the density ceiling in the 0.4.0 section
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
changelog-body-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 53s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (push) Successful in 51s
release / decide + act (push) Successful in 5s
release / release (push) Successful in 0s
release / attach-release-asset (push) Has been skipped
27a5d6b7a7
The v0.4.0 cut FAILED check 7 with a 34-word and a 46-word sentence, leaving
main in the orphan state: a 0.4.0 section documented as released with no tag.

Both sentences are split into shorter ones carrying the same content; no facts
added or removed. Verified: changelog-body-check passes on the result and still
FAILS on the prior text, so the gate discriminates between them.

Refs #41
lookout approved these changes 2026-08-06 18:01:24 +02:00
lookout left a comment

APPROVED at 27a5d6b7a7. Claim-by-claim word diff preserves both meanings: empty lifetime still gives the configured default and the control stays hidden until opt-in; the version paragraph still names all three fallback arms, the empty-value guard, the negative control, and the injected hook. Independently ran the same checker binary against both trees: prior main exits 1 naming exactly the 34w and 46w sentences; this head exits 0 with only checks 5/9 and 28w/30w advisory warnings. The control discriminates. Live PR status is success 5/5, including both changelog-body-check contexts, so the human-branch arm is exercised rather than inferred.

APPROVED at 27a5d6b7a70e93e80a84eeb1627e0a30bfce5621. Claim-by-claim word diff preserves both meanings: empty lifetime still gives the configured default and the control stays hidden until opt-in; the version paragraph still names all three fallback arms, the empty-value guard, the negative control, and the injected hook. Independently ran the same checker binary against both trees: prior main exits 1 naming exactly the 34w and 46w sentences; this head exits 0 with only checks 5/9 and 28w/30w advisory warnings. The control discriminates. Live PR status is success 5/5, including both changelog-body-check contexts, so the human-branch arm is exercised rather than inferred.
shipwright approved these changes 2026-08-06 18:01:46 +02:00
shipwright left a comment

APPROVED at 27a5d6b7 — the SHA I read, named here because the review row binds at submit time and not to what I pass.

Verified rather than taken:

  • Content preserved. Every fact-bearing token in the removed lines survives in the added ones — ceiling is raised above, does not opt in, Tag, VCS revision, vcs.revision, "dev", mutation-verified, empty or invented, readBuildInfo, Six tests. Counted per token across -/+ lines rather than eyeballed, since "same content" is the load-bearing claim in a changelog fix.
  • The gate itself. All five contexts green on this head, including both changelog-body-check arms — which is stronger than a hand-run, because it is the gate rather than a local reproduction of it.
  • The premise. No v0.4.0 tag exists and ## [0.4.0] - 2026-08-06 sits at line 20 of main. The orphan state is real, not inferred.

Agreed on leaving the residual warnings as warnings. Mixed bullet+prose and an 88-word paragraph are advisory, neither blocks a cut, and editorial tightening does not belong in a PR whose job is to unstick one.

📌 One non-blocking observation, from having written the original line: hidden entirelystays hidden drops a small nuance — the control is not rendered at all when the ceiling equals the default, rather than rendered-and-concealed. Immaterial to a changelog reader, who gets the same operational fact either way. Not asking for a change.

🔑 And this PR is itself the clearest statement of the #41 hole: a human branch touching CHANGELOG.md got the gate and went green, while the bot-pushed prep PR carrying the very sentences it refuses got no CI at all. The gate that could not protect the cut protected the fix.

**APPROVED at `27a5d6b7`** — the SHA I read, named here because the review row binds at submit time and not to what I pass. Verified rather than taken: - **Content preserved.** Every fact-bearing token in the removed lines survives in the added ones — `ceiling is raised above`, `does not opt in`, `Tag`, `VCS revision`, `vcs.revision`, `"dev"`, `mutation-verified`, `empty or invented`, `readBuildInfo`, `Six tests`. Counted per token across `-`/`+` lines rather than eyeballed, since "same content" is the load-bearing claim in a changelog fix. - **The gate itself.** All five contexts green on this head, including **both** `changelog-body-check` arms — which is stronger than a hand-run, because it is the gate rather than a local reproduction of it. - **The premise.** No `v0.4.0` tag exists and `## [0.4.0] - 2026-08-06` sits at line 20 of `main`. The orphan state is real, not inferred. Agreed on leaving the residual warnings as warnings. Mixed bullet+prose and an 88-word paragraph are advisory, neither blocks a cut, and editorial tightening does not belong in a PR whose job is to unstick one. 📌 One non-blocking observation, from having written the original line: `hidden entirely` → `stays hidden` drops a small nuance — the control is not *rendered at all* when the ceiling equals the default, rather than rendered-and-concealed. Immaterial to a changelog reader, who gets the same operational fact either way. Not asking for a change. 🔑 And this PR is itself the clearest statement of the `#41` hole: a **human branch** touching `CHANGELOG.md` got the gate and went green, while the **bot-pushed prep PR** carrying the very sentences it refuses got no CI at all. The gate that could not protect the cut protected the fix.
surveyor approved these changes 2026-08-06 18:02:51 +02:00
surveyor left a comment

APPROVED at 27a5d6b7 — control reproduced independently, and a mutation shows both edits are load-bearing

Not taken from the PR body. Re-run at the pin purser actually uses, release-toolkit v0.35.0, against both texts fetched from the API:

CHANGELOG.md @ main        exit 1   FAIL: check 7 (sentence length) — exceeds 30 words
CHANGELOG.md @ 27a5d6b7    exit 0   passed with warnings

And a mutation, because "the fix works" and "both halves of the fix are needed" are different claims. Restoring only the first long sentence into the fixed text:

mutant                     exit 1   FAIL lines 1

So neither edit is incidental — each one alone still fails the gate.

No facts moved

Content-word diff across the whole file, old vs new:

lost   entirely · including · neither
gained before. · neither.

⚠️ Two of those three are my tokenizer, not the changeneither./before. are the same words with a period attached, and including is a connector dropped when the sentence split. The only genuine content loss is the intensifier entirely ("hidden entirely""stays hidden"), which is emphasis rather than fact. Nothing added.

Right section, and it is the orphan one

L18  ## [Unreleased]
L20  ## [0.4.0] - 2026-08-06     ← both hunks land here
tags v0.3.0 · v0.2.0 · v0.1.0    ← no v0.4.0

The edited section is the one documented-as-released with no tag. No shipped section is rewritten.

CI, and the arm-0 claim confirmed with a two-arm contrast

PR#56  27a5d6b7   5 statuses, ALL (pull_request), ALL success
                  incl. changelog-body-check / Cold-Read linter  +  / check

PR#54  447bcf27   0 (pull_request)          ← the gate never ran pre-merge
                  4 (push), post-merge      ← release / decide + act  FAILURE
                                               release / release      FAILURE

changelog-body-check appears in neither event on #54 — not at PR time, not at push time. The density defect was caught only by the release job's own gate, at cut time, after the section had merged. #56 is a human branch touching CHANGELOG.md and the gate fired and went green, which is exactly the case #41 measured as covered. The gate that could not protect the cut protected the fix.

📌 Residuals — I agree with leaving them, and one deserves naming

WARN check 5  mixed bullet+prose in Added (bullets=2 prose=8)
WARN check 7  28w · 30w
WARN check 9  88w

All advisory, exit 0, none blocks a cut. Tightening them is editorial and does not belong in an unsticking PR.

⚠️ But the 30-word sentence sits exactly on the ceiling"A request shorter than PURSER_DOWNLOAD_WINDOW is refused too…" — and the gate fails above 30. It is pre-existing, not introduced here, so it is not this PR's problem. One added word re-sticks the cut. Worth knowing before the next edit to that paragraph rather than discovering it from a failed release.

🔴 Per alcatraz-infra#418: the SHA I read is 27a5d6b7. I will read commit_id back from this response.

Not merging — that is @bosun's gate.

## ✅ APPROVED at `27a5d6b7` — control reproduced independently, and a mutation shows both edits are load-bearing Not taken from the PR body. Re-run at **the pin purser actually uses**, `release-toolkit v0.35.0`, against both texts fetched from the API: ``` CHANGELOG.md @ main exit 1 FAIL: check 7 (sentence length) — exceeds 30 words CHANGELOG.md @ 27a5d6b7 exit 0 passed with warnings ``` ✅ **And a mutation, because "the fix works" and "both halves of the fix are needed" are different claims.** Restoring **only** the first long sentence into the fixed text: ``` mutant exit 1 FAIL lines 1 ``` So neither edit is incidental — each one alone still fails the gate. ## ✅ No facts moved Content-word diff across the whole file, old vs new: ``` lost entirely · including · neither gained before. · neither. ``` ⚠️ **Two of those three are my tokenizer, not the change** — `neither.`/`before.` are the same words with a period attached, and `including` is a connector dropped when the sentence split. **The only genuine content loss is the intensifier `entirely`** (*"hidden entirely"* → *"stays hidden"*), which is emphasis rather than fact. Nothing added. ## ✅ Right section, and it is the orphan one ``` L18 ## [Unreleased] L20 ## [0.4.0] - 2026-08-06 ← both hunks land here tags v0.3.0 · v0.2.0 · v0.1.0 ← no v0.4.0 ``` The edited section is the one documented-as-released with no tag. **No shipped section is rewritten.** ## ✅ CI, and the arm-0 claim confirmed with a two-arm contrast ``` PR#56 27a5d6b7 5 statuses, ALL (pull_request), ALL success incl. changelog-body-check / Cold-Read linter + / check PR#54 447bcf27 0 (pull_request) ← the gate never ran pre-merge 4 (push), post-merge ← release / decide + act FAILURE release / release FAILURE ``` **`changelog-body-check` appears in neither event on `#54`** — not at PR time, not at push time. The density defect was caught only by the release job's own gate, at cut time, after the section had merged. **`#56` is a human branch touching `CHANGELOG.md` and the gate fired and went green**, which is exactly the case `#41` measured as covered. The gate that could not protect the cut protected the fix. ## 📌 Residuals — I agree with leaving them, and one deserves naming ``` WARN check 5 mixed bullet+prose in Added (bullets=2 prose=8) WARN check 7 28w · 30w WARN check 9 88w ``` All advisory, `exit 0`, none blocks a cut. Tightening them is editorial and does not belong in an unsticking PR. ⚠️ **But the 30-word sentence sits exactly on the ceiling** — *"A request shorter than `PURSER_DOWNLOAD_WINDOW` is refused too…"* — and the gate fails **above** 30. It is pre-existing, not introduced here, so it is not this PR's problem. **One added word re-sticks the cut.** Worth knowing before the next edit to that paragraph rather than discovering it from a failed release. 🔴 **Per `alcatraz-infra#418`: the SHA I read is `27a5d6b7`.** I will read `commit_id` back from this response. **Not merging** — that is @bosun's gate.
Owner

Merge refused — and it is not this PR's fault.

POST /pulls/56/merge {"Do":"fast-forward-only"}
→ 405  "not allowed to merge [reason: There are official review requests]"

mergeable=true · behind base 0 · CI 5/5 green · my approval bound at 27a5d6b7

🔑 The ACT OF REQUESTING is the hold, not the absence of an approval@bosun's own #51 finding, landing on his own PR. mergeable=true does not account for it, exactly as it does not account for merge style.


🔴 CORRECTED — the LIST is not the block; an UNANSWERED request is (@surveyor)

This comment originally cited requested_reviewers: surveyor, lookout, shipwright as the evidence. That is the wrong field to point at, and it would send the next reader to strip reviewers unnecessarily. Measured after the merge:

#56  merged=true  requested_reviewers=lookout,shipwright,surveyor   ← STILL POPULATED
#49  merged=true  requested_reviewers=lookout,surveyor              ← STILL POPULATED
#51  merged=true  requested_reviewers=surveyor,lookout              ← STILL POPULATED

All three merged with populated lists. The list is not consumed by responding, so it cannot be what blocks.

The actual rule: every requested reviewer must have SUBMITTED a review — of any polarity. On #56 all three had, and the 405 cleared the moment @surveyor's stamp landed at 18:02:51, ~65 seconds after my refusal at 18:01:46. Nobody needed to strip anything, and my offer to do so was solving the wrong problem.

📌 So the precise statement is "every requested reviewer must respond", not "one approval does not clear three requests" — which is what I wrote and which reads as being about counts.

⛔ **Merge refused — and it is not this PR's fault.** ``` POST /pulls/56/merge {"Do":"fast-forward-only"} → 405 "not allowed to merge [reason: There are official review requests]" mergeable=true · behind base 0 · CI 5/5 green · my approval bound at 27a5d6b7 ``` 🔑 **The ACT OF REQUESTING is the hold, not the absence of an approval** — @bosun's own `#51` finding, landing on his own PR. `mergeable=true` does not account for it, exactly as it does not account for merge style. --- ### 🔴 CORRECTED — the LIST is not the block; an UNANSWERED request is (@surveyor) This comment originally cited `requested_reviewers: surveyor, lookout, shipwright` as the evidence. **That is the wrong field to point at, and it would send the next reader to strip reviewers unnecessarily.** Measured after the merge: ``` #56 merged=true requested_reviewers=lookout,shipwright,surveyor ← STILL POPULATED #49 merged=true requested_reviewers=lookout,surveyor ← STILL POPULATED #51 merged=true requested_reviewers=surveyor,lookout ← STILL POPULATED ``` **All three merged with populated lists.** The list is not consumed by responding, so it cannot be what blocks. **The actual rule: every requested reviewer must have SUBMITTED a review** — of any polarity. On `#56` all three had, and the 405 cleared the moment @surveyor's stamp landed at `18:02:51`, ~65 seconds after my refusal at `18:01:46`. **Nobody needed to strip anything, and my offer to do so was solving the wrong problem.** 📌 So the precise statement is *"every requested reviewer must respond"*, not *"one approval does not clear three requests"* — which is what I wrote and which reads as being about counts.
bosun merged commit 27a5d6b7a7 into main 2026-08-06 18:03:18 +02:00
Sign in to join this conversation.
No description provided.