@@ -394,21 +394,61 @@ export async function requireDefaultExport(specifier, load, importerUrl, options
394394 * The shared frame, in `check-i18n-coverage.mjs`'s wording and order: what is
395395 * unmet, why, the command that clears it, and — load-bearing — that nothing was
396396 * measured, so the exit code says nothing about the gate's actual question.
397+ *
398+ * ── The pipe-shape advisory, and why it says what it says ──────────────────
399+ *
400+ * Every importer of this module inherits the closing paragraph verbatim, so a
401+ * wrong claim there is wrong in every gate at once. Measured here 2026-08-31 on
402+ * node 22.22.2 / bash 5, one refusing gate plus constructed producers:
403+ *
404+ * node scripts/check-test-completeness.mjs -> 3 (no pipe)
405+ * … 2>&1 | tail -4 $? = 0 ${PIPESTATUS[0]} = 3 pipefail -> 3
406+ * … 2>&1 | head -1 $? = 0 ${PIPESTATUS[0]} = 3 pipefail -> 3
407+ *
408+ * So the false green is `$?` after ANY pipe — it is the LAST command's status,
409+ * and `head`/`tail` both essentially never fail. It is NOT a property of one
410+ * shape, and choosing a different shape does not repair it.
411+ *
412+ * ⚠️ `| head -N` does close the read end early and the producer DOES take EPIPE
413+ * — proven by a producer that prints what it caught — but node ignores SIGPIPE
414+ * and swallows the stdout write error, so the gate still reaches its own exit:
415+ * a producer instrumented to exit 7 reported `${PIPESTATUS[0]}` = 7 through
416+ * `| head -1`. ⛔ Do NOT write that `| head` turns `${PIPESTATUS[0]}`/`pipefail`
417+ * green; it does not, and an earlier draft of this advisory said so. What `head`
418+ * really costs is the VERDICT TEXT (truncated), and — for a producer that does
419+ * not ignore SIGPIPE, unlike node — a real code replaced by 141: `seq 1
420+ * 100000000 | head -1` reports `${PIPESTATUS[0]}` = 141. That is a false RED,
421+ * the opposite direction.
422+ *
423+ * `selfTest` pins the four load-bearing clauses below.
397424 */
398425export function reportPrerequisiteNotMet ( importerUrl , verdict , measures ) {
426+ console . error ( prerequisiteNotMetText ( importerUrl , verdict , measures ) ) ;
427+ process . exit ( 1 ) ;
428+ }
429+
430+ /**
431+ * The text `reportPrerequisiteNotMet` prints, as a value — so the self-test can
432+ * assert on the advisory without spawning a process or stubbing `process.exit`.
433+ */
434+ function prerequisiteNotMetText ( importerUrl , verdict , measures ) {
399435 const gate = fileURLToPath ( importerUrl ) . split ( '/' ) . pop ( ) . replace ( / \. m j s $ / , '' ) ;
400436 const subject = measures ? `whether ${ measures } ` : `what it gates` ;
401- console . error (
437+ return (
402438 `\n${ gate } : PREREQUISITE NOT MET — ${ verdict . headline } \n\n` +
403439 verdict . detail . map ( ( l ) => ( l ? ` ${ l } ` : '' ) ) . join ( '\n' ) +
404440 `\n\n Fix: ${ verdict . fix } \n\n` +
405441 ` Nothing was measured: this gate exited before running a single check, so this\n` +
406442 ` result says NOTHING about ${ subject } . It is NOT a finding, and it is not\n` +
407443 ` evidence that anything in the tree is wrong.\n` +
408- ` (Exit code 1 — but piping this gate reports the PIPE's status, so\n` +
409- ` \`node scripts/${ gate } .mjs | tail -4\` reads green either way. Use \`echo "EXIT=$?"\`.)` ,
444+ ` (Exit code 1 — capture it BEFORE any pipe:\n` +
445+ ` \`node scripts/${ gate } .mjs > /tmp/${ gate } .log 2>&1; echo "EXIT=$?"\`.\n` +
446+ ` Piped, \`$?\` is the LAST command's status, and \`head\`/\`tail\` essentially never fail — that\n` +
447+ ` is the false green, and no pipe shape repairs it. \`\${PIPESTATUS[0]}\`/\`pipefail\` do recover\n` +
448+ ` this gate's own code: \`| tail\` reads to EOF and forwards it, while \`| head -N\` closes the\n` +
449+ ` read end early — the gate takes EPIPE, its verdict text is TRUNCATED, and a producer that\n` +
450+ ` dies on SIGPIPE reports 141 rather than what it meant to say.)`
410451 ) ;
411- process . exit ( 1 ) ;
412452}
413453
414454// ---------------------------------------------------------------------------
@@ -586,6 +626,32 @@ export function selfTest() {
586626 rmSync ( dir , { recursive : true , force : true } ) ;
587627 }
588628
629+ // ── the inherited pipe-shape advisory ───────────────────────────────────
630+ // Pinned HERE and nowhere else, because this is the one copy 45 importers
631+ // print. The clauses are the four the advisory is for; the negative one is
632+ // the load-bearing one, since the wrong claim it excludes reads perfectly
633+ // plausible and shipped once already.
634+ const advisory = prerequisiteNotMetText (
635+ new URL ( 'file:///repo/scripts/check-fixture-gate.mjs' ) . href ,
636+ { headline : 'h' , detail : [ 'd' ] , fix : 'f' } ,
637+ undefined ,
638+ ) ;
639+ t ( 'the advisory prescribes capturing the code BEFORE any pipe' ,
640+ advisory . includes ( 'capture it BEFORE any pipe' ) && advisory . includes ( '> /tmp/check-fixture-gate.log 2>&1' ) ,
641+ advisory ) ;
642+ t ( 'the advisory names the shape-independent false green: `$?` is the LAST command\'s status' ,
643+ advisory . includes ( "`$?` is the LAST command's status" ) && advisory . includes ( 'no pipe shape repairs it' ) ) ;
644+ t ( 'the advisory keeps `| tail` as the shape that FORWARDS the true status' ,
645+ advisory . includes ( '`| tail` reads to EOF and forwards it' ) ) ;
646+ t ( 'the advisory names `| head -N` and the EPIPE mechanism, with its real cost' ,
647+ advisory . includes ( '`| head -N` closes the' ) && advisory . includes ( 'EPIPE' )
648+ && advisory . includes ( 'TRUNCATED' ) && advisory . includes ( '141' ) ) ;
649+ // ⛔ The claim this gate must never make again: measured 2026-08-31, `| head`
650+ // does NOT defeat `${PIPESTATUS[0]}`/`pipefail` — node ignores SIGPIPE and
651+ // reaches its own exit. See the mechanism note on `reportPrerequisiteNotMet`.
652+ t ( 'the advisory does NOT claim a pipe shape defeats `${PIPESTATUS[0]}`/`pipefail`' ,
653+ ! / t u r n s e v e n .* P I P E S T A T U S .* g r e e n | r e a d s g r e e n e i t h e r w a y / . test ( advisory ) ) ;
654+
589655 const failed = cases . filter ( ( c ) => ! c . ok ) ;
590656 for ( const c of failed ) console . error ( ` ✗ ${ c . name } ${ c . detail ? ` -- ${ c . detail } ` : '' } ` ) ;
591657 if ( failed . length ) {
0 commit comments