fix(changelog): one fragment, one diagnosis, wherever the long entry sits

awk runs END on the way out of an exit from a main rule, so the length
row printed mid-file was followed by the citation row it outranks — the
internal protocol line landing inside the human-facing excerpt. Found by
claude-bot and kimi-bot in #262's first round, independently and with the
same reproduction.

The guard is the reported flag the empty-heading walk in this same
predicate already uses. The fixtures are the axis 57.md could not reach:
its over-bound entry is last, so only END's flush can print. 58.md puts
one before another bullet, 59.md before a heading and after a misplaced
cite. With lib/changelog.sh alone reverted they red, which is what the
green suite was hiding.

Refs #262.
This commit is contained in:
cndgrr 2026-08-04 12:38:13 +00:00
parent 84c73fe22e
commit 75a5b68c8a
3 changed files with 42 additions and 4 deletions

View file

@ -7,6 +7,9 @@
- The refusal distinguishes an entry carrying no reference at all from one - The refusal distinguishes an entry carrying no reference at all from one
whose reference is present but not terminal, and names the shape to whose reference is present but not terminal, and names the shape to
write in both (#262). write in both (#262).
- The 300-character bound still outranks the citation across the whole
fragment, and the outranked problem stays out of the message it lost
to: one fragment, one diagnosis, wherever in the file it sits (#262).
### Changed ### Changed

View file

@ -208,6 +208,7 @@ changelog_fragment_problem() {
sub(/ $/, "", e) sub(/ $/, "", e)
len = length(e) len = length(e)
if (len > max) { if (len > max) {
reported = 1
printf "long\t%d\t%s\n", len, excerpt(e) printf "long\t%d\t%s\n", len, excerpt(e)
return 1 return 1
} }
@ -227,9 +228,15 @@ changelog_fragment_problem() {
} }
/^[[:space:]]*$/ { next } /^[[:space:]]*$/ { next }
entry != "" { entry = entry " " $0 } entry != "" { entry = entry " " $0 }
# An exit from a main rule still runs END, so a length row printed
# mid-file would be followed by the citation row it outranks — two
# lines spliced into one diagnosis, the internal protocol row landing
# inside the human-facing excerpt (#262 round 1). The reported flag is
# the same guard the empty-heading walk above uses, for the same
# reason: one diagnosis per fragment is the contract.
END { END {
if (flush()) exit if (flush()) exit
if (cite_kind != "") printf "%s\t\t%s\n", cite_kind, cite_excerpt if (!reported && cite_kind != "") printf "%s\t\t%s\n", cite_kind, cite_excerpt
} }
' "$file" ' "$file"
)" )"

View file

@ -420,16 +420,44 @@ check "cite: the reference need not match the filename" 0 "" \
check "cite: an over-bound entry outranks an earlier uncited one" 1 \ check "cite: an over-bound entry outranks an earlier uncited one" 1 \
"57.md' has a 301-character entry" \ "57.md' has a 301-character entry" \
changelog_fragment_problem "$PF/57.md" changelog_fragment_problem "$PF/57.md"
assert_one_diagnosis_57() { assert_one_diagnosis() {
local count local count
count="$(changelog_fragment_problem "$PF/57.md" | wc -l)" count="$(changelog_fragment_problem "$PF/$1.md" | wc -l)"
[ "$count" = 1 ] || { [ "$count" = 1 ] || {
printf 'wanted one diagnosis, got %s\n' "$count" printf 'wanted one diagnosis, got %s\n' "$count"
return 1 return 1
} }
} }
check "cite: the outranked citation problem is not reported beside it" 0 "" \ check "cite: the outranked citation problem is not reported beside it" 0 "" \
assert_one_diagnosis_57 assert_one_diagnosis 57
# The axis 57.md cannot test: its over-bound entry is LAST, so the only
# flush that can print is END's, which exits immediately. A flush from a
# main rule exits too — but awk runs END on the way out, so the citation
# row a mid-file length row outranks would print after it unless END is
# guarded. Both ways out of the walk, a bullet and a heading.
{
printf -- '- Uncited, and it comes first.\n'
printf -- '- %s\n' "$(mkchars 301)"
printf -- '- A later entry the walk never reaches (#57).\n'
} >"$PF/58.md"
check "cite: an over-bound entry that is not the last one still reports the bound" 1 \
"58.md' has a 301-character entry" \
changelog_fragment_problem "$PF/58.md"
check "cite: and it is still one diagnosis, not the protocol row spliced into it" 0 "" \
assert_one_diagnosis 58
{
printf '### Fixed\n'
printf -- '- Misplaced, and it comes first. (#59)\n'
printf -- '- %s\n' "$(mkchars 301)"
printf '### Changed\n'
printf -- '- The heading is the other way out of the walk (#59).\n'
} >"$PF/59.md"
check "cite: a heading after the over-bound entry is the same one diagnosis" 1 \
"59.md' has a 301-character entry" \
changelog_fragment_problem "$PF/59.md"
check "cite: the misplaced row does not ride along with it either" 0 "" \
assert_one_diagnosis 59
# Published sections keep their pre-rule prose (D4): reddening history is a # Published sections keep their pre-rule prose (D4): reddening history is a
# wall, not a guard. Every shipped section predates the cite. # wall, not a guard. Every shipped section predates the cite.