feat: changelog_fragment_problem bounds entries at 300 characters (#167)
One definition in the fragment predicate; changelog-armed reds the PR that writes the fragment and the assembler refuses at release, both by inheritance. Doctrine names the number in BUILDER.md and CHANGELOG.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9008e03b03
commit
6c746364af
6 changed files with 150 additions and 5 deletions
|
|
@ -141,7 +141,11 @@ triage bug, and the move is to say so on the issue, not to guess.
|
|||
cross-repo) — the exact prose that will be published, nothing else: `- `
|
||||
bullets, and in a grouped repo the `### Added` / `### Changed` /
|
||||
`### Fixed` headings inside the fragment, creating a rarer kind only when
|
||||
a change genuinely is one. Never edit `CHANGELOG.md` for an entry — the
|
||||
a change genuinely is one. An entry is at most 300 characters — the
|
||||
fragment guard reds longer (#167) — so a genuinely long change ships
|
||||
several short entries, never one long one; wrapping an entry over
|
||||
continuation lines is fine and never counts against it. Never edit
|
||||
`CHANGELOG.md` for an entry — the
|
||||
release PR assembles the section from the fragments (#112); the monotonic
|
||||
guard still refuses anything that deletes a shipped heading.
|
||||
- Follow the repo's conventions file and match the code you touch. Tests are
|
||||
|
|
|
|||
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -2,10 +2,12 @@
|
|||
|
||||
The curated history of the ceremony itself. Each release's section is
|
||||
published verbatim as that release's body (lib/changelog.sh extracts it),
|
||||
so entries say what changed, cite the issue, and stop. Entries arrive as
|
||||
fragments — one `changelog.d/<issue>.md` per PR, never an edit to this
|
||||
file — and the release PR assembles them into the next section here
|
||||
(`bin/changelog-assemble`, #112).
|
||||
so entries say what changed, cite the issue, and stop — at most 300
|
||||
characters each, guard-enforced on the PR that writes the fragment (#167);
|
||||
a genuinely long change ships several short entries, never one long one.
|
||||
Entries arrive as fragments — one `changelog.d/<issue>.md` per PR, never
|
||||
an edit to this file — and the release PR assembles them into the next
|
||||
section here (`bin/changelog-assemble`, #112).
|
||||
|
||||
## 0.3.0 — 2026-07-24
|
||||
|
||||
|
|
|
|||
5
changelog.d/167.md
Normal file
5
changelog.d/167.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
- `changelog_fragment_problem` bounds every entry at 300 normalized
|
||||
characters, red on the PR that writes the fragment; the armed guard and
|
||||
the assembler inherit the one definition (#167).
|
||||
- BUILDER.md and CHANGELOG.md state the bound and the split rule: a long
|
||||
change ships several short entries, never one long one (#167).
|
||||
|
|
@ -106,6 +106,15 @@ changelog_fragments() {
|
|||
# publisher enforces at release time, moved onto the PR;
|
||||
# - no '### ' heading without a bullet before the next heading or EOF:
|
||||
# the dangling grouped heading #98 taught us to refuse.
|
||||
# - no entry longer than 300 characters (#167): 0.3.0 shipped a cluster of
|
||||
# 316–789-character entries straight through the prose rule, so the
|
||||
# bound moves onto the PR like every other fragment rule. Measured on
|
||||
# the normalized entry — continuation lines joined, whitespace runs
|
||||
# collapsed to one space, the '- '/'* ' marker stripped, the '(#N)'
|
||||
# citation included — so wrapping alone can never red an entry. 300
|
||||
# splits the measured history: every healthy entry passes untouched,
|
||||
# the drift cluster does not. mawk's length() counts bytes; prose here
|
||||
# is ASCII and the fuzz is acceptable.
|
||||
changelog_fragment_problem() {
|
||||
local file="$1" base problem
|
||||
base="${file##*/}"
|
||||
|
|
@ -147,6 +156,40 @@ changelog_fragment_problem() {
|
|||
printf "fragment '%s' has an empty heading: '%s'\n" "$file" "$problem"
|
||||
return 1
|
||||
fi
|
||||
|
||||
problem="$(
|
||||
awk -v max=300 '
|
||||
function flush( len, e) {
|
||||
if (entry == "") return 0
|
||||
e = entry
|
||||
entry = ""
|
||||
gsub(/[[:space:]]+/, " ", e)
|
||||
sub(/^ /, "", e)
|
||||
sub(/ $/, "", e)
|
||||
len = length(e)
|
||||
if (len > max) {
|
||||
printf "%d\t%s\n", len, substr(e, 1, 60)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
/^### / { if (flush()) exit; next }
|
||||
/^[[:space:]]*[-*][[:space:]]/ {
|
||||
if (flush()) exit
|
||||
entry = $0
|
||||
sub(/^[[:space:]]*[-*][[:space:]]+/, "", entry)
|
||||
next
|
||||
}
|
||||
/^[[:space:]]*$/ { next }
|
||||
entry != "" { entry = entry " " $0 }
|
||||
END { flush() }
|
||||
' "$file"
|
||||
)"
|
||||
if [ -n "$problem" ]; then
|
||||
printf "fragment '%s' has a %s-character entry — '%s…' — the bound is 300: split it into multiple '- ' entries in this same fragment\n" \
|
||||
"$file" "${problem%%$'\t'*}" "${problem#*$'\t'}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# changelog_shape_problem <changelog> <fragments-dir>
|
||||
|
|
|
|||
|
|
@ -279,6 +279,25 @@ printf '%s\n' "- Added fragment mode." >"$TMP/fragments-dev-flat/changelog.d/115
|
|||
check "fragment -dev + well-formed flat fragment passes" 0 "fragment mode" \
|
||||
in_tree fragments-dev-flat
|
||||
|
||||
# The entry length bound (#167) reds the PR that writes the fragment, with
|
||||
# the shared changelog_fragment_problem diagnosis.
|
||||
fragment_tree fragments-dev-over-bound 1.2.4-dev <<'EOF'
|
||||
# Changelog
|
||||
|
||||
## 1.2.3 — 2026-07-20
|
||||
|
||||
- The shipped entry.
|
||||
EOF
|
||||
printf -- '- %s\n' \
|
||||
"$(awk 'BEGIN { s = ""; while (length(s) < 301) s = s "a"; print s }')" \
|
||||
>"$TMP/fragments-dev-over-bound/changelog.d/115.md"
|
||||
check "fragment mode refuses an over-bound entry, fragment and length named" 1 \
|
||||
"115.md' has a 301-character entry" \
|
||||
in_tree fragments-dev-over-bound
|
||||
check "fragment mode over-bound refusal names the bound and the split fix" 1 \
|
||||
"the bound is 300: split it into multiple '- ' entries in this same fragment" \
|
||||
in_tree fragments-dev-over-bound
|
||||
|
||||
fragment_tree fragments-dev-grouped 1.2.4-dev <<'EOF'
|
||||
# Changelog
|
||||
|
||||
|
|
|
|||
|
|
@ -259,6 +259,78 @@ check "fragment predicate: a dangling grouped heading is refused, heading named"
|
|||
"has an empty heading: '### Added'" \
|
||||
changelog_fragment_problem "$PF/22.md"
|
||||
|
||||
# --- the entry length bound (#167) -------------------------------------------
|
||||
|
||||
# mkchars <n> — a run of n 'a's, for entries of exact constructed length.
|
||||
mkchars() {
|
||||
awk -v n="$1" 'BEGIN { s = ""; while (length(s) < n) s = s "a"; print s }'
|
||||
}
|
||||
|
||||
printf -- '- %s\n' "$(mkchars 301)" >"$PF/30.md"
|
||||
check "length bound: a 301-character entry is refused, fragment and length named" 1 \
|
||||
"30.md' has a 301-character entry" \
|
||||
changelog_fragment_problem "$PF/30.md"
|
||||
check "length bound: the refusal names the bound and the split fix" 1 \
|
||||
"the bound is 300: split it into multiple '- ' entries in this same fragment" \
|
||||
changelog_fragment_problem "$PF/30.md"
|
||||
|
||||
printf -- '- %s\n' "$(mkchars 300)" >"$PF/31.md"
|
||||
check "length bound: an entry of exactly 300 passes" 0 "" \
|
||||
changelog_fragment_problem "$PF/31.md"
|
||||
|
||||
{
|
||||
printf -- '- %s\n' "$(mkchars 150)"
|
||||
printf -- '- %s\n' "$(mkchars 150)"
|
||||
printf -- '- %s\n' "$(mkchars 150)"
|
||||
} >"$PF/32.md"
|
||||
check "length bound: several within-bound entries pass though the file totals over 300" 0 "" \
|
||||
changelog_fragment_problem "$PF/32.md"
|
||||
|
||||
{
|
||||
printf -- '- %s\n' "$(mkchars 50)"
|
||||
printf ' %s\n' "$(mkchars 50)"
|
||||
printf ' %s\n' "$(mkchars 50)"
|
||||
printf ' %s\n' "$(mkchars 50)"
|
||||
printf ' %s\n' "$(mkchars 50)"
|
||||
} >"$PF/33.md"
|
||||
check "length bound: a ~250-character entry wrapped over four continuation lines passes" 0 "" \
|
||||
changelog_fragment_problem "$PF/33.md"
|
||||
|
||||
{
|
||||
printf '### Added\n\n'
|
||||
printf -- '- %s\n' "$(mkchars 300)"
|
||||
} >"$PF/34.md"
|
||||
check "length bound: a '### ' heading counts toward no entry — 300 under it still passes" 0 "" \
|
||||
changelog_fragment_problem "$PF/34.md"
|
||||
|
||||
{
|
||||
printf '### Added\n\n'
|
||||
printf -- '- %s\n' "$(mkchars 301)"
|
||||
} >"$PF/35.md"
|
||||
check "length bound: a grouped bullet is bounded the same as a flat one" 1 \
|
||||
"35.md' has a 301-character entry" \
|
||||
changelog_fragment_problem "$PF/35.md"
|
||||
|
||||
{
|
||||
printf -- '- %s\n' "$(mkchars 301)"
|
||||
printf -- '- Short.\n'
|
||||
} >"$PF/36.md"
|
||||
assert_single_diagnosis() {
|
||||
local count
|
||||
count="$(changelog_fragment_problem "$PF/36.md" | grep -c "301-character")"
|
||||
[ "$count" = 1 ] || {
|
||||
printf 'wanted one diagnosis, got %s\n' "$count"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
check "length bound: an over-bound entry mid-file is diagnosed exactly once" 0 "" \
|
||||
assert_single_diagnosis
|
||||
|
||||
check "length bound: published sections stay unvalidated — 0.3.0's over-bound entries red nothing" 0 "" \
|
||||
changelog_section_problem "$ROOT/CHANGELOG.md" 0.3.0
|
||||
check "length bound: published sections stay unvalidated — 0.2.0 reds nothing either" 0 "" \
|
||||
changelog_section_problem "$ROOT/CHANGELOG.md" 0.2.0
|
||||
|
||||
# --- the assembler (#114) ----------------------------------------------------
|
||||
|
||||
assert_assemble() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue