@@ -478,6 +478,63 @@ function report(problems) {
478478// handshake is a flag rather than a returned sentinel.
479479let selfTestReachedVerdict = false ;
480480
481+ // ── The self-test's own battery roster and floor (#13489) ──────────────────
482+ //
483+ // A zero failure count used to be this self-test's ONLY success condition, so
484+ // "every case held" and "the cases never ran" printed the same line. Closed the
485+ // PR #13487 way: what is pinned is the registered NAMES, not a number.
486+ //
487+ // This self-test is TABLE-DRIVEN — one literal `cases` table, one loop over it,
488+ // and a sink (`failed += 1`) that writes only when a case FAILS. Routing THAT
489+ // sink through `registerCase()` would register a case only when it fails: a
490+ // fully green run would register 0 and every battery would read DID NOT RUN, the
491+ // floor inverted rather than installed. So the roster is the table's own rows.
492+ // Each row's own `label` is a declared battery, verbatim, with a floor of 1, and
493+ // `registerCase()` is the FIRST statement of the driving loop body — so the case
494+ // is attributed to the row actually being run, whatever that row asserts
495+ // afterwards. It stays first even where the body carries `continue`: the floor
496+ // asserts REACH, and placing it after a guard reintroduces the very inversion
497+ // this shape exists to avoid. There is no `battery()` opener: for a table-driven
498+ // self-test the ROW is the battery, so attribution is the loop variable rather
499+ // than a most-recently-opened section.
500+ //
501+ // ⛔ A pinned TOTAL is not the repair, and neither is a roster DERIVED from the
502+ // table: `cases.length` moves with the table, so a deleted row would delete its
503+ // own floor. The roster below is a LITERAL the table is checked against, which
504+ // is what lets a deleted or renamed row name ITSELF in the refusal.
505+ //
506+ // The counts are a FLOOR, not an equality — a row that grows into several
507+ // registrations must not red. 1 is the honest floor for a table row: the loop
508+ // reaches it exactly once per run.
509+ const SELF_TEST_BATTERIES = Object . freeze ( {
510+ 'the landed wording (exact 17.x pin) → GREEN' : 1 ,
511+ 'R1 — a stale major (17.x → 16.x, the #5245 drift) → RED naming file/declared/actual/fix' : 1 ,
512+ 'R3 — no `compatibility:` key at all → RED (absence is never a skip, #4690)' : 1 ,
513+ 'an empty `compatibility:` value → RED' : 1 ,
514+ 'no frontmatter at all → RED' : 1 ,
515+ 'R4 — wording switched to an unpinned range (#5245 option ②) → RED, not a silent no-op' : 1 ,
516+ 'R4b — zero pins repo-wide with every file exempt → RED via the anti-no-op assertion' : 1 ,
517+ 'R5 — an exemption naming a file that is not scanned → RED (anti-dormancy)' : 1 ,
518+ 'R7 — an exempt file whose written justification is gone → RED (exemption self-invalidates)' : 1 ,
519+ 'an exempt file that grows a pin → RED (the exemption is now dead config)' : 1 ,
520+ 'an exempt file whose pin is ALSO wrong → RED on the pin (exemptions never cover a claim)' : 1 ,
521+ 'R8 — a half-pinned line (one pinned, one bare mention) → RED on the bare one' : 1 ,
522+ 'a pin naming a package the workspace does not have → RED' : 1 ,
523+ 'R6 — an empty scan → RED, never a green skip (#4690, the whole point)' : 1 ,
524+ 'no workspace packages discovered → RED' : 1 ,
525+ 'multi-package line, both pinned correctly → GREEN' : 1 ,
526+ 'wording reflowed around a correct pin → stays GREEN' : 1 ,
527+ 'a prerelease major still reconciles by major (17.0.0-rc.5 ↔ 17.x) → GREEN' : 1 ,
528+ } ) ;
529+
530+ // DELETING an entry silences that battery's floor exactly as effectively as
531+ // zeroing it, so the roster's own size is pinned too. This pin is also half of
532+ // the duplicate-label refusal: two rows sharing a label collapse to ONE key in
533+ // the literal above, so the roster falls below this number; the table
534+ // cross-check in the floor block is the other half, and names WHICH label
535+ // collided.
536+ const SELF_TEST_BATTERY_FLOOR = 18 ;
537+
481538function selfTest ( ) {
482539 console . log ( 'check-skill-compatibility-version self-test\n' ) ;
483540
@@ -643,8 +700,15 @@ function selfTest() {
643700 } ,
644701 ] ;
645702
703+ // The ledger this self-test's floor is evaluated against (#13489).
704+ const batterySeen = new Map ( ) ;
705+ const registerCase = ( name ) => {
706+ batterySeen . set ( name , ( batterySeen . get ( name ) ?? 0 ) + 1 ) ;
707+ } ;
708+
646709 let failed = 0 ;
647710 for ( const c of cases ) {
711+ registerCase ( c . label ) ;
648712 let problems ;
649713 try {
650714 ( { problems } = runAllChecks ( c . files , c . pkgs ?? PKGS , EXEMPT ) ) ;
@@ -755,8 +819,64 @@ function selfTest() {
755819 for ( const f of declFailures ) console . error ( ` ✗ dispatch-gates declaration: ${ f } ` ) ;
756820 failed += declFailures . length ;
757821
822+ // ── The floor: every declared row RAN, and ran its case (#13489) ───────
823+ //
824+ // Evaluated after every row has had its chance and BEFORE the verdict, so the
825+ // success line below can only be printed by a run in which the set of rows
826+ // that registered EQUALS the set declared. A set difference names WHICH row
827+ // stopped; a count says only that something did.
828+ const floorFailure = ( message ) => {
829+ console . error ( `✗ self-test floor: ${ message } ` ) ;
830+ failed += 1 ;
831+ } ;
832+ const declaredBatteries = Object . keys ( SELF_TEST_BATTERIES ) ;
833+ let floorBreached = false ;
834+ if ( declaredBatteries . length < SELF_TEST_BATTERY_FLOOR ) {
835+ floorBreached = true ;
836+ floorFailure (
837+ `SELF_TEST_BATTERIES declares ${ declaredBatteries . length } batteries, below the pinned ` +
838+ `${ SELF_TEST_BATTERY_FLOOR } — a battery deleted from the roster takes its own floor with it.` ,
839+ ) ;
840+ }
841+ const rowLabels = cases . map ( ( c ) => c . label ) ;
842+ const duplicated = [ ...new Set ( rowLabels . filter ( ( name , i ) => rowLabels . indexOf ( name ) !== i ) ) ] ;
843+ if ( duplicated . length > 0 ) {
844+ floorBreached = true ;
845+ floorFailure (
846+ `the cases table uses ${ duplicated . map ( ( n ) => JSON . stringify ( n ) ) . join ( ', ' ) } as a row label more than once — ` +
847+ 'two rows sharing a label are ONE battery, so the second can stop running while the first keeps the floor met.' ,
848+ ) ;
849+ }
850+ for ( const [ name , count ] of batterySeen ) {
851+ if ( declaredBatteries . includes ( name ) ) continue ;
852+ floorBreached = true ;
853+ floorFailure (
854+ `self-test battery "${ name } " registered ${ count } case(s) but is not declared in ` +
855+ 'SELF_TEST_BATTERIES — a case attributed to no declared battery is one nothing floors.' ,
856+ ) ;
857+ }
858+ for ( const name of declaredBatteries ) {
859+ const count = batterySeen . get ( name ) ?? 0 ;
860+ if ( count >= SELF_TEST_BATTERIES [ name ] ) continue ;
861+ floorBreached = true ;
862+ floorFailure (
863+ count === 0
864+ ? `self-test battery "${ name } " DID NOT RUN — 0 cases registered, ${ SELF_TEST_BATTERIES [ name ] } pinned. ` +
865+ 'The verdict below would have claimed that case holds.'
866+ : `self-test battery "${ name } " registered ${ count } case(s), below its pinned floor of ` +
867+ `${ SELF_TEST_BATTERIES [ name ] } — cases that used to run no longer do.` ,
868+ ) ;
869+ }
870+ if ( floorBreached ) {
871+ floorFailure (
872+ 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' +
873+ 'number. Find what stopped registering (a deleted row, a renamed label, a loop that no longer ' +
874+ 'reaches it) and restore it.' ,
875+ ) ;
876+ }
877+
758878 if ( failed > 0 ) {
759- console . error ( `\n✗ check-skill-compatibility-version self-test failed ( ${ failed } case (s)).` ) ;
879+ console . error ( `\n✗ check-skill-compatibility-version self-test: ${ failed } failure (s) (cases and floor ).` ) ;
760880 process . exit ( 1 ) ;
761881 }
762882 console . log ( `\n✓ check-skill-compatibility-version self-test: ${ cases . length } cases pass, plus 7 dispatch-gates declaration cases.` ) ;
0 commit comments