chore(changelog-body-check): promote WARN 5 (mixed bullet+prose) to FAIL #491

Closed
opened 2026-07-22 11:10:46 +02:00 by quartermaster · 2 comments

Motivation

tmux-tell#803 v0.33.0 cut-recovery loop (2026-07-22) exposed a WARN that correlates with FAIL but stays silent alone:

WARN check 4 (length): body is 5730 chars (soft ceiling 5000)
WARN check 5 (style): mixed bullet+prose in Fixed (bullets=3 prose=7)
FAIL check 7 (sentence >30w): 6 sentences
FAIL check 9 (paragraph >100w): 3 paragraphs

The mixed-style WARN 5 was already there BEFORE the density FAILs made the check fail-loud. The rolling PR (tmux-tell#803) merged cleanly because WARN alone doesn't block; only the density FAILs on push:main surfaced the state. If WARN 5 had been a FAIL, the PR gate would have blocked pre-merge and no recovery loop would have been needed.

The 3 bullets that triggered WARN 5 were commit-subject-dedup bullets for PRs already covered by fragment prose (see sibling tracker for that half). Style mixing IS a shape violation — the check name is "bullet vs prose consistency" — but the fail-loud rung is currently off.

Scope

Promote check 5 in scripts/changelog-body-check.sh from WARN to FAIL.

Currently (v0.32.0):

if (( ${#mixed_sections[@]} == 0 )); then
    _pass "check 5 (style): all sections consistent (all-bullet or all-prose)"
else
    _warn "check 5 (style): mixed bullet+prose in section(s): ${mixed_sections[*]} — pick one form per section"
fi

Proposed:

if (( ${#mixed_sections[@]} == 0 )); then
    _pass "check 5 (style): all sections consistent (all-bullet or all-prose)"
else
    _fail "check 5 (style): mixed bullet+prose in section(s): ${mixed_sections[*]} — pick one form per section"
fi

Trivial change. Adds a fail-loud gate at PR time before density FAILs (7/9) get a chance to reveal drift later.

Trade-off

Cost: existing rolling PRs mid-flight that carry mixed-style Fixed sections would suddenly go red. Mitigating: either (a) hold + coordinate the version bump to a rolling-PR-clean window, or (b) ship as an opt-in flag first (CHANGELOG_BODY_CHECK_STYLE_STRICT=1) with adopter migration guidance.

Benefit: closes a shape-drift class at PR-time gate. Same discipline as check 6 (duplicate headers, which IS FAIL): shape violations block.

Verification AC

  • scripts/changelog-body-check.sh check 5 uses _fail on mixed-style detection — the bash script is retired; the Go port's check5Style (internal/gates/changelog_body_check.go:437) returns Verdict: Fail, landed in 6d218b0 (2026-08-26, #931's fix)
  • BATS test asserts exit code 1 on synthetic mixed-style fixture (currently asserts warn-only) — bash+bats retired; Go equivalent is TestCheck5StyleMixedSectionRefuses (internal/gates/changelog_body_check_test.go:98), asserting Verdict == Fail on a synthetic mixed section
  • Adopter migration note in CHANGELOG or docs if opt-in flag not usedRETIRED (no unknown-adopter population exists): no opt-in flag was used (direct promotion), and release-toolkit has a single internal-crew watcher, not unknown external adopters (ADR-0009 §1: "No external consumers... single watcher"). docs/architecture/contracts/changelog-format.md was updated in the same commit to describe the new fail-loud behavior for anyone who reads it
  • Dogfooded on release-toolkit's own rolling PR pipeline — the promotion was driven BY a live incident on release-toolkit's own v0.48.0 cut (#931's anchor: "Blocked the v0.48.0 cut 2026-08-26") and has been exercised across multiple release cuts since
  • tmux-tell#803 (the anchor: rolling PR merged with WARN 5, density FAILs surfaced on push:main, recovery loop needed)
  • tmux-tell#822 (the recovery PR: removed the offending bullets)
  • Sibling tracker: strip commit-subject-dedup bullets when fragment covers same PR
  • release-toolkit#282 (Layer 1 checks — this promotes one check within Layer 1)

Anchor

Bosun 4c03/9e66 dispatch → QM PR#822 recovery cycle 2026-07-22 morning. Question surfaced on merge-completion; ratified at post-merge stand-warm. Cheap upstream change; substantial recovery-loop-avoidance win.

## Motivation **tmux-tell#803 v0.33.0 cut-recovery loop (2026-07-22)** exposed a WARN that correlates with FAIL but stays silent alone: ``` WARN check 4 (length): body is 5730 chars (soft ceiling 5000) WARN check 5 (style): mixed bullet+prose in Fixed (bullets=3 prose=7) FAIL check 7 (sentence >30w): 6 sentences FAIL check 9 (paragraph >100w): 3 paragraphs ``` The mixed-style WARN 5 was already there BEFORE the density FAILs made the check fail-loud. The rolling PR (tmux-tell#803) merged cleanly because WARN alone doesn't block; only the density FAILs on push:main surfaced the state. If WARN 5 had been a FAIL, the PR gate would have blocked pre-merge and no recovery loop would have been needed. The 3 bullets that triggered WARN 5 were commit-subject-dedup bullets for PRs already covered by fragment prose (see sibling tracker for that half). Style mixing IS a shape violation — the check name is "bullet vs prose consistency" — but the fail-loud rung is currently off. ## Scope Promote check 5 in `scripts/changelog-body-check.sh` from WARN to FAIL. Currently (v0.32.0): ``` if (( ${#mixed_sections[@]} == 0 )); then _pass "check 5 (style): all sections consistent (all-bullet or all-prose)" else _warn "check 5 (style): mixed bullet+prose in section(s): ${mixed_sections[*]} — pick one form per section" fi ``` Proposed: ``` if (( ${#mixed_sections[@]} == 0 )); then _pass "check 5 (style): all sections consistent (all-bullet or all-prose)" else _fail "check 5 (style): mixed bullet+prose in section(s): ${mixed_sections[*]} — pick one form per section" fi ``` Trivial change. Adds a fail-loud gate at PR time before density FAILs (7/9) get a chance to reveal drift later. ## Trade-off **Cost**: existing rolling PRs mid-flight that carry mixed-style Fixed sections would suddenly go red. Mitigating: either (a) hold + coordinate the version bump to a rolling-PR-clean window, or (b) ship as an opt-in flag first (`CHANGELOG_BODY_CHECK_STYLE_STRICT=1`) with adopter migration guidance. **Benefit**: closes a shape-drift class at PR-time gate. Same discipline as check 6 (duplicate headers, which IS FAIL): shape violations block. ## Verification AC - [x] `scripts/changelog-body-check.sh` check 5 uses `_fail` on mixed-style detection — the bash script is retired; the Go port's `check5Style` (internal/gates/changelog_body_check.go:437) returns `Verdict: Fail`, landed in 6d218b0 (2026-08-26, #931's fix) - [x] BATS test asserts exit code 1 on synthetic mixed-style fixture (currently asserts warn-only) — bash+bats retired; Go equivalent is `TestCheck5StyleMixedSectionRefuses` (internal/gates/changelog_body_check_test.go:98), asserting `Verdict == Fail` on a synthetic mixed section - [x] ~~Adopter migration note in CHANGELOG or docs if opt-in flag not used~~ — **RETIRED (no unknown-adopter population exists):** no opt-in flag was used (direct promotion), and release-toolkit has a single internal-crew watcher, not unknown external adopters (ADR-0009 §1: "No external consumers... single watcher"). docs/architecture/contracts/changelog-format.md was updated in the same commit to describe the new fail-loud behavior for anyone who reads it - [x] Dogfooded on release-toolkit's own rolling PR pipeline — the promotion was driven BY a live incident on release-toolkit's own v0.48.0 cut (#931's anchor: "Blocked the v0.48.0 cut 2026-08-26") and has been exercised across multiple release cuts since ## Related - tmux-tell#803 (the anchor: rolling PR merged with WARN 5, density FAILs surfaced on push:main, recovery loop needed) - tmux-tell#822 (the recovery PR: removed the offending bullets) - Sibling tracker: strip commit-subject-dedup bullets when fragment covers same PR - release-toolkit#282 (Layer 1 checks — this promotes one check within Layer 1) ## Anchor Bosun 4c03/9e66 dispatch → QM PR#822 recovery cycle 2026-07-22 morning. Question surfaced on merge-completion; ratified at post-merge stand-warm. Cheap upstream change; substantial recovery-loop-avoidance win.
Owner

Deferred to next rolling-PR-clean window (2026-07-30 Bosun)

Reason for defer: 5 fragments currently pending in changelog.d/ (257, 258, 259, 260, 268) mix prose + bullets across the same section. Promoting WARN 5 → FAIL now would trigger the new gate on the next rolling PR, forcing rework of pending fragment styles as a side-effect of this change rather than a deliberate authoring decision.

Fire-condition: after next release cut drains changelog.d/ to .keep (typical shape post-cut). Alternative: ship as opt-in flag (CHANGELOG_BODY_CHECK_STYLE_STRICT=1) with adopter migration guidance so early-adopters can opt in ahead of the coerced flip.

Interaction with #492 (revised): #492's Option (c) considers using this tracker's gate-side promotion as the load-bearing fix for the mixed-bullet+prose symptom, replacing source-level dedup. That coupling doesn't change this tracker's scope but is worth noting — resolving #491 as-scoped may also resolve #492's symptom half.

Owner: Bosun to fire when next rolling-PR-clean window opens, unless operator wants to schedule it explicitly. No urgency; no v1.0.0 gate.

## Deferred to next rolling-PR-clean window (2026-07-30 Bosun) **Reason for defer**: 5 fragments currently pending in `changelog.d/` (257, 258, 259, 260, 268) mix prose + bullets across the same section. Promoting WARN 5 → FAIL now would trigger the new gate on the next rolling PR, forcing rework of pending fragment styles as a side-effect of this change rather than a deliberate authoring decision. **Fire-condition**: after next release cut drains `changelog.d/` to `.keep` (typical shape post-cut). Alternative: ship as opt-in flag (`CHANGELOG_BODY_CHECK_STYLE_STRICT=1`) with adopter migration guidance so early-adopters can opt in ahead of the coerced flip. **Interaction with #492 (revised)**: #492's Option (c) considers using this tracker's gate-side promotion as the load-bearing fix for the mixed-bullet+prose symptom, replacing source-level dedup. That coupling doesn't change this tracker's scope but is worth noting — resolving #491 as-scoped may also resolve #492's symptom half. **Owner**: Bosun to fire when next rolling-PR-clean window opens, unless operator wants to schedule it explicitly. No urgency; no v1.0.0 gate.
Owner

Checked the premise before implementing, per Bosun's dispatch note. This tracker's original framing (scripts/changelog-body-check.sh) doesn't survive contact with the code — that bash script is retired; the check now lives as check5Style in internal/gates/changelog_body_check.go (Go port).

Already resolved, not by me. Commit 6d218b0 (2026-08-26 16:40, Carpenter, fixing #931 — today's v0.48.0 cut-blocking incident) promoted check5Style from Verdict: Warn to Verdict: Fail as part of that fix:

return CheckOutcome{Verdict: Fail, Message: fmt.Sprintf("check 5 (style): mixed bullet+prose in section(s): %s — pick one form per section", strings.Join(mixed, " "))}

The commit's own doc comment cites both #931 and #491 together — this promotion was a deliberate two-birds fix. Confirmed end-to-end, not just internally marked: BodyCheckResult.Exit() returns 1 on any Fail verdict among its checks (internal/gates/changelog_body_check.go:112-116), and compose-verify derives its own pass/fail from ChangelogBodyCheck(...).Exit() == 0 (internal/gates/compose_verify.go:104) — so the promotion propagates to both gates that share this check, which is exactly #931's own AC3 ("compose-verify REFUSES on mixed bullet/prose instead of warning").

All four verification ACs checked against the live repo and ticked with evidence above. One (the adopter migration note) is marked RETIRED rather than DONE — no opt-in flag was used, and release-toolkit's only watcher is the operator (ADR-0009 §1), so there's no unknown external population to migrate; the contract doc was updated in the same commit for anyone who does read it.

Closing — the ask is satisfied on main, verified rather than assumed.

Checked the premise before implementing, per Bosun's dispatch note. This tracker's original framing (`scripts/changelog-body-check.sh`) doesn't survive contact with the code — that bash script is retired; the check now lives as `check5Style` in `internal/gates/changelog_body_check.go` (Go port). **Already resolved, not by me.** Commit `6d218b0` (2026-08-26 16:40, Carpenter, fixing #931 — today's v0.48.0 cut-blocking incident) promoted `check5Style` from `Verdict: Warn` to `Verdict: Fail` as part of that fix: ```go return CheckOutcome{Verdict: Fail, Message: fmt.Sprintf("check 5 (style): mixed bullet+prose in section(s): %s — pick one form per section", strings.Join(mixed, " "))} ``` The commit's own doc comment cites both `#931` and `#491` together — this promotion was a deliberate two-birds fix. Confirmed end-to-end, not just internally marked: `BodyCheckResult.Exit()` returns 1 on any `Fail` verdict among its checks (`internal/gates/changelog_body_check.go:112-116`), and `compose-verify` derives its own pass/fail from `ChangelogBodyCheck(...).Exit() == 0` (`internal/gates/compose_verify.go:104`) — so the promotion propagates to both gates that share this check, which is exactly #931's own AC3 ("`compose-verify` REFUSES on mixed bullet/prose instead of warning"). All four verification ACs checked against the live repo and ticked with evidence above. One (the adopter migration note) is marked RETIRED rather than DONE — no opt-in flag was used, and release-toolkit's only watcher is the operator (ADR-0009 §1), so there's no unknown external population to migrate; the contract doc was updated in the same commit for anyone who does read it. Closing — the ask is satisfied on main, verified rather than assumed.
pilot closed this issue 2026-08-26 23:35:58 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#491
No description provided.