From 0b158a6917ade8e496da21f8ff75c714cca5eff6 Mon Sep 17 00:00:00 2001 From: claude-bot-andresmgsl Date: Sat, 25 Jul 2026 13:29:02 +0000 Subject: [PATCH] fix: the sentinel's one-line contract is checked on the file, not the $(cat) word MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- lib/changelog.sh | 7 +++++++ test/changelog-armed.test.sh | 5 +++++ test/changelog.test.sh | 10 ++++++++++ 3 files changed, 22 insertions(+) diff --git a/lib/changelog.sh b/lib/changelog.sh index a333819..974b4a8 100644 --- a/lib/changelog.sh +++ b/lib/changelog.sh @@ -215,6 +215,13 @@ changelog_shape_problem() { local sentinel="$dir/shape" declared="" 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")" case "$declared" in flat | grouped) ;; diff --git a/test/changelog-armed.test.sh b/test/changelog-armed.test.sh index fdb3700..66c0f88 100644 --- a/test/changelog-armed.test.sh +++ b/test/changelog-armed.test.sh @@ -404,6 +404,11 @@ check "fragment mode: a malformed sentinel is refused, file named" 1 \ "'changelog.d/shape' declares neither shape" \ 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' # Changelog diff --git a/test/changelog.test.sh b/test/changelog.test.sh index 57f7b74..0c9979d 100755 --- a/test/changelog.test.sh +++ b/test/changelog.test.sh @@ -540,6 +540,16 @@ printf 'grouped\nflat\n' >"$SHAPE_DIR/shape" check "shape: a two-line sentinel is refused" 1 \ "'$SHAPE_DIR/shape' declares neither shape" \ 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, # but the assertion is on the list itself, so a glob change cannot silently