@@ -441,6 +441,184 @@ function main({ prePush = false } = {}) {
441441// import — only when this file IS the entry point.
442442const invokedDirectly = isEntrypoint ( import . meta. url ) ;
443443
444+ /* ------------------------------------------------ self-test: battery roster */
445+
446+ // The self-test's own battery roster and floor (#13799).
447+ //
448+ // `noDist && noTree && armed && table && fixture` was this self-test's ONLY
449+ // success condition, so "every sub-check held" and "the sub-checks never ran"
450+ // printed the same line. A callee whose body stops doing its work still returns
451+ // whatever its last surviving statement produces, and the green verdict below
452+ // then claims this file's whole deferred-merge wiring is sound. Closed the way
453+ // the Tier C pilot closed it (PR #15326, `git-merge-regen.mjs`): what is pinned
454+ // is the registered NAMES, not a number.
455+ //
456+ // -- Why the CALLEE NAME is the battery --
457+ //
458+ // This file has no `selfTest()` entry function: the `--self-test` dispatch at
459+ // the bottom invokes THREE named callees, each printing its own section and
460+ // returning a boolean. So the roster's unit is the CALLEE, and its label is the
461+ // one the SOURCE ALREADY CARRIES -- the function's own name. Nothing is invented
462+ // and nothing is judged per comment, and a set difference names WHICH sub-check
463+ // stopped rather than saying only that something did.
464+ // `registerCase('<calleeName>')` is the FIRST statement of every callee, above
465+ // any early return, so what the ledger records is that the callee RAN -- which
466+ // is why each floor is 1 rather than the number of assertions the callee
467+ // happens to contain.
468+ //
469+ // STOP -- the three callees' own inner sinks are NOT batteries here, and they
470+ // are worth naming because all three are different shapes: `fixtureSelfTest`'s
471+ // `check()` helper (14 calls), `decisionTableSelfTest`'s literal 8-row table
472+ // with its driving loop, and `prePushIsArmedSelfTest`'s bare boolean. Recipe A
473+ // (PR #15271, `check-sdui-manifest`) does make a table row a battery -- for a
474+ // file whose SELF-TEST *is* the table: one literal table, one driving loop over
475+ // it, one sink. Here the table is a local of ONE callee among three, and that
476+ // callee already reduces its rows to a single returned verdict of its own.
477+ // Flooring those rows would floor one callee's internals while the other two
478+ // stayed at callee granularity -- a roster whose unit changes per entry. The
479+ // rule: the battery is the unit the DISPATCH names.
480+ //
481+ // STOP -- the TWO assertions written INLINE in the dispatch block (`noDist`,
482+ // `noTree`) are deliberately OUTSIDE this roster, and that is a DECLARED GAP,
483+ // not an oversight (ruled Q1 = A on #13799; the pilot and batches 7a/7b set the
484+ // precedent). They are not callees: the dispatch names no unit for them, so
485+ // there is no name the source already carries. Inventing a label would put a
486+ // hand-written string into a roster whose entire property is that every entry
487+ // is read off a declaration, and hoisting them into named callees is a reshape
488+ // with its own card, never a rider here. What the gap costs, stated plainly:
489+ // those two lines can stop running and this floor will not say so.
490+ //
491+ // STOP -- a pinned TOTAL is not the repair: one callee dropping all its work
492+ // keeps a total "right" the moment a sibling grows.
493+ //
494+ // The counts are a FLOOR, not an equality -- a callee that grows a second
495+ // registration must not red. 1 is the honest floor for a callee: the dispatch
496+ // reaches it exactly once per run.
497+ const SELF_TEST_BATTERIES = Object . freeze ( {
498+ prePushIsArmedSelfTest : 1 ,
499+ decisionTableSelfTest : 1 ,
500+ fixtureSelfTest : 1 ,
501+ } ) ;
502+
503+ // DELETING an entry silences that battery's floor exactly as effectively as
504+ // zeroing it, so the roster's own size is pinned too. This pin is also half of
505+ // the duplicate refusal: two dispatch entries naming ONE callee collapse to one
506+ // key in the literal above, so the roster falls below this number; the
507+ // roster/dispatch cross-check in `batteryFloorFailures()` is the other half, and
508+ // it names WHICH callee was listed twice.
509+ const SELF_TEST_BATTERY_FLOOR = 3 ;
510+
511+ // The key a registration is filed under when a callee registers no name at all.
512+ // It is not a declared battery, so it reds by the same set difference rather
513+ // than silently inflating whichever battery registered last.
514+ const UNATTRIBUTED_BATTERY = '(no callee named)' ;
515+
516+ // The battery ledger, read by `batteryFloorFailures()` from the dispatch block
517+ // at the very bottom of this file. It is MODULE-level rather than local to a
518+ // self-test body because this file HAS no self-test body: the registrations
519+ // happen inside three separate callees and the floor is read at the dispatch's
520+ // verdict site, so the ledger has to outlive every one of those frames.
521+ //
522+ // Named for the roster's role, deliberately WITHOUT a self-test spelling:
523+ // `check:pm-dispatch-gates` anchors on a top-level declaration whose NAME spells
524+ // self-test, and every such name owes a row in its COMPOUND_ANCHOR_LEDGER. The
525+ // three callee names above already spell self-test and already carry their rows;
526+ // an object KEY is not a column-0 declaration, so the roster owes no new row.
527+ // `battery` is also the accurate word.
528+ const batterySeen = new Map ( ) ;
529+
530+ /**
531+ * Record that a self-test callee RAN.
532+ *
533+ * Called as the FIRST statement of each of the three callees the `--self-test`
534+ * dispatch invokes -- above any early return, so a callee that bails out early
535+ * still reports that it ran, and the floor is never met by a frame that returned
536+ * before doing anything.
537+ */
538+ function registerCase ( name ) {
539+ const key = name ?? UNATTRIBUTED_BATTERY ;
540+ batterySeen . set ( key , ( batterySeen . get ( key ) ?? 0 ) + 1 ) ;
541+ }
542+
543+ /**
544+ * The floor: every declared callee RAN (#13799).
545+ *
546+ * Evaluated at the dispatch's verdict site -- after all three callees have had
547+ * their chance and immediately before the success line -- and reached only from
548+ * the `--self-test` branch, so a production `pre-commit` / `pre-push` run never
549+ * reads the ledger at all.
550+ *
551+ * @param {string[] } invoked the callee names the dispatch block actually invokes
552+ * @returns {string[] } floor breaches; empty means the floor held
553+ */
554+ function batteryFloorFailures ( invoked ) {
555+ const declared = Object . keys ( SELF_TEST_BATTERIES ) ;
556+ const problems = [ ] ;
557+ if ( declared . length < SELF_TEST_BATTERY_FLOOR ) {
558+ problems . push (
559+ `SELF_TEST_BATTERIES declares ${ declared . length } batteries, below the pinned `
560+ + `${ SELF_TEST_BATTERY_FLOOR } -- a battery deleted from the roster takes its own floor with it.` ,
561+ ) ;
562+ }
563+
564+ // -- Roster vs dispatch, both directions --
565+ // `invoked` is read off the dispatch's own list of callees, so this pair says
566+ // WHICH name lost its counterpart. A declared name nothing invokes would also
567+ // read DID NOT RUN below; naming it here reports the cause (nothing calls it)
568+ // rather than only the symptom (nothing registered).
569+ const duplicated = [ ...new Set ( invoked . filter ( ( name , i ) => invoked . indexOf ( name ) !== i ) ) ] ;
570+ if ( duplicated . length ) {
571+ problems . push (
572+ `the dispatch invokes ${ duplicated . map ( ( n ) => JSON . stringify ( n ) ) . join ( ', ' ) } more than once -- `
573+ + 'two entries naming one callee are ONE battery, so the second can stop running while the '
574+ + 'first keeps the floor met.' ,
575+ ) ;
576+ }
577+ for ( const name of invoked ) {
578+ if ( declared . includes ( name ) ) continue ;
579+ problems . push (
580+ `the dispatch invokes "${ name } ", which is not declared in SELF_TEST_BATTERIES -- a callee `
581+ + 'nothing declares is a sub-check nothing floors.' ,
582+ ) ;
583+ }
584+ for ( const name of declared ) {
585+ if ( invoked . includes ( name ) ) continue ;
586+ problems . push (
587+ `SELF_TEST_BATTERIES declares "${ name } ", which the dispatch block does not invoke -- a floor `
588+ + 'over a battery nothing can reach.' ,
589+ ) ;
590+ }
591+
592+ // -- Roster vs ledger, both directions --
593+ for ( const [ name , count ] of batterySeen ) {
594+ if ( declared . includes ( name ) ) continue ;
595+ problems . push (
596+ `self-test battery "${ name } " registered ${ count } case(s) but is not declared in `
597+ + 'SELF_TEST_BATTERIES -- a case attributed to no declared battery is one nothing floors.' ,
598+ ) ;
599+ }
600+ for ( const name of declared ) {
601+ const count = batterySeen . get ( name ) ?? 0 ;
602+ if ( count >= SELF_TEST_BATTERIES [ name ] ) continue ;
603+ problems . push (
604+ count === 0
605+ ? `self-test battery "${ name } " DID NOT RUN -- 0 cases registered, ${ SELF_TEST_BATTERIES [ name ] } pinned. `
606+ + 'The verdict below would have claimed that sub-check holds.'
607+ : `self-test battery "${ name } " registered ${ count } case(s), below its pinned floor of `
608+ + `${ SELF_TEST_BATTERIES [ name ] } -- cases that used to run no longer do.` ,
609+ ) ;
610+ }
611+
612+ if ( problems . length ) {
613+ problems . push (
614+ 'A battery at or below its floor means a sub-check STOPPED RUNNING -- the battery is the bug, '
615+ + 'not the number. Find what stopped registering (a deleted invocation, a renamed callee, a '
616+ + '`registerCase()` moved below an early return) and restore it.' ,
617+ ) ;
618+ }
619+ return problems ;
620+ }
621+
444622/**
445623 * Replay the deferred-merge sequence against a THROWAWAY git repo (#8047).
446624 *
@@ -456,6 +634,7 @@ const invokedDirectly = isEntrypoint(import.meta.url);
456634 * a full spec build.
457635 */
458636function fixtureSelfTest ( ) {
637+ registerCase ( 'fixtureSelfTest' ) ;
459638 const dir = mkdtempSync ( join ( tmpdir ( ) , 'os-regen-defer-' ) ) ;
460639 const git = ( args , opts = { } ) =>
461640 execFileSync ( 'git' , args , { cwd : dir , encoding : 'utf8' , stdio : [ 'ignore' , 'pipe' , 'pipe' ] , ...opts } ) ;
@@ -606,6 +785,7 @@ function fixtureSelfTest() {
606785 * undischarged deferral leaves the machine in silence.
607786 */
608787function prePushIsArmedSelfTest ( ) {
788+ registerCase ( 'prePushIsArmedSelfTest' ) ;
609789 let mode = '' ;
610790 try {
611791 mode = execFileSync ( 'git' , [ 'ls-files' , '-s' , '.githooks/pre-push' ] , {
@@ -630,6 +810,7 @@ function prePushIsArmedSelfTest() {
630810
631811/** The five `decide` cases, including the ones a fixture cannot reach. */
632812function decisionTableSelfTest ( ) {
813+ registerCase ( 'decisionTableSelfTest' ) ;
633814 const cases = [
634815 [ { blocked : 0 , merging : null , deferral : null } , 'clear' ] ,
635816 [ { blocked : 0 , merging : null , deferral : { head : 'a' } } , 'discharged' ] ,
@@ -658,19 +839,67 @@ if (invokedDirectly) {
658839 if ( process . argv . includes ( '--self-test' ) ) {
659840 // Touches no repo state: the interesting logic is the staleness rules, and
660841 // their dangerous direction is "says fresh when stale".
842+ //
843+ // STOP -- these TWO assertions are written INLINE here rather than inside a
844+ // named callee, so they are outside SELF_TEST_BATTERIES and the floor below
845+ // cannot see them stop running (ruled Q1 = A on #13799). The gap, and why
846+ // labelling or hoisting them is not this card's business, is stated at the
847+ // roster declaration above.
661848 const noDist = distIsStale ( join ( REPO_ROOT , 'scripts' ) ) === true ;
662849 console . log ( `${ noDist ? '✓' : '✗' } a directory with no dist/ reads as STALE (conservative default)` ) ;
663850 const noTree = schemaTreeIsStale ( join ( REPO_ROOT , 'scripts' ) ) === true ;
664851 console . log ( `${ noTree ? '✓' : '✗' } a directory with no json-schema/ reads as STALE (conservative default)` ) ;
665- console . log ( '\ndeferred-merge collection point:' ) ;
666- const armed = prePushIsArmedSelfTest ( ) ;
667- console . log ( '\ndeferred-merge decision table:' ) ;
668- const table = decisionTableSelfTest ( ) ;
669- console . log ( '\ndeferred-merge sequence, replayed on a throwaway repo:' ) ;
670- const fixture = fixtureSelfTest ( ) ;
671- const ok = noDist && noTree && armed && table && fixture ;
672- console . log ( ok ? '\n✓ check-regen-pending self-test passed.' : '\n✗ self-test failed.' ) ;
673- process . exit ( ok ? 0 : 1 ) ;
852+
853+ // The three callees as a literal LIST rather than three bare calls, so the
854+ // names this block invokes are data the floor below can cross-check the
855+ // roster against, in both directions. The names are read off the function
856+ // declarations themselves (`fn.name`), so a renamed callee moves this list
857+ // with it and cannot drift from the roster in silence. Each entry carries
858+ // the section banner that already sat immediately above its call, so what is
859+ // printed is byte-identical to the three banner + `const ... = callee()`
860+ // pairs this replaces.
861+ //
862+ // EVALUATION ORDER AND COMPLETENESS ARE UNCHANGED, on the red path too. The
863+ // original invoked all three eagerly -- one `const` per callee, executed
864+ // unconditionally -- and only THEN reduced the five booleans with `&&`, so
865+ // the short-circuit was always over VALUES, never over calls. `.map()` runs
866+ // the same three in the same order: nothing that used to run stops, and
867+ // nothing that used to be skipped now runs.
868+ const callees = [
869+ [ '\ndeferred-merge collection point:' , prePushIsArmedSelfTest ] ,
870+ [ '\ndeferred-merge decision table:' , decisionTableSelfTest ] ,
871+ [ '\ndeferred-merge sequence, replayed on a throwaway repo:' , fixtureSelfTest ] ,
872+ ] ;
873+ const results = callees . map ( ( [ banner , run ] ) => {
874+ console . log ( banner ) ;
875+ return run ( ) ;
876+ } ) ;
877+
878+ // -- The assertion floor, at the verdict site --
879+ // There is no verdict site inside a self-test body here, because there is no
880+ // self-test body: this dispatch IS the verdict site, and the reduction below
881+ // is the verdict. So the floor is evaluated here, after every callee has had
882+ // its chance and immediately before the success line -- the only place a run
883+ // in which a callee never ran can still be stopped from reporting that the
884+ // self-test passed. It sits inside the `--self-test` branch, so the
885+ // production path (`main()`, which `pre-commit` and `pre-push` spawn) never
886+ // reads the ledger.
887+ const floorBreaches = batteryFloorFailures ( callees . map ( ( [ , run ] ) => run . name ) ) ;
888+ for ( const breach of floorBreaches ) console . error ( `✗ self-test floor: ${ breach } ` ) ;
889+
890+ // The verdict is an explicit BRANCH rather than a ternary over
891+ // `process.exit`, so the failure path literally IS a `process.exit(1)`.
892+ // That is what `measure-self-test-floor.mjs` reads as a floor that PRODUCES
893+ // A FAILURE rather than one that merely names a roster: this file's previous
894+ // `process.exit(ok ? 0 : 1)` classified NONE on that instrument even with a
895+ // sound floor above it. The success line's text is unchanged.
896+ const failures = [ noDist , noTree , ...results ] . filter ( ( ok ) => ! ok ) . length + floorBreaches . length ;
897+ if ( failures > 0 ) {
898+ console . log ( `\n✗ self-test failed -- ${ failures } failure(s) (cases and floor).` ) ;
899+ process . exit ( 1 ) ;
900+ }
901+ console . log ( '\n✓ check-regen-pending self-test passed.' ) ;
902+ process . exit ( 0 ) ;
674903 }
675904 process . exit ( main ( { prePush : process . argv . includes ( '--pre-push' ) } ) ) ;
676905}
0 commit comments