feat(gates): changelog-body-check gate — Go port (#534 PR 2/3) #539
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!539
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/534-changelog-body-check"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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 gatecompose-verifyfires 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) BodyCheckResultruns all nine checks and returns per-check verdicts (PASS / WARN / FAIL) plus the 0/1/2 exit code:@vX.Y.Zabove header)###headersExit()= 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_sectiondirectly (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 notchangelog.Parser.Parse, whose strictErrMalformedHeadingfail-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 intointernal/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 byreusable-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 -wword 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 structuralVERDICT: check Nprefix — 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). Notablyfenced-code.mdgrades 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):
fenced-code.mdRED (fence content now counted → Go 7:FAIL + 8:FAIL exit 1 vs bash 7/8:PASS exit 0).n > 1→n > 2) →dup-headers.mdRED (Go 6:PASS vs bash 6:FAIL).⚠️ Mutation-verify requires
go test -count=1— theoracleshimis runtime-built (rebuilt inTestMainviaexec go build), invisible togo 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 -lclean ·shellcheckclean ongates-oracle.sh.What this does NOT do
CHANGELOG_BODY_CHECK_*env thresholds — PR-time-CI / Phase-6 orchestrator concern (design note 4).internal/release.Cutter—Prepareconsumes it in Phase 6.Refs #534 · reviewer @surveyor · merge @bosun (no self-merge)
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 ofchangelog-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 realchangelog-body-check.shon 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:VERDICTvector and exit vs the realchangelog-body-check.sh), with the specific check verdicts confirmed — not justgo==oraclebut the right verdict firing:## [not-a-version]+ body1:FAIL … 3:PASS, exit 1 (heading found leniently, check-1 fails it)## [line at all((((deep))))+ a 30+word sentence inside a ``` fence7:PASS 8:PASS(verbatim-skipped)v1.2.3, body@v1.2.4/@v1.2.3/@v1.2.23:FAIL(only the strictly-greater@v1.2.4trips)(one (two`)`code) back)8:PASS(the`)`span removed before the paren scan)## [v1.2.3] — 2026-01-011:PASS((?:-|—)alternation, not a[-—]byte-class)## [Unreleased]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 fromchangelog.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 fromfindTargetHeaderLine(making it strict). Under-count=1,bad-header.mdreds — bash expects1:FAIL … exit 1, the mutant emits NoSection→exit 2 — whileno-section.mdstays 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 the6b22ee4archive (cmpclean).Bands byte-preserved (note 3) — #491 not smuggled
Verified in code and reproduced:
check5Stylereturns onlyWarn/Pass(neverFail) andcheck4LengthisWarn/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 hit5:WARNdirectly.Verification ledger (built / executed / reproduced — not read)
6b22ee4; basev2/next@8cbf5ca= current tip (#538 merged);merge_base==base(clean-ff); open, unmerged, mergeable/commits/6b22ee4/status→state=success,total_count=1,status=success(actions run 2020)golangci-lint run ./...→ 0 issues;go build/vet/gofmt -l/go test -count=1 ./...(whole module) all cleangates-oracle.shexecsRT_BODY_CHECK_SH(realchangelog-body-check.sh), greps the structural(PASS|WARN|FAIL): check Nprefix →N:VERDICTvector + propagates exit; oracleshim exec'd directly (notgo run, which would collapse the exit-1/2 contract)5:WARNbad-header.md, sparesno-section.md; reverted byte-identical to archivewcWords/maxParenDepth/splitSentences/iterParagraphs/parseSemver) viareflect.DeepEqual— real assertionsThe 4 design calls
_find_target_header_line/find_target_sectiondirectly, NOTchangelog.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."check5Stylenever returnsFail) + reproduced.CHANGELOG_BODY_CHECK_*env thresholds deferred (built-in defaults) — ✅ endorse; same disclosed boundary as register #435, carried to Phase 6 (below).utf8.RuneCountInStringmatches a UTF-8-locale bash${#BODY}; the byte-locale divergence near the 5000 boundary is warn-only and disclosed in-code, andwarn-length.mdgrades green on the CI locale. Non-blocking.Carry-forward to Phase 6 (disclosed boundaries, not findings)
CHANGELOG_BODY_CHECK_SENTENCE_MAX, etc.): the bash script reads them andreusable-changelog-body-check.ymlpasses them through; the Go port usesconstdefaults. Same shape as the register #435 override — when Phase 6 wires the Go gate for per-invocation thresholds,ChangelogBodyCheckneeds 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.Must-fix
None.
Stamp: APPROVED, head-pinned at
6b22ee4. All nine Cold-Read checks port byte-faithfully tochangelog-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