fix: the sentinel's one-line contract is checked on the file, not the $(cat) word

Command substitution strips every trailing newline, so 'grouped\n\n'
reached the case as a clean word and passed — the round's shared blocker
(codex, grok). A line count taken from the file itself now refuses any
physically multi-line sentinel before the word check runs, with the
existing diagnosis naming the file. Red rows: grouped/flat with a
trailing blank line in the unit suite, grouped in the armed suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
claude-bot-andresmgsl 2026-07-25 13:29:02 +00:00
parent be666ebed6
commit 0b158a6917
3 changed files with 22 additions and 0 deletions

View file

@ -215,6 +215,13 @@ changelog_shape_problem() {
local sentinel="$dir/shape" declared="" local sentinel="$dir/shape" declared=""
if [ -f "$sentinel" ]; then if [ -f "$sentinel" ]; then
# The one-line contract is checked on the file itself: command
# substitution strips every trailing newline, so the captured word
# cannot tell 'grouped' from 'grouped' plus blank lines.
if [ "$(wc -l <"$sentinel")" -gt 1 ]; then
printf "'%s' declares neither shape — its whole content must be 'flat' or 'grouped', one line\n" "$sentinel"
return 1
fi
declared="$(cat "$sentinel")" declared="$(cat "$sentinel")"
case "$declared" in case "$declared" in
flat | grouped) ;; flat | grouped) ;;

View file

@ -404,6 +404,11 @@ check "fragment mode: a malformed sentinel is refused, file named" 1 \
"'changelog.d/shape' declares neither shape" \ "'changelog.d/shape' declares neither shape" \
in_tree fragments-dev-flip in_tree fragments-dev-flip
printf 'grouped\n\n' >"$TMP/fragments-dev-flip/changelog.d/shape"
check "fragment mode: a sentinel with a trailing blank line is refused, file named" 1 \
"'changelog.d/shape' declares neither shape" \
in_tree fragments-dev-flip
fragment_tree fragments-dev-no-published 1.2.4-dev <<'EOF' fragment_tree fragments-dev-no-published 1.2.4-dev <<'EOF'
# Changelog # Changelog

View file

@ -540,6 +540,16 @@ printf 'grouped\nflat\n' >"$SHAPE_DIR/shape"
check "shape: a two-line sentinel is refused" 1 \ check "shape: a two-line sentinel is refused" 1 \
"'$SHAPE_DIR/shape' declares neither shape" \ "'$SHAPE_DIR/shape' declares neither shape" \
changelog_shape_problem "$SHAPE_CHANGELOG" "$SHAPE_DIR" changelog_shape_problem "$SHAPE_CHANGELOG" "$SHAPE_DIR"
# Trailing blank lines are the case command substitution launders away: the
# captured word is a clean 'grouped', only the file's line count still knows.
printf 'grouped\n\n' >"$SHAPE_DIR/shape"
check "shape: 'grouped' with a trailing blank line is refused, file named" 1 \
"'$SHAPE_DIR/shape' declares neither shape" \
changelog_shape_problem "$SHAPE_CHANGELOG" "$SHAPE_DIR"
printf 'flat\n\n' >"$SHAPE_DIR/shape"
check "shape: 'flat' with a trailing blank line is refused, file named" 1 \
"'$SHAPE_DIR/shape' declares neither shape" \
changelog_shape_problem "$SHAPE_CHANGELOG" "$SHAPE_DIR"
# The sentinel is not a fragment (#182 D3): the *.md glob is the mechanism, # The sentinel is not a fragment (#182 D3): the *.md glob is the mechanism,
# but the assertion is on the list itself, so a glob change cannot silently # but the assertion is on the list itself, so a glob change cannot silently