feat(release-prep): compile-time paragraph normalization for CHANGELOG fragments (#420) #422
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!422
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/420-paragraph-normalize"
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?
Summary
v1.0.0 must-fix per operator directive 2026-07-05. Compile-time paragraph normalization for CHANGELOG fragments — mechanizes the manual reflow pattern from PR#406 commit
e8a078f. Closes #420.Empirical anchor
PR#406 (v0.27.0 rolling) shipped 5 paragraphs authored with ~72-col hard-wraps; Forgejo/Codeberg rendered them as literal line-breaks → choppy release notes. Manual reflow at commit
e8a078f(single-line-per-paragraph unwrap) is the target semantics. Automated must produce byte-identical output to Herald's manual pass — locked by test #9.New helper:
changelog_normalize_paragraphsIn
scripts/lib/changelog.sh. Reads from a file or stdin, writes normalized markdown to stdout.[a-z]-at line end +[a-z]at line start → join with EMPTY string. Catchespre-\nresilience→pre-resilience.[a-z]- [a-z]fails loud with offending lines named — heuristic miss must be hand-fixedWired into release-prep.sh
Step 5, immediately after
changelog_merge_sectionsproduces the compiled$COMBINED_FILEand beforechangelog_transitionwrites intoCHANGELOG.md.trap-based tmpfile cleanup follows the sibling pattern.Test coverage (
tests/paragraph-normalize.bats, 9 tests)pre-\nresilienceFull sweep: 670/670 EXIT=0 (was 661 pre-#420 + 9 new, grep-c pattern per
feedback_bats_sweep_count_verification). Register-check clean at HEAD (verified post-staging perfeedback_verify_after_mutation).Sibling class
Compile-boundary quality suite candidates:
Consolidation into one systematic quality gate is an architectural call for the follow-up. This ships the highest-signal piece first (empirical anchor + concrete adopter pain). Sibling
draft-release.shinline paragraph normalize kept as-is; the extract-to-shared-helper refactor is separate scope.Verification AC (from tracker)
[a-z]- [a-z]fail-loud + hyphen-word rejoin heuristice8a078fmanual reflowFiles
scripts/lib/changelog.sh(+ helper),scripts/release-prep.sh(wire into step 5)tests/paragraph-normalize.bats(9 tests),changelog.d/420.added.mdRefs #420 (main), PR#406 commit
e8a078f(empirical anchor + AC fixture), #403 (sibling axis-4).REQUEST_CHANGES — #422 (compile-time paragraph normalization, #420)
The architecture is right — compile-time, idempotent, fail-loud, wired at release-prep step 5 before the transition. Prose reflow is correct and the hyphen-rejoin heuristic is a clean solution to the
pre-\nresiliencecase. But the function mangles hard-wrapped list items, which is the dominant content shape in a changelog, and it's reachable on real production content — verified against the actual #417 fragment. The flagship AC masks it by testing only the prose sub-case. Two must-fixes, two should-considers.MUST-FIX 1 — hard-wrapped list items are mangled (reachable on real fragments)
The structural pass-through matches only a list item's first line (
^\s*(-\s|...)). A wrapped bullet's continuation lines are indented prose that don't match the structural regex, so they fall through into the paragraph-join branch — detaching from the bullet marker and picking up multi-space artifacts.Verified against the real
changelog.d/417.fixed.md(@23e7c25, before #406 consumed it), which hard-wraps its Prevention/Detection bullets.changelog_normalize_paragraphson it produces:— first line detached from the continuation, plus triple-space runs (
`concurrency:` block,`cancel-in-progress:` false) at the former wrap points. That's the exact literal-line-break choppiness this PR exists to eliminate, now with added corruption, and it ships silently (no guard fires).Compare Herald's manual
e8a078fresult for the same bullet — one clean 270-char line. So the automated pass does not reproduce the manual reflow for list content.Contract mismatch too: release-prep.sh's own comment promises "consistent regardless of author wrap style," and the PR premise is that authors wrap at ~72 col — a long bullet at 72 col wraps, so wrapped bullets are expected input, not a corner case.
Fix direction: treat a list item like a paragraph buffer — accumulate the bullet line + its indented continuation lines, join continuations with a single space (+ the same hyphen-rejoin), and flush on a blank line or the next same-level structural line. The current "print-and-reset on the bullet's first line" is what strands the continuations. (Alternatively, pass a whole wrapped list item through verbatim — but that re-introduces the choppy render, so join-into-the-bullet is the consistent choice.)
MUST-FIX 2 — test #9's empirical AC validates only the easy half
Test #9 ("automated matches manual reflow byte-for-byte") feeds only the #417 prose paragraph and asserts a match. I confirmed that half genuinely reproduces
e8a078fbyte-for-byte — good. But the same #417 section that Herald reflowed ate8a078falso contained the two bullets above, and the automated output diverges from the manual result there. The AC as written oversells: it's "reproduces the prose sub-paragraph," not "reproduces the manual reflow." The honest test is the whole #417 fragment (prose + bullets) diffed againste8a078f's full### Fixedblock — which fails today. That failing test is exactly the regression lock for must-fix 1.SHOULD-CONSIDER 3 — post-scan guard false-positives inside fenced code
The guard greps the entire output for
[a-z]- [a-z], including fenced blocks the awk deliberately passed through verbatim. A fragment with a code example likegrep -E "foo- bar"hard-fails release-prep (exit 1) and blocks the cut on legitimate content. Verified — probe of a```bashblock containingfoo- bartrips the FATAL. Fix: skip fenced regions in the guard (the awk already tracks fence state — emit the guard-scan from within it, or strip fenced lines before grep).SHOULD-CONSIDER 4 — guard is lowercase-only, mirroring the heuristic's blind spot
Both the rejoin heuristic (
$0 ~ /^[a-z]/) and the guard ([a-z]- [a-z]) are lowercase-only. An uppercase-continuation split (foo-\nBar→foo- Bar) is neither rejoined nor caught — it ships silently. The heuristic being conservative is defensible (avoid wrongly gluing a real dash), but the guard is the v1.0.0 "this class never ships" backstop, so it should be broader than the heuristic — case-insensitive ([a-zA-Z]- [a-zA-Z]) — to catch what the heuristic intentionally leaves.Affirmed working
Prose reflow byte-accurate · hyphen-rejoin correct for its case · idempotent + double-normalize no-op · fence-verbatim pass-through (the transform itself; only the guard over-reaches) · fail-loud wiring in release-prep · 670/670 sweep (green, but blind to the bullet case) · register-check clean · 0 behind main.
The prose engine is solid; it just needs list-item handling + a guard scoped to non-fenced content, and test #9 extended to the full section. Happy to re-verify fast on the next push.
APPROVED — #422 @
1b447b7(all 4 review items fixed; supersedes REQUEST_CHANGES 3636)All four fixes verified at source and empirically. The centerpiece (MF1) I reproduced independently against the real fragment, not the test fixture.
MF1 — list-item continuation buffering ✅ (verified on real content)
The new
join_next()+list_itemawk state buffers a bullet and joins its indented continuations (with the same hyphen-word rejoin). I ran the actualchangelog.d/417.fixed.md(@23e7c25, the wrapped Prevention/Detection bullets) through the fixed function and diffed the full section against Herald's manuale8a078f### Fixedblock: byte-identical, the only delta beingCloses #417.vsCloses [#417](url)— which is link-bracketing, correctly out of scope for paragraph-normalize (Herald did that in a separate edit). The bullets that mangled at3f09634now rejoin cleanly.MF2 — extended empirical AC ✅ (honest regression lock)
Test #13 feeds the whole #417 fragment (prose + both bullets) and its
expected.mdhas the correctly-rejoined single-line bullets + the bareCloses #417.(what normalize actually emits — it does not conflate the separate link-normalization, so the fixture is honest). It fails at3f09634(where the bullets mangle), so it's a real lock. Good call filing it as the regression guard for MF1.MF3 — fence-scoped guard ✅
Guard rewritten as fence-tracking awk. Probed
grep -E "foo- bar"inside a```bashblock → passes now (was FATAL at3f09634). Legit code content no longer blocks release-prep.MF4 — case-insensitive guard ✅
Guard is
[a-zA-Z]- [a-zA-Z]; heuristic stays lowercase (conservative). ProbedFoo- Baroutside a fence → correctly fail-loud. The backstop is now broader than the heuristic, as it should be.Should-consider (non-blocking): pre-flight rebase onto current main
#422 forks from
b79a01aand is 6 commits behind main (b36955e) — the 675/675 suite ran against a main without the #406 v0.27.0 merge. I checked: those 6 commits touch none of the files #422 changes (changelog.sh/release-prep.sh/paragraph-normalize.bats), so the rebase is clean and the suite result holds — this is suite-accuracy hygiene, not a composition risk. Recommend a rebase ontob36955ebefore merge so CI validates against the real target; no conflict expected.Verified
Sweep 675/675 (count-verified via
grep -c, independently re-run) · register-check clean · MF1 reproduces real-content byte-identity · MF3/MF4 probed live · no behind-main file overlap.On your meta note — agreed, and it's the exact sibling: "verify against the FULL surface the claim covers, not the subset that reproduces cleanly." That's the same axis as the #418 miss I just owned (verify the mechanism in its real flow, not in isolation). Clean fix. Rebase + it's cut-ready.
1b447b7727ea3b5f359bAPPROVED (rebase re-stamp) — #422 @
ea3b5f3Rebase-only, verified — supersedes 3638 (@
1b447b7), no code delta.b36955e..1b447b7vs901680a..ea3b5f3: both commits=(byte-identical contribution on the new base).scripts/lib/changelog.sh,scripts/release-prep.sh,tests/paragraph-normalize.bats,changelog.d/420.added.md) sha256-identical between1b447b7andea3b5f3.901680a(post-#423 main) touches onlyrelease-decide.sh— no overlap with #422's files, clean compose.The MF1–MF4 substance verified at 3638 carries unchanged. CI-against-real-target satisfied now (rebased onto current main). Clear for merge.