Skip to content

Commit f158daf

Browse files
baozhoutaoclaude
andauthored
test(scripts): batch 10d — floor check-regen-pending on its three named callees (#15338)
* test(scripts): floor check-regen-pending's self-test on its three named callees `noDist && noTree && armed && table && fixture` was the only success condition, so "every sub-check held" and "the sub-checks never ran" printed the same line. Pin the registered NAMES, not a number: a roster of the three callees the `--self-test` dispatch invokes, floor 1 each, `registerCase()` as each callee's first statement, and the floor evaluated at the verdict site inside the `--self-test` branch only. The dispatch becomes a literal list of function references mapped through `run()`, so the invoked names are data the floor cross-checks the roster against in both directions. Evaluation order and completeness are unchanged: the original invoked all three eagerly and reduced booleans with `&&`, so the short-circuit was never over calls. The two assertions written inline in the dispatch stay outside the roster with the gap written at the site; the callees' inner sinks are not batteries — the battery is the unit the dispatch names. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk * test(scripts): spell the self-test's failure exit as process.exit(1) The floor above produces a real failure, but the verdict spelled it as `process.exit(failures === 0 ? 0 : 1)` — the ternary this file already carried. `scripts/measure-self-test-floor.mjs` reads a floor as sound only when the code PRODUCES a failure (`process.exit(1)`, `exitCode = 1`, `failures.push`, a throw), deliberately refusing to key on the roster's name alone, so the file classified NONE with a sound roster above it. Make the failure path an explicit branch ending in `process.exit(1)`. The success line's text and the whole green-path output are unchanged byte for byte; only the red path's spelling moves. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 13c48c2 commit f158daf

1 file changed

Lines changed: 238 additions & 9 deletions

File tree

scripts/check-regen-pending.mjs

Lines changed: 238 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,184 @@ function main({ prePush = false } = {}) {
441441
// import — only when this file IS the entry point.
442442
const 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
*/
458636
function 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
*/
608787
function 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. */
632812
function 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

Comments
 (0)