Skip to content

Commit e9b8cbb

Browse files
claude[bot]claude
andauthored
test(scripts): floor git-merge-regen's self-test on its eleven named callees (#15326)
The `--self-test` dispatch decided success by `results.every(Boolean)` and nothing else, so "every sub-check held" and "the sub-checks never ran" printed the same line. This file has no self-test entry function and no section banners: a top-level `if (--self-test)` block invokes eleven named callees, each printing its own line and returning a boolean, ANDed at the end. So the roster's unit is the CALLEE and its label is the one the source already carries — the function's own name. `registerCase('<calleeName>')` is the first statement of every callee, above any early return, so the ledger records that the callee RAN and each floor is 1. The eleven invocations become a literal list of function references, which makes the names the dispatch invokes into data the floor cross-checks the roster against in both directions, with a duplicate refusal; the floor itself is evaluated at the AND/verdict site, immediately before the success line and only on the `--self-test` path, so the production merge-driver path never reads the ledger. The `[name, ok]` table inside `reconcileOwnership()` deliberately stays as it is: recipe A makes a table row a battery for a file whose self-test IS the table, and here the table is a local of one callee among eleven. The battery is the unit the dispatch names; that boundary is stated at the site. Part of the assertion-floor rollout. Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2ed6be6 commit e9b8cbb

1 file changed

Lines changed: 213 additions & 14 deletions

File tree

scripts/git-merge-regen.mjs

Lines changed: 213 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
281450
function 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. */
288457
function 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
*/
340510
function 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
*/
446617
function 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
*/
518690
function 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
*/
553726
function 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

604778
function 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
*/
647822
function 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
*/
683859
function 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
*/
782959
function 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
*/
8421020
function 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
*/
8911070
function 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

9611141
if (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

Comments
 (0)