@@ -278,6 +278,175 @@ function drive(argv) {
278278
279279/* ------------------------------------------------------------------ self-test */
280280
281+ // ── The self-test's own battery roster and floor (#13489) ──────────────────
282+ //
283+ // `results.every(Boolean)` was this self-test's ONLY success condition, so
284+ // "every sub-check held" and "the sub-checks never ran" printed the same line.
285+ // Closed the PR #13487 way: what is pinned is the registered NAMES, not a
286+ // number.
287+ //
288+ // ── Why the CALLEE NAME is the battery ──
289+ //
290+ // This file has no `selfTest()` entry function and no named section banners:
291+ // the `--self-test` dispatch at the bottom invokes ELEVEN named callees, each
292+ // printing its own line and returning a boolean. So the roster's unit is the
293+ // CALLEE, and its label is the one the SOURCE ALREADY CARRIES — the function's
294+ // own name. Nothing is invented and nothing is judged per comment, and a set
295+ // difference names WHICH sub-check stopped rather than saying only that
296+ // something did. `registerCase('<calleeName>')` is the FIRST statement of every
297+ // callee, above any early return, so what the ledger records is that the callee
298+ // RAN — which is why each floor is 1 rather than the number of assertions the
299+ // callee happens to contain.
300+ //
301+ // ⛔ The rows of the literal `[name, ok]` table inside `reconcileOwnership()`
302+ // are NOT batteries, and the boundary is worth stating because recipe A
303+ // (PR #15271, `check-sdui-manifest`) makes a table row a battery. It does so
304+ // for a file whose SELF-TEST *is* the table: one literal table, one driving
305+ // loop over it, and a sink that writes only when a row fails. Here the table is
306+ // a local of ONE callee among eleven, its rows are evaluated eagerly into
307+ // booleans before anything loops, and the callee already reduces them to a
308+ // single printed verdict of its own. Flooring those rows would floor one
309+ // callee's internals while the other ten stayed at callee granularity — a
310+ // roster whose unit changes per entry. The rule: the battery is the unit the
311+ // DISPATCH names.
312+ //
313+ // ⛔ A pinned TOTAL is not the repair: one callee dropping all its work keeps a
314+ // total "right" the moment a sibling grows.
315+ //
316+ // The counts are a FLOOR, not an equality — a callee that grows a second
317+ // registration must not red. 1 is the honest floor for a callee: the dispatch
318+ // reaches it exactly once per run.
319+ const SELF_TEST_BATTERIES = Object . freeze ( {
320+ reconcileAttributes : 1 ,
321+ reconcileAttributeSemantics : 1 ,
322+ reconcileScripts : 1 ,
323+ reconcileGenerators : 1 ,
324+ reconcileUntrackedDispositions : 1 ,
325+ reconcileOwnership : 1 ,
326+ hookIsExecutable : 1 ,
327+ registeredDriverResolves : 1 ,
328+ reconcileMixedComparators : 1 ,
329+ endToEnd : 1 ,
330+ endToEndMixed : 1 ,
331+ } ) ;
332+
333+ // DELETING an entry silences that battery's floor exactly as effectively as
334+ // zeroing it, so the roster's own size is pinned too. This pin is also half of
335+ // the duplicate refusal: two dispatch entries naming ONE callee collapse to one
336+ // key in the literal above, so the roster falls below this number; the
337+ // roster ↔ dispatch cross-check in the floor block is the other half, and it
338+ // names WHICH callee was listed twice.
339+ const SELF_TEST_BATTERY_FLOOR = 11 ;
340+
341+ // The key a registration is filed under when a callee registers no name at all.
342+ // It is not a declared battery, so it reds by the same set difference rather
343+ // than silently inflating whichever battery registered last.
344+ const UNATTRIBUTED_BATTERY = '(no callee named)' ;
345+
346+ // The battery ledger, read by `batteryFloorFailures()` from the dispatch block
347+ // at the very bottom of this file. It is MODULE-level rather than local to a
348+ // self-test body because this file HAS no self-test body: the registrations
349+ // happen inside eleven separate callees and the floor is read at the dispatch's
350+ // verdict site, so the ledger has to outlive every one of those frames.
351+ //
352+ // ⚠️ Named for the roster's role, deliberately NOT with a self-test spelling:
353+ // `check:pm-dispatch-gates` anchors on a top-level declaration whose NAME
354+ // spells self-test, and every such name owes a row in its
355+ // COMPOUND_ANCHOR_LEDGER. `battery` is also the accurate word.
356+ const batterySeen = new Map ( ) ;
357+
358+ /**
359+ * Record that a self-test callee RAN.
360+ *
361+ * Called as the FIRST statement of each of the eleven callees the `--self-test`
362+ * dispatch invokes — above any early return, so a callee that bails out early
363+ * still reports that it ran, and the floor is never met by a frame that
364+ * returned before doing anything.
365+ */
366+ function registerCase ( name ) {
367+ const key = name ?? UNATTRIBUTED_BATTERY ;
368+ batterySeen . set ( key , ( batterySeen . get ( key ) ?? 0 ) + 1 ) ;
369+ }
370+
371+ /**
372+ * The floor: every declared callee RAN (#13489).
373+ *
374+ * Evaluated at the dispatch's verdict site — after all eleven callees have had
375+ * their chance and immediately before the success line — and reached only from
376+ * the `--self-test` branch, so a production merge-driver run never reads the
377+ * ledger at all.
378+ *
379+ * @param {string[] } invoked the callee names the dispatch block actually invokes
380+ * @returns {string[] } floor breaches; empty means the floor held
381+ */
382+ function batteryFloorFailures ( invoked ) {
383+ const declared = Object . keys ( SELF_TEST_BATTERIES ) ;
384+ const problems = [ ] ;
385+ if ( declared . length < SELF_TEST_BATTERY_FLOOR ) {
386+ problems . push (
387+ `SELF_TEST_BATTERIES declares ${ declared . length } batteries, below the pinned `
388+ + `${ SELF_TEST_BATTERY_FLOOR } — a battery deleted from the roster takes its own floor with it.` ,
389+ ) ;
390+ }
391+
392+ // ── Roster ↔ dispatch, both directions ──
393+ // `invoked` is read off the dispatch's own list of callees, so this pair says
394+ // WHICH name lost its counterpart. A declared name nothing invokes would also
395+ // read DID NOT RUN below; naming it here reports the cause (nothing calls it)
396+ // rather than only the symptom (nothing registered).
397+ const duplicated = [ ...new Set ( invoked . filter ( ( name , i ) => invoked . indexOf ( name ) !== i ) ) ] ;
398+ if ( duplicated . length ) {
399+ problems . push (
400+ `the dispatch invokes ${ duplicated . map ( ( n ) => JSON . stringify ( n ) ) . join ( ', ' ) } more than once — `
401+ + 'two entries naming one callee are ONE battery, so the second can stop running while the '
402+ + 'first keeps the floor met.' ,
403+ ) ;
404+ }
405+ for ( const name of invoked ) {
406+ if ( declared . includes ( name ) ) continue ;
407+ problems . push (
408+ `the dispatch invokes "${ name } ", which is not declared in SELF_TEST_BATTERIES — a callee `
409+ + 'nothing declares is a sub-check nothing floors.' ,
410+ ) ;
411+ }
412+ for ( const name of declared ) {
413+ if ( invoked . includes ( name ) ) continue ;
414+ problems . push (
415+ `SELF_TEST_BATTERIES declares "${ name } ", which the dispatch block does not invoke — a floor `
416+ + 'over a battery nothing can reach.' ,
417+ ) ;
418+ }
419+
420+ // ── Roster ↔ ledger, both directions ──
421+ for ( const [ name , count ] of batterySeen ) {
422+ if ( declared . includes ( name ) ) continue ;
423+ problems . push (
424+ `self-test battery "${ name } " registered ${ count } case(s) but is not declared in `
425+ + 'SELF_TEST_BATTERIES — a case attributed to no declared battery is one nothing floors.' ,
426+ ) ;
427+ }
428+ for ( const name of declared ) {
429+ const count = batterySeen . get ( name ) ?? 0 ;
430+ if ( count >= SELF_TEST_BATTERIES [ name ] ) continue ;
431+ problems . push (
432+ count === 0
433+ ? `self-test battery "${ name } " DID NOT RUN — 0 cases registered, ${ SELF_TEST_BATTERIES [ name ] } pinned. `
434+ + 'The verdict below would have claimed that sub-check holds.'
435+ : `self-test battery "${ name } " registered ${ count } case(s), below its pinned floor of `
436+ + `${ SELF_TEST_BATTERIES [ name ] } — cases that used to run no longer do.` ,
437+ ) ;
438+ }
439+
440+ if ( problems . length ) {
441+ problems . push (
442+ 'A battery at or below its floor means a sub-check STOPPED RUNNING — the battery is the bug, '
443+ + 'not the number. Find what stopped registering (a deleted invocation, a renamed callee, a '
444+ + '`registerCase()` moved below an early return) and restore it.' ,
445+ ) ;
446+ }
447+ return problems ;
448+ }
449+
281450function fail ( msg ) {
282451 console . error ( `✗ ${ msg } ` ) ;
283452 process . exitCode = 1 ;
@@ -286,6 +455,7 @@ function fail(msg) {
286455
287456/** `.gitattributes` and the table must name the same paths — in both directions. */
288457function reconcileAttributes ( ) {
458+ registerCase ( 'reconcileAttributes' ) ;
289459 const file = join ( REPO_ROOT , '.gitattributes' ) ;
290460 if ( ! existsSync ( file ) ) return fail ( '.gitattributes is missing — the driver is mapped to nothing.' ) ;
291461 const mapped = readFileSync ( file , 'utf8' )
@@ -338,6 +508,7 @@ function manifestFor(dir) {
338508 * command the driver prints for it is the command the `pre-commit` gate spawns.
339509 */
340510function reconcileScripts ( ) {
511+ registerCase ( 'reconcileScripts' ) ;
341512 const workspace = workspacePackages ( REPO_ROOT ) ;
342513 const byOwner = new Map ( ) ;
343514 for ( const e of REGEN_ARTIFACTS ) {
@@ -444,6 +615,7 @@ function reconcileScripts() {
444615 * been born unable to see its own motivating case.
445616 */
446617function reconcileGenerators ( ) {
618+ registerCase ( 'reconcileGenerators' ) ;
447619 const workspace = workspacePackages ( REPO_ROOT ) ;
448620 const manifests = [
449621 { dir : '.' , manifest : JSON . parse ( readFileSync ( join ( REPO_ROOT , 'package.json' ) , 'utf8' ) ) } ,
@@ -516,6 +688,7 @@ function reconcileGenerators() {
516688 * later, as the merge conflict this whole file exists to pre-empt.
517689 */
518690function reconcileUntrackedDispositions ( ) {
691+ registerCase ( 'reconcileUntrackedDispositions' ) ;
519692 const claims = NOT_DRIVER_MANAGED . filter ( ( e ) => e . untracked ) ;
520693 const wrong = [ ] ;
521694 for ( const e of claims ) {
@@ -551,6 +724,7 @@ function reconcileUntrackedDispositions() {
551724 * as "covered" until someone looks.
552725 */
553726function reconcileAttributeSemantics ( ) {
727+ registerCase ( 'reconcileAttributeSemantics' ) ;
554728 const tracked = execFileSync ( 'git' , [ 'ls-files' , '-z' ] , {
555729 cwd : REPO_ROOT ,
556730 encoding : 'utf8' ,
@@ -602,6 +776,7 @@ function reconcileAttributeSemantics() {
602776}
603777
604778function reconcileOwnership ( ) {
779+ registerCase ( 'reconcileOwnership' ) ;
605780 const workspace = workspacePackages ( REPO_ROOT ) ;
606781 const rootScripts = JSON . parse ( readFileSync ( join ( REPO_ROOT , 'package.json' ) , 'utf8' ) ) ;
607782 const specDir = ownerDir ( DEFAULT_OWNER , workspace ) ;
@@ -645,6 +820,7 @@ function reconcileOwnership() {
645820 * commits went through with the hook installed and inert.
646821 */
647822function hookIsExecutable ( ) {
823+ registerCase ( 'hookIsExecutable' ) ;
648824 try {
649825 const mode = execFileSync ( 'git' , [ 'ls-files' , '-s' , '.githooks/pre-commit' ] , {
650826 cwd : REPO_ROOT ,
@@ -681,6 +857,7 @@ function hookIsExecutable() {
681857 * - the value has drifted from what `setup-git-hooks.mjs` registers.
682858 */
683859function registeredDriverResolves ( ) {
860+ registerCase ( 'registeredDriverResolves' ) ;
684861 const { key, value : expected } = GIT_SETTINGS . find ( ( s ) => s . key === `merge.${ DRIVER_NAME } .driver` ) ;
685862
686863 let actual = '' ;
@@ -780,6 +957,7 @@ function realpath(p) {
780957 * (`%P` order, git-dir resolution in a worktree) is exactly what silently rots.
781958 */
782959function endToEnd ( ) {
960+ registerCase ( 'endToEnd' ) ;
783961 const dir = mkdtempSync ( join ( tmpdir ( ) , 'os-regen-selftest-' ) ) ;
784962 const git = ( ...args ) => execFileSync ( 'git' , args , { cwd : dir , encoding : 'utf8' , stdio : [ 'ignore' , 'pipe' , 'pipe' ] } ) ;
785963 try {
@@ -840,6 +1018,7 @@ function endToEnd() {
8401018 * call different, which is the firing control the "safe" verdict rests on.
8411019 */
8421020function reconcileMixedComparators ( ) {
1021+ registerCase ( 'reconcileMixedComparators' ) ;
8431022 const rows = REGEN_ARTIFACTS . filter ( ( e ) => e . mixed ) ;
8441023 const unknown = rows . filter ( ( e ) => ! MIXED_COMPARATORS [ e . mixed ] ) ;
8451024 if ( unknown . length ) {
@@ -889,6 +1068,7 @@ function reconcileMixedComparators() {
8891068 * success unless something reads the resulting bytes.
8901069 */
8911070function endToEndMixed ( ) {
1071+ registerCase ( 'endToEndMixed' ) ;
8921072 const entry = REGEN_ARTIFACTS . find ( ( e ) => e . mixed ) ;
8931073 if ( ! entry ) {
8941074 console . log ( '✓ end-to-end (mixed): no mixed rows declared — nothing to prove' ) ;
@@ -960,23 +1140,42 @@ function endToEndMixed() {
9601140
9611141if ( process . argv . includes ( '--self-test' ) ) {
9621142 console . log ( 'git-merge-regen --self-test\n' ) ;
963- const results = [
964- reconcileAttributes ( ) ,
965- reconcileAttributeSemantics ( ) ,
966- reconcileScripts ( ) ,
967- reconcileGenerators ( ) ,
968- reconcileUntrackedDispositions ( ) ,
969- reconcileOwnership ( ) ,
970- hookIsExecutable ( ) ,
971- registeredDriverResolves ( ) ,
972- reconcileMixedComparators ( ) ,
973- endToEnd ( ) ,
974- endToEndMixed ( ) ,
1143+ // The eleven callees as a literal LIST rather than eleven bare calls, so the
1144+ // names this block invokes are data the floor below can cross-check the
1145+ // roster against, in both directions. The names are read off the function
1146+ // declarations themselves (`fn.name`), so a renamed callee moves this list
1147+ // with it and cannot drift from the roster in silence.
1148+ const callees = [
1149+ reconcileAttributes ,
1150+ reconcileAttributeSemantics ,
1151+ reconcileScripts ,
1152+ reconcileGenerators ,
1153+ reconcileUntrackedDispositions ,
1154+ reconcileOwnership ,
1155+ hookIsExecutable ,
1156+ registeredDriverResolves ,
1157+ reconcileMixedComparators ,
1158+ endToEnd ,
1159+ endToEndMixed ,
9751160 ] ;
1161+ const results = callees . map ( ( run ) => run ( ) ) ;
1162+
1163+ // ── The assertion floor, at the verdict site ─────────────────────
1164+ // There is no verdict site inside a self-test body here, because there is no
1165+ // self-test body: this dispatch IS the verdict site, and the AND below is the
1166+ // verdict. So the floor is evaluated here, after every callee has had its
1167+ // chance and immediately before the success line — the only place a run in
1168+ // which a callee never ran can still be stopped from reporting that the
1169+ // wiring is consistent. It sits inside the `--self-test` branch, so the
1170+ // production merge-driver path (the `else` arm) never reads the ledger.
1171+ const floorBreaches = batteryFloorFailures ( callees . map ( ( run ) => run . name ) ) ;
1172+ for ( const breach of floorBreaches ) fail ( `self-test floor: ${ breach } ` ) ;
1173+
1174+ const failures = results . filter ( ( ok ) => ! ok ) . length + floorBreaches . length ;
9761175 console . log (
977- results . every ( Boolean )
1176+ failures === 0
9781177 ? `\n✓ merge driver wiring is consistent (${ NOT_DRIVER_MANAGED . length } path(s) deliberately excluded).`
979- : ' \n✗ merge driver wiring is inconsistent — see above.' ,
1178+ : ` \n✗ merge driver wiring is inconsistent — ${ failures } failure(s) (cases and floor); see above.` ,
9801179 ) ;
9811180} else {
9821181 try {
0 commit comments