diff --git a/.github/scripts/drill-recorded.sh b/.github/scripts/drill-recorded.sh index ee77c27..514e986 100755 --- a/.github/scripts/drill-recorded.sh +++ b/.github/scripts/drill-recorded.sh @@ -95,15 +95,31 @@ esac # humans reading the log. # # grab is re-armed by every '## ' line, so the section ends at the next one — -# a record cannot borrow the body of the record below it. sed drops the blank -# padding under the heading and the command substitution eats the trailing -# blanks, which is what makes "present but EMPTY" distinguishable from -# "present with content": a heading with nothing but whitespace under it -# extracts to the empty string. -record="$(awk -v ver="$ver" ' - /^## / { grab = ($2 == "Release" && $3 == "drill" && $4 == "—" && $5 == ver); next } - grab { print } -' "$runs" | sed '/./,$!d')" +# a record cannot borrow the body of the record below it. +# +# `grab && NF` is the non-blank rule, and it is load-bearing rather than +# tidiness. NF is 0 on a line that is empty OR contains only whitespace, so +# `record` is non-empty exactly when a line with real content exists. The +# first cut of this piped through `sed '/./,$!d'` and the comment here claimed +# "a heading with nothing but whitespace under it extracts to the empty +# string" — which is precisely what that pipeline did NOT guarantee, because +# `.` matches a space. A heading followed by one tab satisfied the gate. The +# comment documented the intended contract and the code did not meet it, which +# on a gate is the whole ballgame: an evidence-free release for the price of an +# invisible character. Found by all three reviewers on #138, independently. +# +# The '(NF == 5 || $6 == dash)' tail constraint keeps this in step with box's +# twin: without it '## Release drill — 0.2.0 stray words' counts as a record +# here and does not there. Two sibling guards disagreeing about what the same +# heading means is the same trap as disagreeing with release-notes.sh. +record="$(awk -v ver="$ver" -v dash="—" ' + /^## / { + grab = ($2 == "Release" && $3 == "drill" && $4 == dash && $5 == ver \ + && (NF == 5 || $6 == dash)) + next + } + grab && NF { print } +' "$runs")" if [ -z "$record" ]; then { diff --git a/test/release.test.ts b/test/release.test.ts index 8e7457b..3ceb3d3 100644 --- a/test/release.test.ts +++ b/test/release.test.ts @@ -798,6 +798,30 @@ describe("drill-recorded.sh — a release version has a drill record", () => { expect(r.output).toContain("no drill record for version '0.2.0'"); }); + // ...and whitespace is not evidence either. The first cut extracted with + // `sed '/./,$!d'`, where `.` matches a space, so a heading followed by one + // tab passed the gate — while the comment above the extractor claimed the + // opposite. An evidence-free release for the price of an invisible + // character, on the one check whose whole job is to demand evidence. + // Found independently by all three reviewers on #138. + it("a record body of only spaces and tabs fails (#138)", async () => { + const runs = `# Drill runs\n\n${heading("0.2.0")}\n \n\t\n\n## Notes\n\nUnrelated.\n`; + const r = await check(treeWith("blank", "0.2.0", runs)); + expect(r.code).toBe(1); + expect(r.output).toContain("no drill record for version '0.2.0'"); + }); + + // The sibling guards must agree about what a heading IS, not just about the + // version in it. box#149 requires the version to be the last field or be + // followed by the em dash; without the same constraint here, this heading is + // a record in cast and not in box. + it("a stray tail after the version is not a heading — in step with box#149", async () => { + const runs = `# Drill runs\n\n## Release drill — 0.2.0 stray words\n${legs}`; + const r = await check(treeWith("tail", "0.2.0", runs)); + expect(r.code).toBe(1); + expect(r.output).toContain("no drill record for version '0.2.0'"); + }); + // The version is matched WHOLE, both directions — release-notes.sh's trap, // solved the same way (awk field equality, no regex, no dot-escaping). A // release candidate's drill is not the release's drill: different tree,