@@ -113,6 +113,117 @@ import { fileURLToPath } from 'node:url';
113113import { execFileSync } from 'node:child_process' ;
114114import { isEntrypoint } from './invoked-as.mjs' ;
115115
116+ // ── The self-test's own battery roster and floor (#13489) ──────────────────
117+ //
118+ // This self-test used to decide success by "no failure was recorded" and
119+ // nothing else, so "every case held" and "the cases never ran" printed the same
120+ // line. Closed the way PR #13487 validated on check-doc-authoring: what is
121+ // pinned is the registered NAMES, not a number. Every section opens with
122+ // `battery('<name>')`, every assertion is attributed to the battery most
123+ // recently opened, and the floor requires the OPENED set to equal the DECLARED
124+ // set with each battery at or above its own count.
125+ //
126+ // ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 keeps
127+ // a total "right" the moment a sibling grows. A set difference says WHICH
128+ // battery stopped; a count says only that something did.
129+ //
130+ // The counts are a FLOOR, not an equality — adding cases is ordinary work and
131+ // must not red. A battery BELOW its floor means cases stopped running; the
132+ // remedy is to find what stopped registering.
133+ //
134+ // The machinery lives HERE, at module scope, rather than inside the self-test:
135+ // this self-test's assertion sink is not a block-bodied helper in its body (it
136+ // is a concise arrow, or a module-scope function), so there is no in-body
137+ // helper to thread a per-run ledger through. Module scope is safe because the
138+ // self-test runs once per process, and it is what lets the existing sink route
139+ // through `registerCase()` with no case rewritten and no assertion changed.
140+ const SELF_TEST_BATTERIES = Object . freeze ( {
141+ 'the id derivation, against a scratch tree (no repo state)' : 12 ,
142+ 'a KNOWN-BAD literal: a command id that resolves to nothing' : 3 ,
143+ 'the delimiter rule: the six measured noise shapes stay OUT' : 6 ,
144+ 'the exemption ledger is site-scoped, not blanket' : 3 ,
145+ 'the ledger self-retires: a listed entry that stops reproducing REDS' : 3 ,
146+ 'the dispatch-gates declaration (#12016\'s own landing obligation)' : 5 ,
147+ 'bin names come from declared data' : 3 ,
148+ 'the live repo returns a verdict, and it is green' : 4 ,
149+ } ) ;
150+
151+ // DELETING an entry silences that battery's floor exactly as effectively as
152+ // zeroing it, so the roster's own size is pinned too.
153+ const SELF_TEST_BATTERY_FLOOR = 8 ;
154+
155+ // The key an assertion is filed under when no battery is open. It is not a
156+ // declared battery, so it reds by the same set difference rather than silently
157+ // inflating whichever battery happened to run last.
158+ const UNATTRIBUTED_BATTERY = '(no battery open)' ;
159+
160+ // ⚠️ None of these helpers is named with a self-test spelling, deliberately and
161+ // on the record: `check:pm-dispatch-gates` anchors on a top-level declaration
162+ // whose NAME spells self-test, and every such name owes a row in that gate's
163+ // COMPOUND_ANCHOR_LEDGER. These are the battery ROSTER's machinery -- they hold
164+ // no fixtures to mask and read no path literal -- so the accurate name is the
165+ // one that says `battery`, not the one that would owe a ledger row for a role
166+ // this code does not have.
167+
168+ /** Cases registered per battery: `battery()` opens one, `registerCase()` files into it. */
169+ const batteryCases = new Map ( ) ;
170+ let openBattery = null ;
171+
172+ /** Open a battery. Every assertion after this line is attributed to it. */
173+ function battery ( name ) {
174+ openBattery = name ;
175+ }
176+
177+ /** Called by the self-test's own assertion sink, once per assertion. */
178+ function registerCase ( ) {
179+ const name = openBattery ?? UNATTRIBUTED_BATTERY ;
180+ batteryCases . set ( name , ( batteryCases . get ( name ) ?? 0 ) + 1 ) ;
181+ }
182+
183+ /**
184+ * The floor: every declared battery RAN, and ran its cases (#13489).
185+ *
186+ * Evaluated after every battery has had its chance and BEFORE the verdict, so
187+ * the success line can only be printed by a run in which the set of batteries
188+ * that registered assertions EQUALS the set declared.
189+ */
190+ function batteryFloorFailures ( ) {
191+ const declared = Object . keys ( SELF_TEST_BATTERIES ) ;
192+ const problems = [ ] ;
193+ if ( declared . length < SELF_TEST_BATTERY_FLOOR ) {
194+ problems . push (
195+ `SELF_TEST_BATTERIES declares ${ declared . length } batteries, below the pinned `
196+ + `${ SELF_TEST_BATTERY_FLOOR } — a battery deleted from the roster takes its own floor with it.` ,
197+ ) ;
198+ }
199+ for ( const [ name , count ] of batteryCases ) {
200+ if ( declared . includes ( name ) ) continue ;
201+ problems . push (
202+ `self-test battery "${ name } " registered ${ count } case(s) but is not declared in `
203+ + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.' ,
204+ ) ;
205+ }
206+ for ( const name of declared ) {
207+ const count = batteryCases . get ( name ) ?? 0 ;
208+ if ( count >= SELF_TEST_BATTERIES [ name ] ) continue ;
209+ problems . push (
210+ count === 0
211+ ? `self-test battery "${ name } " DID NOT RUN — 0 cases registered, ${ SELF_TEST_BATTERIES [ name ] } pinned. `
212+ + 'The verdict below would have claimed those cases hold.'
213+ : `self-test battery "${ name } " registered ${ count } case(s), below its pinned floor of `
214+ + `${ SELF_TEST_BATTERIES [ name ] } — cases that used to run no longer do.` ,
215+ ) ;
216+ }
217+ if ( problems . length ) {
218+ problems . push (
219+ 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the '
220+ + 'number. Find what stopped registering (an early return, a deleted block, a guard that now '
221+ + 'skips) and restore it.' ,
222+ ) ;
223+ }
224+ return problems ;
225+ }
226+
116227const REPO_ROOT = join ( dirname ( fileURLToPath ( import . meta. url ) ) , '..' ) ;
117228const OCLIF_COMMANDS_DIR = 'src/commands' ;
118229
@@ -453,9 +564,13 @@ let selfTestReachedVerdict = false;
453564
454565function selfTest ( ) {
455566 const cases = [ ] ;
456- const t = ( name , ok , detail = '' ) => cases . push ( { name, ok, detail } ) ;
567+ const t = ( name , ok , detail = '' ) => {
568+ registerCase ( ) ;
569+ return cases . push ( { name, ok, detail } ) ;
570+ } ;
457571
458572 // -- the id derivation, against a scratch tree (no repo state) --------------
573+ battery ( 'the id derivation, against a scratch tree (no repo state)' ) ;
459574 const dir = mkdtempSync ( join ( tmpdir ( ) , 'cli-cmd-ids-' ) ) ;
460575 try {
461576 const cmds = join ( dir , 'src' , 'commands' ) ;
@@ -499,6 +614,7 @@ function selfTest() {
499614 }
500615
501616 // -- a KNOWN-BAD literal: a command id that resolves to nothing -------------
617+ battery ( 'a KNOWN-BAD literal: a command id that resolves to nothing' ) ;
502618 const ids = new Set ( [ 'migrate apply' , 'migrate' , 'build' ] ) ;
503619 const topics = new Set ( [ 'migrate' ] ) ;
504620 const bad = literalsOn ( "throw new Error('run \"os migrate nonexistent-command\" first');" , [ 'os' ] ) ;
@@ -508,6 +624,7 @@ function selfTest() {
508624 resolveId ( literalsOn ( '`os migrate apply`' , [ 'os' ] ) [ 0 ] . words , ids , topics ) === 'migrate apply' ) ;
509625
510626 // -- the delimiter rule: the six measured noise shapes stay OUT ------------
627+ battery ( 'the delimiter rule: the six measured noise shapes stay OUT' ) ;
511628 t ( 'Spanish prose ("envios diarios") is not a literal' , literalsOn ( "label: 'Limite de envios diarios'," , [ 'os' ] ) . length === 0 ) ;
512629 t ( 'a Python import example is not a literal' , literalsOn ( "import os from 'os';" , [ 'os' ] ) . length === 0 ) ;
513630 t ( 'an unquoted sentence is not a literal' , literalsOn ( '`carry an os validate-clean security posture`,' , [ 'os' ] ) . length === 0 ) ;
@@ -516,11 +633,13 @@ function selfTest() {
516633 t ( 'a bin name at a quote IS a literal' , literalsOn ( 'via "os migrate apply --allow-destructive".' , [ 'os' ] ) . length === 1 ) ;
517634
518635 // -- the exemption ledger is site-scoped, not blanket ----------------------
636+ battery ( 'the exemption ledger is site-scoped, not blanket' ) ;
519637 t ( 'a declared fixture is exempt' , isExempt ( 'scripts/docs-audit/check-drift-comment.mjs' , 'os demo' ) ) ;
520638 t ( 'the SAME text elsewhere is NOT exempt' , ! isExempt ( 'packages/drivers/driver-sql/src/schema-drift.ts' , 'os demo' ) ) ;
521639 t ( 'a DIFFERENT text in an exempt file is NOT exempt' , ! isExempt ( 'scripts/docs-audit/check-drift-comment.mjs' , 'os migrate gone' ) ) ;
522640
523641 // -- the ledger self-retires: a listed entry that stops reproducing REDS ---
642+ battery ( 'the ledger self-retires: a listed entry that stops reproducing REDS' ) ;
524643 t ( 'every ledger entry reproduces in the live scan' , audit ( ) . stale . length === 0 ,
525644 audit ( ) . stale . map ( ( e ) => `${ e . file } "${ e . text } "` ) . join ( '; ' ) ) ;
526645 t ( 'a fabricated ledger entry would be reported stale' ,
@@ -546,6 +665,7 @@ function selfTest() {
546665 // forever and pays itself out as a dev dispatched on a scripts/ card with this gate
547666 // missing from the brief. Both directions are pinned, and both matter — a missing
548667 // declaration is a silent gate, a surplus one is a lying gate.
668+ battery ( 'the dispatch-gates declaration (#12016\'s own landing obligation)' ) ;
549669 const separatorless = POPULATION_ROOTS . filter ( ( r ) => ! r . includes ( '/' ) ) ;
550670 t ( 'every whole-root population entry is declared as a subtree (a bare root is refused by '
551671 + 'hintCovers as too generic, so it needs the `<root>/**` spelling)' ,
@@ -588,11 +708,13 @@ function selfTest() {
588708 && new Set ( audit ( ) . resolved . filter ( ( x ) => x . file . startsWith ( 'scripts/' ) ) . map ( ( x ) => x . file ) ) . size >= 5 ) ;
589709
590710 // -- bin names come from declared data ------------------------------------
711+ battery ( 'bin names come from declared data' ) ;
591712 t ( 'oclif.bin is read' , binNamesOf ( { oclif : { bin : 'os' } } ) . includes ( 'os' ) ) ;
592713 t ( 'bin keys join it' , binNamesOf ( { oclif : { bin : 'os' } , bin : { objectstack : './bin/run.js' } } ) . includes ( 'objectstack' ) ) ;
593714 t ( 'a package with no oclif block declares no bins' , binNamesOf ( { bin : { foo : 'x' } } ) . length === 0 ) ;
594715
595716 // -- the live repo returns a verdict, and it is green ----------------------
717+ battery ( 'the live repo returns a verdict, and it is green' ) ;
596718 const live = audit ( ) ;
597719 t ( 'the live audit returns a verdict' , live . refusal === null , live . refusal ?? '' ) ;
598720 t ( 'the live repo has at least one CLI package' , live . refusal === null && live . clis . length >= 1 ) ;
@@ -602,6 +724,10 @@ function selfTest() {
602724 live . refusal === null && live . resolved . some ( ( x ) =>
603725 x . file === 'packages/drivers/driver-sql/src/schema-drift.ts' && x . text === 'os migrate multi-value-columns' ) ) ;
604726
727+ // The floor runs BEFORE the verdict below, so a success line can only be
728+ // printed by a run in which every declared battery registered its cases.
729+ for ( const message of batteryFloorFailures ( ) ) cases . push ( { name : message , ok : false , detail : '' } ) ;
730+
605731 const failed = cases . filter ( ( c ) => ! c . ok ) ;
606732 for ( const c of failed ) console . error ( ` ✗ ${ c . name } ${ c . detail ? ` -- ${ c . detail } ` : '' } ` ) ;
607733 if ( failed . length ) {
0 commit comments