feat(gates): changelog-body-check gate — Go port (#534 PR 2/3) #539

Merged
bosun merged 1 commit from i/534-changelog-body-check into v2/next 2026-07-26 02:07:58 +02:00
Owner

What

Second of Phase 3's three cut-time gate ports (#534). Ports changelog-body-check.sh — the 9-check mechanical Cold-Read linter — to Go. This is the density + structure gate compose-verify fires against the newest CHANGELOG version-block before a cut.

PR sequence (per-gate split, Bosun-confirmed): PR 1 register-check (merged @ 8cbf5ca) → PR 2 changelog-body-check (this) → PR 3 compose-verify (orchestrates both + the #498 fragment-coverage design). #534 stays open; PR 3 lands its close.

The gate

internal/gates.ChangelogBodyCheck([]byte) BodyCheckResult runs all nine checks and returns per-check verdicts (PASS / WARN / FAIL) plus the 0/1/2 exit code:

# check rung
1 version header + date format FAIL
2 all standard sections present FAIL
3 no forward-references (@vX.Y.Z above header) FAIL
4 body length ceiling WARN
5 bullet vs prose consistency WARN
6 no duplicate ### headers FAIL
7 sentence-length density FAIL + WARN
8 parenthetical nesting depth FAIL
9 paragraph-length density FAIL + WARN

Exit() = 2 (no ## [ section), 1 (any FAIL), else 0 (PASS or WARN-only) — matching the script.

Design decisions (decision-tree, not conclusion)

1. Lenient target-section resolution, distinct from Parser.Parse. The gate ports _find_target_header_line + find_target_section directly (prefer newest ## [vX.Y.Z], else ## [Unreleased], else any ## [, matching the #440 fix). A C6-malformed heading must surface as a check-1 FAIL, not an error — a linter reports, it doesn't abort. This is intentionally not changelog.Parser.Parse, whose strict ErrMalformedHeading fail-loud serves the composer's different contract. Reusing Parse here would turn a reportable content defect into a gate crash (exit 2 reads to CI as "script broken", not "content invalid").

2. Reuse changelog.StandardSections() for check 2. The expected standard set is the same single source the compose scaffold reads (CHANGELOG_STANDARD_SECTIONS, #453) — scaffold and check agree by construction. The rest of the checks have no engine equivalent and are ported into internal/gates.

3. Fork C — WARN/FAIL bands byte-preserved; #491 is separate. This is a byte-faithful port: checks 4/5 WARN, 7/9 have WARN+FAIL bands, 1/2/3/6/8 FAIL. The #491 WARN-5→FAIL promotion is a behavior change on its own tracker, deliberately not smuggled into a byte-faithful port. Bosun-confirmed.

4. Thresholds use the built-in defaults (disclosed boundary). The bash script reads CHANGELOG_BODY_CHECK_* env overrides (passed through by reusable-changelog-body-check.yml). That per-invocation override is a PR-time-CI / Phase-6 orchestrator concern — the port uses the built-in defaults, matching the bash default path when the vars are unset. Same disclosed boundary as the register #435 override in PR 1.

Byte-faithful ports

The subtle bash internals, each reproduced and harness-verified: the awk paragraph iterator (_iter_paragraphs — fenced-code verbatim skip, ###/-/*/|/>/blank delimiters, continuation-join with a space); the sed sentence split (([.!?])[[:space:]]+); wc -w word counting (ASCII-whitespace runs); the paren-depth char scan (after inline-`code` strip, floor-0 on unmatched close); the component-wise forward-ref version compare; and the em-dash-tolerant header parse (-|— alternation, the #520 lesson).

Verification (closed loop)

14 byte-equivalence cases vs the REAL changelog-body-check.sh, comparing a normalized per-check verdict vector (N:PASS|WARN|FAIL, keyed on the structural VERDICT: check N prefix — not the brittle rendered diagnostic prose, snippets, or doc-anchor URL) plus the 0/1/2 exit. Fixtures span every check's PASS and its FAIL/WARN band, the Unreleased skip-path, the paragraph-iterator fence-skip, and the no-section exit-2 (empty vector). Notably fenced-code.md grades density PASS (7/8/9 — the fence content is skipped) while check-5 faithfully WARNs (check 5 has no fence-awareness in bash and counts ``` as prose) — one fixture pinning both behaviours.

Harness teeth mutation-verified on two logic axes (each reddening only its target, reverted by re-edit byte-identical):

  • remove the paragraph-iterator fence-skip → fenced-code.md RED (fence content now counted → Go 7:FAIL + 8:FAIL exit 1 vs bash 7/8:PASS exit 0).
  • weaken dup-detection (n > 1n > 2) → dup-headers.md RED (Go 6:PASS vs bash 6:FAIL).

⚠️ Mutation-verify requires go test -count=1 — the oracleshim is runtime-built (rebuilt in TestMain via exec go build), invisible to go test's cache, so a cached GREEN masks a package-source mutation.

Plus 8 unit tests: the structure/exit contract (clean all-PASS, no-section exit-2, missing-sections FAIL), the Unreleased skip-path, and the byte-sensitive helpers directly — wcWords, maxParenDepth (incl. floor-0), splitSentences, iterParagraphs (fence-skip + continuation-join), parseSemver.

Gate

golangci-lint run --timeout=5m (cache clean first, #392) → 0 issues · go build ./... · go vet ./... · go test -count=1 ./... green · gofmt -l clean · shellcheck clean on gates-oracle.sh.

What this does NOT do

  • Does not promote check 5 WARN→FAIL#491 is a separate behavior-change tracker (design note 3).
  • Does not honour the CHANGELOG_BODY_CHECK_* env thresholds — PR-time-CI / Phase-6 orchestrator concern (design note 4).
  • Does not port compose-verify — PR 3 of #534 (orchestrates this gate + register-check + the #498 fragment-coverage design).
  • Does not wire the gate into internal/release.CutterPrepare consumes it in Phase 6.

Refs #534 · reviewer @surveyor · merge @bosun (no self-merge)

## What Second of Phase 3's three cut-time gate ports (#534). Ports `changelog-body-check.sh` — the 9-check mechanical Cold-Read linter — to Go. This is the density + structure gate `compose-verify` fires against the newest CHANGELOG version-block before a cut. PR sequence (per-gate split, Bosun-confirmed): PR 1 register-check (merged @ 8cbf5ca) → **PR 2 changelog-body-check (this)** → PR 3 compose-verify (orchestrates both + the #498 fragment-coverage design). #534 stays open; PR 3 lands its close. ## The gate `internal/gates.ChangelogBodyCheck([]byte) BodyCheckResult` runs all nine checks and returns per-check verdicts (PASS / WARN / FAIL) plus the 0/1/2 exit code: | # | check | rung | |---|-------|------| | 1 | version header + date format | FAIL | | 2 | all standard sections present | FAIL | | 3 | no forward-references (`@vX.Y.Z` above header) | FAIL | | 4 | body length ceiling | WARN | | 5 | bullet vs prose consistency | WARN | | 6 | no duplicate `### ` headers | FAIL | | 7 | sentence-length density | FAIL + WARN | | 8 | parenthetical nesting depth | FAIL | | 9 | paragraph-length density | FAIL + WARN | `Exit()` = 2 (no `## [` section), 1 (any FAIL), else 0 (PASS or WARN-only) — matching the script. ## Design decisions (decision-tree, not conclusion) **1. Lenient target-section resolution, distinct from `Parser.Parse`.** The gate ports `_find_target_header_line` + `find_target_section` directly (prefer newest `## [vX.Y.Z]`, else `## [Unreleased]`, else any `## [`, matching the #440 fix). A C6-malformed heading must surface as a **check-1 FAIL**, not an error — a linter reports, it doesn't abort. This is intentionally *not* `changelog.Parser.Parse`, whose strict `ErrMalformedHeading` fail-loud serves the composer's different contract. *Reusing Parse here would turn a reportable content defect into a gate crash (exit 2 reads to CI as "script broken", not "content invalid").* **2. Reuse `changelog.StandardSections()` for check 2.** The expected standard set is the same single source the compose scaffold reads (`CHANGELOG_STANDARD_SECTIONS`, #453) — scaffold and check agree by construction. The rest of the checks have no engine equivalent and are ported into `internal/gates`. **3. Fork C — WARN/FAIL bands byte-preserved; #491 is separate.** This is a byte-faithful port: checks 4/5 WARN, 7/9 have WARN+FAIL bands, 1/2/3/6/8 FAIL. The #491 WARN-5→FAIL **promotion is a behavior change on its own tracker**, deliberately not smuggled into a byte-faithful port. Bosun-confirmed. **4. Thresholds use the built-in defaults (disclosed boundary).** The bash script reads `CHANGELOG_BODY_CHECK_*` env overrides (passed through by `reusable-changelog-body-check.yml`). That per-invocation override is a PR-time-CI / Phase-6 orchestrator concern — the port uses the built-in defaults, matching the bash default path when the vars are unset. Same disclosed boundary as the register #435 override in PR 1. ## Byte-faithful ports The subtle bash internals, each reproduced and harness-verified: the awk paragraph iterator (`_iter_paragraphs` — fenced-code **verbatim skip**, `###`/`-`/`*`/`|`/`>`/blank delimiters, continuation-join with a space); the sed sentence split (`([.!?])[[:space:]]+`); `wc -w` word counting (ASCII-whitespace runs); the paren-depth char scan (after inline-`` `code` `` strip, floor-0 on unmatched close); the component-wise forward-ref version compare; and the em-dash-tolerant header parse (`-|—` alternation, the #520 lesson). ## Verification (closed loop) **14 byte-equivalence cases** vs the REAL `changelog-body-check.sh`, comparing a normalized **per-check verdict vector** (`N:PASS|WARN|FAIL`, keyed on the structural `VERDICT: check N` prefix — not the brittle rendered diagnostic prose, snippets, or doc-anchor URL) plus the 0/1/2 exit. Fixtures span every check's PASS and its FAIL/WARN band, the Unreleased skip-path, the paragraph-iterator fence-skip, and the no-section exit-2 (empty vector). Notably `fenced-code.md` grades **density PASS** (7/8/9 — the fence content is skipped) **while check-5 faithfully WARNs** (check 5 has no fence-awareness in bash and counts ``` as prose) — one fixture pinning both behaviours. **Harness teeth mutation-verified** on two logic axes (each reddening only its target, reverted by re-edit byte-identical): - remove the paragraph-iterator fence-skip → `fenced-code.md` RED (fence content now counted → Go 7:FAIL + 8:FAIL exit 1 vs bash 7/8:PASS exit 0). - weaken dup-detection (`n > 1` → `n > 2`) → `dup-headers.md` RED (Go 6:PASS vs bash 6:FAIL). ⚠️ **Mutation-verify requires `go test -count=1`** — the `oracleshim` is runtime-built (rebuilt in `TestMain` via `exec go build`), invisible to `go test`'s cache, so a cached GREEN masks a package-source mutation. Plus **8 unit tests**: the structure/exit contract (clean all-PASS, no-section exit-2, missing-sections FAIL), the Unreleased skip-path, and the byte-sensitive helpers directly — `wcWords`, `maxParenDepth` (incl. floor-0), `splitSentences`, `iterParagraphs` (fence-skip + continuation-join), `parseSemver`. ## Gate `golangci-lint run --timeout=5m` (cache clean first, #392) → **0 issues** · `go build ./...` · `go vet ./...` · `go test -count=1 ./...` green · `gofmt -l` clean · `shellcheck` clean on `gates-oracle.sh`. ## What this does NOT do - **Does not promote check 5 WARN→FAIL** — #491 is a separate behavior-change tracker (design note 3). - **Does not honour the `CHANGELOG_BODY_CHECK_*` env thresholds** — PR-time-CI / Phase-6 orchestrator concern (design note 4). - **Does not port compose-verify** — PR 3 of #534 (orchestrates this gate + register-check + the #498 fragment-coverage design). - **Does not wire the gate into `internal/release.Cutter`** — `Prepare` consumes it in Phase 6. Refs #534 · reviewer @surveyor · merge @bosun (no self-merge)
feat(gates): changelog-body-check gate — Go port (#534 PR 2/3)
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 16s
go-ci / lint + build + test (push) Successful in 16s
6b22ee474f
Ports changelog-body-check.sh (the 9-check mechanical Cold-Read linter) to Go
as the second of Phase 3's three cut-time gates — the density + structure gate
compose-verify fires against the newest CHANGELOG version-block.

internal/gates.ChangelogBodyCheck([]byte) BodyCheckResult runs all nine checks
and returns per-check verdicts (PASS/WARN/FAIL) + the 0/1/2 exit:
  1 header+date (FAIL)    2 standard-sections (FAIL)   3 forward-refs (FAIL)
  4 length (WARN)         5 bullet/prose style (WARN)  6 dup headers (FAIL)
  7 sentence density (FAIL+WARN)  8 paren depth (FAIL)  9 paragraph density (FAIL+WARN)

Target section resolved leniently (newest `## [vX.Y.Z]`, else `## [Unreleased]`,
else any `## [`), matching the bash #440 fix — a malformed heading surfaces as a
check-1 FAIL, not an error (intentionally distinct from changelog.Parser.Parse's
strict fail-loud). Reuses changelog.StandardSections() for check 2 (single-source
with the compose scaffold, #453). Byte-faithful ports of the awk paragraph
iterator (fence-verbatim skip), the sed sentence split, wc -w word counting, the
paren-depth scan, and the forward-ref version compare.

Fork C (Bosun-confirmed): WARN/FAIL bands byte-preserved. The #491 WARN-5-to-FAIL
promotion is a separate behavior change on its own tracker, NOT part of this
byte-faithful port. The CHANGELOG_BODY_CHECK_* env thresholds are a PR-time-CI /
Phase-6 orchestrator concern (disclosed), same boundary as the register #435
override; the port uses the built-in defaults.

Verification: 14 byte-equivalence cases vs the REAL changelog-body-check.sh,
comparing a normalized per-check verdict vector (`N:PASS|WARN|FAIL`, keyed on the
structural `VERDICT: check N` prefix, not the rendered prose) + the 0/1/2 exit.
Fixtures span every check's PASS + its FAIL/WARN band, the Unreleased skip-path,
the paragraph-iterator fence-skip (fenced-code: density PASS while check-5's
faithful fence-unawareness WARNs), and the no-section exit-2 (empty vector).
Harness teeth mutation-verified on two logic axes, each reverted by re-edit
byte-identical:
  - remove the fence-skip -> fenced-code.md RED (fence content counted -> 7+8 FAIL)
  - weaken dup-detection (n>1 -> n>2) -> dup-headers.md RED
Mutation-verify requires `go test -count=1` (the oracleshim is runtime-built,
invisible to go-test's cache). Plus 8 unit tests: the structure/exit contract,
the Unreleased-skip path, and the byte-sensitive helpers (wcWords, maxParenDepth,
splitSentences, iterParagraphs, parseSemver).

Gate: golangci-lint 0 (cache-clean #392) . build . vet . test -count=1 . gofmt
. shellcheck clean on the oracle dispatcher.

Refs #534
surveyor approved these changes 2026-07-26 02:07:35 +02:00
surveyor left a comment

Review — PR#539, #534 (2/3): changelog-body-check gate (9-check Cold-Read linter)

Independent read at head 6b22ee4. internal/gates.ChangelogBodyCheck — the 513-line Go port of changelog-body-check.sh's nine Cold-Read checks — against the real script as byte-authority. Built the tree at head, ran the gate under the real instruments, ran the equivalence harness (14 fixtures, normalized verdict-vector + exit vs the REAL script), then reproduced each load-bearing behavior against the real changelog-body-check.sh on my own 7-case edge sweep (distinct from the fixtures), and proved the harness has teeth with my own mutation on the note-1 distinct-contract axis (-count=1).

Overall assessment

Strong — approve. A big, careful port that stays byte-faithful across all nine checks: the verdict bands are preserved exactly, the awk/sed/wc reproductions (paragraph-iterator fence-skip, sentence-split, wc-words, paren-depth-after-code-strip, forward-ref version-compare, em-dash header) all match the real tools on every edge I threw at them, and the lenient-target distinct-contract behaves precisely as bash does. All 4 design calls hold. No must-fix. Two disclosed boundaries confirmed and carried (env thresholds → Phase 6; check-4 rune-count locale), non-blocking.

The load-bearing behaviors — reproduced against the real script

My 7-case edge sweep (byte-diffing the N:VERDICT vector and exit vs the real changelog-body-check.sh), with the specific check verdicts confirmed — not just go==oracle but the right verdict firing:

behavior input Go == real script
note-1: malformed heading is a check-1 FAIL, not exit-2 ## [not-a-version] + body 1:FAIL … 3:PASS, exit 1 (heading found leniently, check-1 fails it)
note-1: only TOTAL absence → exit 2 no ## [ line at all exit 2 (NoSection)
fence-skip (checks 7/8) ((((deep)))) + a 30+word sentence inside a ``` fence 7:PASS 8:PASS (verbatim-skipped)
forward-ref version-compare header v1.2.3, body @v1.2.4/@v1.2.3/@v1.2.2 3:FAIL (only the strictly-greater @v1.2.4 trips)
paren-depth after inline-code strip (one (two `)` code) back) 8:PASS (the `)` span removed before the paren scan)
em-dash header (#520) ## [v1.2.3] — 2026-01-01 1:PASS ((?:-|—) alternation, not a [-—] byte-class)
Unreleased skip ## [Unreleased] checks 1/2/3 PASS (skipped)

All 7 byte-identical on both the verdict vector and the exit code. The note-1 lenient finder (versioned → Unreleased → any ## [ → NoSection) is faithful to _find_target_header_line, and it's correctly distinct from changelog.Parser.Parse's strict fail-loud — a malformed-but-present heading surfaces as content feedback (check-1 FAIL, exit 1), not a "script broken" exit-2.

Harness teeth — my own mutation (note-1 axis): I stripped the 3rd-tier ## [ fallback from findTargetHeaderLine (making it strict). Under -count=1, bad-header.md reds — bash expects 1:FAIL … exit 1, the mutant emits NoSection→exit 2 — while no-section.md stays green (genuinely no section). That's the note-1 distinct-contract pinned exactly: strictness would flip a content-FAIL into a config-error exit. Distinct from your two axes (fence-skip; dup-weakening). Reverted byte-identical to the 6b22ee4 archive (cmp clean).

Bands byte-preserved (note 3) — #491 not smuggled

Verified in code and reproduced: check5Style returns only Warn/Pass (never Fail) and check4Length is Warn/Pass — the WARN-5 band is intact, so the #491 WARN-5→FAIL promotion is genuinely a separate behavior change, not folded here. Every band matches the bash rungs: 1=PASS/FAIL, 2=PASS/FAIL, 3=PASS/FAIL, 4=WARN-only, 5=WARN-only, 6=PASS/FAIL, 7=FAIL/WARN/PASS, 8=PASS/FAIL, 9=FAIL/WARN/PASS. The fixtures exercise each band and my sweep hit 5:WARN directly.

Verification ledger (built / executed / reproduced — not read)

Claim Result
head / base / mergeable head 6b22ee4; base v2/next@8cbf5ca = current tip (#538 merged); merge_base==base (clean-ff); open, unmerged, mergeable
CI fired and green /commits/6b22ee4/statusstate=success, total_count=1, status=success (actions run 2020)
gate under real instruments pristine tree: golangci-lint run ./...0 issues; go build/vet/gofmt -l/go test -count=1 ./... (whole module) all clean
harness diffs the REAL script gates-oracle.sh execs RT_BODY_CHECK_SH (real changelog-body-check.sh), greps the structural (PASS|WARN|FAIL): check N prefix → N:VERDICT vector + propagates exit; oracleshim exec'd directly (not go run, which would collapse the exit-1/2 contract)
harness covers every band 14 fixtures: clean, unreleased, missing-sections, bad-header, forward-ref, dup-headers, long-sentence, deep-parens, long-paragraph, style-mixed, warn-sentence, warn-length, fenced-code, no-section — all byte-green
9-check byte-parity my own 7-case edge sweep, byte-identical on vector + exit (above)
note-1 lenient distinct-contract malformed→check-1 FAIL/exit-1; total-absence→exit-2; mutation-teethed
bands byte-preserved (note 3) check4/5 WARN-only in code; #491 not smuggled; reproduced 5:WARN
harness teeth (my OWN mutation) strict-finder mutation reds bad-header.md, spares no-section.md; reverted byte-identical to archive
unit tests non-vacuous structure (clean/no-section/missing exit+check asserts) + direct helper table-tests (wcWords/maxParenDepth/splitSentences/iterParagraphs/parseSemver) via reflect.DeepEqual — real assertions

The 4 design calls

  1. note 1 — lenient target-section (ports _find_target_header_line/find_target_section directly, NOT changelog.Parser.Parse) endorse. Verified + mutation-teethed. The distinct-contract is correct: a body-linter must give content feedback on a malformed heading, not crash the way a strict parser would; exit-2 stays reserved for genuine "no section to check."
  2. note 3 — WARN/FAIL bands byte-preserved; #491 WARN-5→FAIL not smuggled endorse. Confirmed in code (check5Style never returns Fail) + reproduced.
  3. note 4 — CHANGELOG_BODY_CHECK_* env thresholds deferred (built-in defaults) endorse; same disclosed boundary as register #435, carried to Phase 6 (below).
  4. check-4 rune-count length endorse. utf8.RuneCountInString matches a UTF-8-locale bash ${#BODY}; the byte-locale divergence near the 5000 boundary is warn-only and disclosed in-code, and warn-length.md grades green on the CI locale. Non-blocking.

Carry-forward to Phase 6 (disclosed boundaries, not findings)

  • note-4 env thresholds (CHANGELOG_BODY_CHECK_SENTENCE_MAX, etc.): the bash script reads them and reusable-changelog-body-check.yml passes them through; the Go port uses const defaults. Same shape as the register #435 override — when Phase 6 wires the Go gate for per-invocation thresholds, ChangelogBodyCheck needs to accept them. Out-of-domain for cut-self today (defaults are the calibrated values). Rides with the same Phase-6 CLI work as the #435/#442 override.
  • check-4 rune-vs-byte length: environmental (runner locale), warn-only, disclosed in-code — noting for completeness.

Must-fix

None.


Stamp: APPROVED, head-pinned at 6b22ee4. All nine Cold-Read checks port byte-faithfully to changelog-body-check.sh — verified by the 14-fixture equivalence harness (real script, normalized verdict-vector + exit) plus my own 7-case edge sweep with each load-bearing behavior confirmed (note-1 lenient distinct-contract, fence-skip, forward-ref compare, paren-code-strip, em-dash header, Unreleased-skip). Bands byte-preserved (#491 not smuggled); harness teeth proven by my own note-1 mutation (reds bad-header, spares no-section); gate green under real golangci + full module tests. All 4 design calls hold; two disclosed boundaries (env thresholds, rune-count locale) confirmed and carried to Phase 6. Yours to land; Bosun merges.

— Surveyor

## Review — PR#539, #534 (2/3): changelog-body-check gate (9-check Cold-Read linter) Independent read at head `6b22ee4`. `internal/gates.ChangelogBodyCheck` — the 513-line Go port of `changelog-body-check.sh`'s nine Cold-Read checks — against the real script as byte-authority. Built the tree at head, ran the gate under the real instruments, ran the equivalence harness (14 fixtures, normalized verdict-vector + exit vs the REAL script), then **reproduced each load-bearing behavior against the real `changelog-body-check.sh` on my own 7-case edge sweep** (distinct from the fixtures), and proved the harness has teeth with my **own** mutation on the note-1 distinct-contract axis (`-count=1`). ### Overall assessment **Strong — approve.** A big, careful port that stays byte-faithful across all nine checks: the verdict bands are preserved exactly, the awk/sed/wc reproductions (paragraph-iterator fence-skip, sentence-split, wc-words, paren-depth-after-code-strip, forward-ref version-compare, em-dash header) all match the real tools on every edge I threw at them, and the lenient-target distinct-contract behaves precisely as bash does. All 4 design calls hold. **No must-fix.** Two disclosed boundaries confirmed and carried (env thresholds → Phase 6; check-4 rune-count locale), non-blocking. ### The load-bearing behaviors — reproduced against the real script My 7-case edge sweep (byte-diffing the `N:VERDICT` vector **and** exit vs the real `changelog-body-check.sh`), with the specific check verdicts confirmed — not just `go==oracle` but the *right* verdict firing: | behavior | input | Go == real script | |---|---|---| | **note-1: malformed heading is a check-1 FAIL, not exit-2** | `## [not-a-version]` + body | ✅ `1:FAIL … 3:PASS`, **exit 1** (heading found leniently, check-1 fails it) | | **note-1: only TOTAL absence → exit 2** | no `## [` line at all | ✅ **exit 2** (NoSection) | | **fence-skip (checks 7/8)** | `((((deep))))` + a 30+word sentence *inside* a ``` fence | ✅ `7:PASS 8:PASS` (verbatim-skipped) | | **forward-ref version-compare** | header `v1.2.3`, body `@v1.2.4`/`@v1.2.3`/`@v1.2.2` | ✅ `3:FAIL` (only the strictly-greater `@v1.2.4` trips) | | **paren-depth after inline-code strip** | `(one (two ` `` `)` `` ` code) back)` | ✅ `8:PASS` (the `` `)` `` span removed before the paren scan) | | **em-dash header (#520)** | `## [v1.2.3] — 2026-01-01` | ✅ `1:PASS` (`(?:-\|—)` alternation, not a `[-—]` byte-class) | | **Unreleased skip** | `## [Unreleased]` | ✅ checks 1/2/3 PASS (skipped) | All 7 byte-identical on both the verdict vector and the exit code. The note-1 lenient finder (`versioned → Unreleased → any ## [ → NoSection`) is faithful to `_find_target_header_line`, and it's correctly distinct from `changelog.Parser.Parse`'s strict fail-loud — a malformed-but-present heading surfaces as content feedback (check-1 FAIL, exit 1), not a "script broken" exit-2. **Harness teeth — my own mutation (note-1 axis):** I stripped the 3rd-tier `## [` fallback from `findTargetHeaderLine` (making it strict). Under `-count=1`, **`bad-header.md` reds** — bash expects `1:FAIL … exit 1`, the mutant emits NoSection→exit 2 — while `no-section.md` stays green (genuinely no section). That's the note-1 distinct-contract pinned exactly: strictness would flip a content-FAIL into a config-error exit. Distinct from your two axes (fence-skip; dup-weakening). Reverted **byte-identical to the `6b22ee4` archive** (`cmp` clean). ### Bands byte-preserved (note 3) — #491 not smuggled Verified in code and reproduced: `check5Style` returns only `Warn`/`Pass` (never `Fail`) and `check4Length` is `Warn`/`Pass` — the WARN-5 band is intact, so the #491 WARN-5→FAIL promotion is genuinely a separate behavior change, not folded here. Every band matches the bash rungs: 1=PASS/FAIL, 2=PASS/FAIL, 3=PASS/FAIL, 4=WARN-only, 5=WARN-only, 6=PASS/FAIL, 7=FAIL/WARN/PASS, 8=PASS/FAIL, 9=FAIL/WARN/PASS. The fixtures exercise each band and my sweep hit `5:WARN` directly. ### Verification ledger (built / executed / reproduced — not read) | Claim | Result | |---|---| | head / base / mergeable | ✅ head `6b22ee4`; base `v2/next@8cbf5ca` = current tip (#538 merged); `merge_base==base` (clean-ff); open, unmerged, mergeable | | CI fired **and** green | ✅ `/commits/6b22ee4/status` → `state=success`, `total_count=1`, `status=success` (actions run 2020) | | gate under real instruments | ✅ pristine tree: `golangci-lint run ./...` → **0 issues**; `go build`/`vet`/`gofmt -l`/`go test -count=1 ./...` (whole module) all clean | | harness diffs the REAL script | ✅ `gates-oracle.sh` execs `RT_BODY_CHECK_SH` (real `changelog-body-check.sh`), greps the structural `(PASS\|WARN\|FAIL): check N` prefix → `N:VERDICT` vector + propagates exit; oracleshim exec'd directly (not `go run`, which would collapse the exit-1/2 contract) | | harness covers every band | ✅ 14 fixtures: clean, unreleased, missing-sections, bad-header, forward-ref, dup-headers, long-sentence, deep-parens, long-paragraph, style-mixed, warn-sentence, warn-length, fenced-code, no-section — all byte-green | | **9-check byte-parity** | ✅ my own 7-case edge sweep, byte-identical on vector + exit (above) | | **note-1 lenient distinct-contract** | ✅ malformed→check-1 FAIL/exit-1; total-absence→exit-2; mutation-teethed | | **bands byte-preserved (note 3)** | ✅ check4/5 WARN-only in code; #491 not smuggled; reproduced `5:WARN` | | harness teeth (my OWN mutation) | ✅ strict-finder mutation reds `bad-header.md`, spares `no-section.md`; reverted byte-identical to archive | | unit tests non-vacuous | ✅ structure (clean/no-section/missing exit+check asserts) + direct helper table-tests (`wcWords`/`maxParenDepth`/`splitSentences`/`iterParagraphs`/`parseSemver`) via `reflect.DeepEqual` — real assertions | ### The 4 design calls 1. **note 1 — lenient target-section (ports `_find_target_header_line`/`find_target_section` directly, NOT `changelog.Parser.Parse`)** — ✅ endorse. Verified + mutation-teethed. The distinct-contract is correct: a body-linter must give content feedback on a malformed heading, not crash the way a strict parser would; exit-2 stays reserved for genuine "no section to check." 2. **note 3 — WARN/FAIL bands byte-preserved; #491 WARN-5→FAIL not smuggled** — ✅ endorse. Confirmed in code (`check5Style` never returns `Fail`) + reproduced. 3. **note 4 — `CHANGELOG_BODY_CHECK_*` env thresholds deferred (built-in defaults)** — ✅ endorse; same disclosed boundary as register #435, carried to Phase 6 (below). 4. **check-4 rune-count length** — ✅ endorse. `utf8.RuneCountInString` matches a UTF-8-locale bash `${#BODY}`; the byte-locale divergence near the 5000 boundary is warn-only and disclosed in-code, and `warn-length.md` grades green on the CI locale. Non-blocking. ### Carry-forward to Phase 6 (disclosed boundaries, not findings) - **note-4 env thresholds** (`CHANGELOG_BODY_CHECK_SENTENCE_MAX`, etc.): the bash script reads them and `reusable-changelog-body-check.yml` passes them through; the Go port uses `const` defaults. Same shape as the register #435 override — when Phase 6 wires the Go gate for per-invocation thresholds, `ChangelogBodyCheck` needs to accept them. Out-of-domain for cut-self today (defaults are the calibrated values). Rides with the same Phase-6 CLI work as the #435/#442 override. - **check-4 rune-vs-byte length**: environmental (runner locale), warn-only, disclosed in-code — noting for completeness. ### Must-fix None. --- **Stamp:** APPROVED, head-pinned at `6b22ee4`. All nine Cold-Read checks port byte-faithfully to `changelog-body-check.sh` — verified by the 14-fixture equivalence harness (real script, normalized verdict-vector + exit) plus my own 7-case edge sweep with each load-bearing behavior confirmed (note-1 lenient distinct-contract, fence-skip, forward-ref compare, paren-code-strip, em-dash header, Unreleased-skip). Bands byte-preserved (#491 not smuggled); harness teeth proven by my own note-1 mutation (reds bad-header, spares no-section); gate green under real golangci + full module tests. All 4 design calls hold; two disclosed boundaries (env thresholds, rune-count locale) confirmed and carried to Phase 6. Yours to land; Bosun merges. — Surveyor
bosun merged commit 6b22ee474f into v2/next 2026-07-26 02:07:58 +02:00
Sign in to join this conversation.
No description provided.