diff --git a/scripts/check-override-consistency.mjs b/scripts/check-override-consistency.mjs index 142dc2a6aa..c2a6716458 100644 --- a/scripts/check-override-consistency.mjs +++ b/scripts/check-override-consistency.mjs @@ -429,6 +429,63 @@ const FIXTURE_LOCKFILE = [ '', ].join('\n'); +// -- The self-test's own battery roster and floor (#13489) ------------------ +// +// `--self-test` reaching its verdict used to be this self-test's ONLY success +// condition, so "every case held" and "the cases never ran" printed the same +// line. Closed the PR #13487 way: what is pinned is the registered NAMES, not a +// number. +// +// This self-test is TABLE-DRIVEN -- one literal `cases` table, one loop over it, +// and a sink that writes only when a case FAILS. Routing THAT sink through +// `registerCase()` would register a case only when it fails: a fully green run +// would register 0 and every battery would read DID NOT RUN, the floor inverted +// rather than installed. So the roster is the table's own rows. Each row LABEL +// is a declared battery, verbatim, with a floor of 1, and `registerCase(name)` +// is the first statement of the driving loop body -- so the case is attributed +// to the row actually being run, whatever that row asserts afterwards. There is +// no `battery()` opener: for a table-driven self-test the ROW is the battery. +// +// The three `// --- ... ---` comments inside the table are grouping rules, not +// section heads, and a comment is NOT promoted to one -- the rows are the +// batteries either way. +// +// A pinned TOTAL is not the repair, and neither is a roster DERIVED from the +// table: `cases.length` moves with the table, so a deleted row would delete its +// own floor. The roster below is a LITERAL the table is checked against, which +// is what lets a deleted or renamed row name ITSELF in the refusal. +// +// The counts are a FLOOR, not an equality -- a row that grows into several +// registrations must not red. 1 is the honest floor for a table row: the loop +// reaches it exactly once per run. +const SELF_TEST_BATTERIES = Object.freeze({ + 'lockfile parses into a consumer index': 1, + 'unparseable lockfile -> census skipped, never a crash': 1, + 'lockfile that is not a mapping -> census skipped': 1, + 'transitive consumer present (snapshot pulls undici) -> NOT reported': 1, + 'workspace importer counts as a consumer (semver) -> NOT reported': 1, + 'zero consumers (nothing pulls form-data) -> REPORTED': 1, + 'census reports only the idle one out of a mixed set': 1, + 'bound equal to the target floor (the #5032 undici shape) -> REPORTED': 1, + 'bound below the target floor (uncovered gap) -> REPORTED': 1, + 'bound above the target version line (the durable shape) -> NOT reported': 1, + 'selector with no upper bound -> NOT reported': 1, + 'inclusive upper bound covers the target -> NOT reported': 1, + 'declared range that reaches the target -> no violation': 1, + 'declared range that cannot reach the target -> violation': 1, + 'no implicit prereleases: ^1.7.0 does not reach 1.7.0-rc.2': 1, + 'declaration outside the selector scope -> override does not apply': 1, + 'declaration inside the selector scope -> override applies': 1, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. This pin is also half of +// the duplicate-label refusal: two rows sharing a label collapse to ONE key in +// the literal above, so the roster falls below this number; the table +// cross-check in the floor block is the other half, and names WHICH label +// collided. +const SELF_TEST_BATTERY_FLOOR = 17; + // Returned by `selfTest()` only after its verdict is printed. The dispatch // refuses anything else: a `return` that leaves the function above that line // prints nothing and still exits 0 — a self-test that never finished, reported @@ -542,9 +599,16 @@ function selfTest() { }, ]; - let passed = true; + // The ledger this self-test's floor is evaluated against (#13489). + const batterySeen = new Map(); + const registerCase = (name) => { + batterySeen.set(name, (batterySeen.get(name) ?? 0) + 1); + }; + + let failures = 0; console.log('check-override-consistency self-test (both directions):'); for (const testCase of cases) { + registerCase(testCase.name); let actual; try { actual = testCase.actual(); @@ -552,15 +616,71 @@ function selfTest() { actual = `threw: ${error.message}`; } const ok = actual === testCase.expect; - if (!ok) passed = false; + if (!ok) failures++; console.log( `${ok ? ' ✓' : ' ✗'} ${testCase.name}` + (ok ? '' : `\n expected ${JSON.stringify(testCase.expect)}, got ${JSON.stringify(actual)}`), ); } - if (!passed) { - console.error('\n✗ self-test failed — this check does not do what it claims.'); + // -- The floor: every declared row RAN, and ran its case (#13489) -------- + // + // Evaluated after every row has had its chance and BEFORE the verdict, so the + // success line below can only be printed by a run in which the set of rows + // that registered EQUALS the set declared. A set difference names WHICH row + // stopped; a count says only that something did. + const floorFailure = (message) => { + console.error(`✗ self-test floor: ${message}`); + failures++; + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + const rowLabels = cases.map((c) => c.name); + const duplicated = [...new Set(rowLabels.filter((name, i) => rowLabels.indexOf(name) !== i))]; + if (duplicated.length > 0) { + floorBreached = true; + floorFailure( + `the cases table uses ${duplicated.map((n) => JSON.stringify(n)).join(', ')} as a row label more than once — ` + + 'two rows sharing a label are ONE battery, so the second can stop running while the first keeps the floor met.', + ); + } + for (const [name, count] of batterySeen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — a case attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = batterySeen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed that case holds.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (a deleted row, a renamed label, a loop that no longer ' + + 'reaches it) and restore it.', + ); + } + + if (failures) { + console.error(`\n✗ check-override-consistency self-test: ${failures} failure(s) (cases and floor).`); process.exit(1); } console.log( diff --git a/scripts/measure-position-name-fold-census.mjs b/scripts/measure-position-name-fold-census.mjs index da07fd2d85..e6fcf147ac 100644 --- a/scripts/measure-position-name-fold-census.mjs +++ b/scripts/measure-position-name-fold-census.mjs @@ -693,75 +693,167 @@ const AUDIT_CONTROLS = [ // handshake is a flag rather than a returned sentinel. let selfTestReachedVerdict = false; +// -- The self-test's own battery roster and floor (#13489) ------------------ +// +// Reaching the verdict used to be this self-test's ONLY success condition, so +// "every control held" and "the controls never ran" printed the same line. +// Closed the way PR #13487 validated on check-doc-authoring: what is pinned is +// the registered NAMES, not a number. +// +// This file's assertion sites are `if (...) problems.push(...)` -- a +// FAILURE-ONLY sink. Routing `problems.push` itself through `registerCase()` +// would register a case only when a control FAILS: a fully green run would +// register 0 and the battery would read DID NOT RUN, the floor inverted rather +// than installed. So the sites are counted by the `check(() => { ... })` THUNK +// PR #15198 measured: the existing `if (...) problems.push(...)` is carried in +// VERBATIM, no condition is touched, and registration happens whether or not +// the site fires. +// +// This file declares ONE battery, opened at the top of the self-test body: it +// carries ZERO named section banners, fewer than the two the sectioning +// criterion needs, and a comment is NOT promoted to a section head. The hoisted +// single battery is the shape PR #14896, PR #15003 and PR #15217 landed. +// +// ⭐ ONE site stays OUTSIDE the roster, deliberately: the `catch` arm of the +// AUDIT_CONTROLS loop (`audit control threw: ...`) ends in `continue`, which is +// illegal inside the thunk's arrow function. Wrapping it would mean rewriting +// that control flow -- exactly what the verbatim rule forbids. Its sibling in +// the same loop body, `if (!ac.expect(result))`, IS floored, so a loop that +// stops running still reds. This is the residue recipe A's non-table +// assertions carry too (PR #15286's `extra` call). +// +// The count is a FLOOR, not an equality -- adding controls is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering, never to lower the number. Four +// of the thirteen floored sites sit inside `for` loops, so the count is the +// number of times a site is REACHED on a run, measured on a run. +const SELF_TEST_BATTERIES = Object.freeze({ + 'measure-position-name-fold-census self-test': 24, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 1; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest({ quiet = false } = {}) { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every site below is attributed to the one most + // recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const batterySeen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + batterySeen.set(b, (batterySeen.get(b) ?? 0) + 1); + }; + battery('measure-position-name-fold-census self-test'); + // The thunk: it registers the case and then runs the existing site VERBATIM, + // so no control's condition is inverted or rewritten and the `problems` sink + // keeps its own semantics. + const check = (fn) => { + registerCase(); + fn(); + }; const problems = []; const c = census(); for (const name of CONTROLS.builtinPositions) { - if (!c.positions.has(name)) { - problems.push(`parse-integrity: built-in POSITION \`${name}\` was not found. The scanner has drifted` - + ' off the declaration shape; every number below it would be an under-count.'); - } + check(() => { + if (!c.positions.has(name)) { + problems.push(`parse-integrity: built-in POSITION \`${name}\` was not found. The scanner has drifted` + + ' off the declaration shape; every number below it would be an under-count.'); + } + }); } for (const name of CONTROLS.builtinPermissionSets) { - if (!c.permissionSets.has(name)) { - problems.push(`parse-integrity: built-in PERMISSION SET \`${name}\` was not found. Same failure, other axis.`); - } + check(() => { + if (!c.permissionSets.has(name)) { + problems.push(`parse-integrity: built-in PERMISSION SET \`${name}\` was not found. Same failure, other axis.`); + } + }); } const a = CONTROLS.junctionAnchor; - if (!c.bindings.some((b) => b.position === a.position && b.set === a.set)) { - problems.push(`positive control (junction) NOT classified as a junction binding: ` - + `${a.position} -> ${a.set}\n ${a.why}`); - } - if (c.nameFolds.some((f) => f.name === a.position)) { - problems.push(`positive control (junction) was reported as a NAME-DEPENDENCY: ${a.position}\n ${a.why}`); - } + check(() => { + if (!c.bindings.some((b) => b.position === a.position && b.set === a.set)) { + problems.push(`positive control (junction) NOT classified as a junction binding: ` + + `${a.position} -> ${a.set}\n ${a.why}`); + } + }); + check(() => { + if (c.nameFolds.some((f) => f.name === a.position)) { + problems.push(`positive control (junction) was reported as a NAME-DEPENDENCY: ${a.position}\n ${a.why}`); + } + }); const b = CONTROLS.nameFoldAnchor; const fold = c.nameFolds.find((f) => f.name === b.name); - if (!fold) { - problems.push(`positive control (name-fold) NOT detected: ${b.name}\n ${b.why}`); - } else if (!fold.permissionSets.some((s) => s.kind === 'artifact')) { - problems.push(`positive control (name-fold) ${b.name} was detected, but its permission-set half is not` - + ' the composed artifact -- the control is passing for the wrong reason.'); - } + check(() => { + if (!fold) { + problems.push(`positive control (name-fold) NOT detected: ${b.name}\n ${b.why}`); + } else if (!fold.permissionSets.some((s) => s.kind === 'artifact')) { + problems.push(`positive control (name-fold) ${b.name} was detected, but its permission-set half is not` + + ' the composed artifact -- the control is passing for the wrong reason.'); + } + }); const n = CONTROLS.negative; - if (c.nameFolds.some((f) => f.name === n.position)) { - problems.push(`negative control reported as a name-fold: ${n.position}\n ${n.why}`); - } - if (!c.bindings.some((x) => x.position === n.position && x.set === n.set)) { - problems.push(`negative control ${n.position} -> ${n.set} was not seen as a junction binding at all;` - + ' the binder scan is blind, so every junction-bound pair would be misfiled as unbound.'); - } + check(() => { + if (c.nameFolds.some((f) => f.name === n.position)) { + problems.push(`negative control reported as a name-fold: ${n.position}\n ${n.why}`); + } + }); + check(() => { + if (!c.bindings.some((x) => x.position === n.position && x.set === n.set)) { + problems.push(`negative control ${n.position} -> ${n.set} was not seen as a junction binding at all;` + + ' the binder scan is blind, so every junction-bound pair would be misfiled as unbound.'); + } + }); const sb = CONTROLS.secondBinder; - if (!c.bindings.some((x) => x.position === sb.position && x.set === sb.set)) { - problems.push(`second-binder control missed: \`${sb.position} -> ${sb.set}\` is a junction binding declared by` - + ' a binder whose list is NOT named POSITION_PERMISSION_SET_BINDINGS. The scan is recognising one app\'s' - + ' spelling again, so other apps\' junction rows are invisible and their positions read as inert.'); - } - if (c.inert.some((i) => i.name === sb.position)) { - problems.push(`second-binder control: \`${sb.position}\` was reported INERT while a binder binds it.`); - } - if (!c.nameFolds.some((f) => f.name === 'sales_manager')) { - problems.push('a position junction-bound to a DIFFERENT set must still count as a name-fold on its own name' - + ' (sales_manager is bound to crm_sales_user and still folds onto the HotCRM `sales_manager` set).' - + ' Losing this would tell 要点 2 the pair is already governed.'); - } + check(() => { + if (!c.bindings.some((x) => x.position === sb.position && x.set === sb.set)) { + problems.push(`second-binder control missed: \`${sb.position} -> ${sb.set}\` is a junction binding declared by` + + ' a binder whose list is NOT named POSITION_PERMISSION_SET_BINDINGS. The scan is recognising one app\'s' + + ' spelling again, so other apps\' junction rows are invisible and their positions read as inert.'); + } + }); + check(() => { + if (c.inert.some((i) => i.name === sb.position)) { + problems.push(`second-binder control: \`${sb.position}\` was reported INERT while a binder binds it.`); + } + }); + check(() => { + if (!c.nameFolds.some((f) => f.name === 'sales_manager')) { + problems.push('a position junction-bound to a DIFFERENT set must still count as a name-fold on its own name' + + ' (sales_manager is bound to crm_sales_user and still folds onto the HotCRM `sales_manager` set).' + + ' Losing this would tell 要点 2 the pair is already governed.'); + } + }); const bleed = CONTROLS.isDefaultBleed; - if (!c.bindings.some((x) => x.position === 'everyone' && x.set === bleed.bound)) { - problems.push(`regression control: \`${bleed.bound}\` declares \`isDefault: true\` and must appear as an` - + ' `everyone` auto-bind. It does not, so the isDefault scan has stopped seeing declarations it owns.'); - } - for (const set of bleed.unbound) { - if (c.bindings.some((x) => x.position === 'everyone' && x.set === set)) { - problems.push(`regression control fired again: \`everyone -> ${set}\` was reported as a junction binding,` - + ` but ${set} declares no \`isDefault\`. The isDefault look-ahead is reading past the declaration's own` - + ' object literal again -- see CONTROLS.isDefaultBleed.'); + check(() => { + if (!c.bindings.some((x) => x.position === 'everyone' && x.set === bleed.bound)) { + problems.push(`regression control: \`${bleed.bound}\` declares \`isDefault: true\` and must appear as an` + + ' `everyone` auto-bind. It does not, so the isDefault scan has stopped seeing declarations it owns.'); } + }); + for (const set of bleed.unbound) { + check(() => { + if (c.bindings.some((x) => x.position === 'everyone' && x.set === set)) { + problems.push(`regression control fired again: \`everyone -> ${set}\` was reported as a junction binding,` + + ` but ${set} declares no \`isDefault\`. The isDefault look-ahead is reading past the declaration's own` + + ' object literal again -- see CONTROLS.isDefaultBleed.'); + } + }); } for (const ac of AUDIT_CONTROLS) { @@ -772,9 +864,11 @@ function selfTest({ quiet = false } = {}) { problems.push(`audit control threw: ${ac.label} -- ${e?.message}`); continue; } - if (!ac.expect(result)) { - problems.push(`audit control FAILED: ${ac.label}\n got: ${JSON.stringify(result)}`); - } + check(() => { + if (!ac.expect(result)) { + problems.push(`audit control FAILED: ${ac.label}\n got: ${JSON.stringify(result)}`); + } + }); } if (problems.length > 0) { @@ -783,6 +877,53 @@ function selfTest({ quiet = false } = {}) { return 1; } if (!quiet) { + // -- The floor: the declared battery RAN, and ran its cases (#13489) ---- + // + // Evaluated after every site has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered cases EQUALS the set declared. + // + // ⭐ It lives INSIDE the `!quiet` branch on purpose. `main()` gates the + // PRODUCTION path on `selfTest({ quiet: true })`, which prints no verdict + // and therefore makes no claim for a floor to guard; evaluating the floor + // there would let a roster edit change what a census run outputs. The floor + // belongs to the `--self-test` verdict, which is the line that would + // otherwise claim controls hold that never ran. + // + // It reports in this file's own idiom -- a stderr block and a non-zero + // return -- and returns BEFORE `selfTestReachedVerdict` is set, so a breach + // reds through the #13798 handshake as well as through the exit code. + const floorFailures = []; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorFailures.push( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} -- a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of batterySeen) { + if (declaredBatteries.includes(name)) continue; + floorFailures.push( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES -- a case attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = batterySeen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorFailures.push( + count === 0 + ? `self-test battery "${name}" DID NOT RUN -- 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those controls hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} -- cases that used to run no longer do.`, + ); + } + if (floorFailures.length > 0) { + process.stderr.write(`x measure-position-name-fold-census self-test floor (${floorFailures.length} breach(es))\n\n${ + floorFailures.map((f) => ` - ${f}`).join('\n\n')}\n\n A battery at or below its floor means cases STOPPED RUNNING -- the battery is the bug, not the number. Find what stopped registering (an early return, a deleted block, a guard that now skips) and restore it.\n\n`); + return 1; + } process.stdout.write( `✓ measure-position-name-fold-census self-test: junction anchor (${a.position} -> ${a.set}) classified as` + ` a junction binding and absent from the name-dependency list; name-fold anchor (${b.name}) detected` diff --git a/scripts/measure-test-shard-timings.mjs b/scripts/measure-test-shard-timings.mjs index d271b125ce..df2fd6424b 100644 --- a/scripts/measure-test-shard-timings.mjs +++ b/scripts/measure-test-shard-timings.mjs @@ -174,6 +174,46 @@ export function buildDataset({ perSummary, fileCounts, provenance }) { }; } +// -- The self-test's own battery roster and floor (#13489) ------------------ +// +// `--self-test` reaching its verdict used to be this self-test's ONLY success +// condition, so "every case held" and "the cases never ran" printed the same +// line. Closed the way PR #13487 validated on check-doc-authoring: what is +// pinned is the registered NAMES, not a number. +// +// This file's assertions are bare `throw`s rather than calls to an assertion +// helper, so they are counted by the `check(() => { ... })` THUNK PR #15198 +// measured: the existing `if (...) throw ...` is carried into the thunk +// VERBATIM and the condition is never touched. Routing these through a boolean +// helper instead would mean inverting 22 failure conditions by hand, and a +// dropped `!` yields an assertion that still registers its case and still +// passes -- invisible to the very floor being installed here. The throw still +// propagates: this file fails fast on the FIRST broken assertion, as it always +// has. +// +// This file declares ONE battery, opened at the top of the self-test body. Its +// blocks are headed by unmarked prose comments -- it carries ZERO named section +// banners, fewer than the two the sectioning criterion needs, and a comment is +// NOT promoted to a section head (that is a judgement per comment this +// transplant does not make). The hoisted single battery is the shape PR #14896, +// PR #15003 and PR #15217 landed for exactly this case. +// +// The count is a FLOOR, not an equality -- adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering, never to lower the number. +const SELF_TEST_BATTERIES = Object.freeze({ + 'measure-test-shard-timings self-test': 22, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 1; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + // Returned by `selfTest()` only after its verdict is printed. The dispatch // refuses anything else: a `return` that leaves the function above that line // prints nothing and still exits 0 — a self-test that never finished, reported @@ -181,6 +221,28 @@ export function buildDataset({ perSummary, fileCounts, provenance }) { const SELF_TEST_VERDICT = 'measure-test-shard-timings self-test reached its verdict'; function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const batterySeen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + batterySeen.set(b, (batterySeen.get(b) ?? 0) + 1); + }; + battery('measure-test-shard-timings self-test'); + // The thunk: it registers the case and then runs the existing site VERBATIM, + // so no assertion condition is inverted or rewritten and the sink keeps its + // own semantics. Registration happens whether or not the site fires, which is + // what makes the count a floor on cases RUN rather than a count of failures. + const check = (fn) => { + registerCase(); + fn(); + }; const summary = (tasks) => ({ tasks }); const testTask = (pkg, start, end, status = 'MISS', exitCode = 0) => ({ taskId: `${pkg}#test`, @@ -190,9 +252,15 @@ function selfTest() { execution: { startTime: start, endTime: end, exitCode }, }); - if (median([3]) !== 3) throw new Error('median: single value'); - if (median([5, 1, 3]) !== 3) throw new Error('median: odd length is not order-dependent'); - if (median([1, 2, 3, 4]) !== 2.5) throw new Error('median: even length averages the middle pair'); + check(() => { + if (median([3]) !== 3) throw new Error('median: single value'); + }); + check(() => { + if (median([5, 1, 3]) !== 3) throw new Error('median: odd length is not order-dependent'); + }); + check(() => { + if (median([1, 2, 3, 4]) !== 2.5) throw new Error('median: even length averages the middle pair'); + }); // A build task in the same summary must not be read as a test duration. const mixed = samplesFromSummary( @@ -202,23 +270,37 @@ function selfTest() { ]), 'f' ); - if (mixed.samples.get('a') !== 2) throw new Error(`task filter: got ${mixed.samples.get('a')}`); - if (mixed.samples.size !== 1) throw new Error('task filter: a non-test task was sampled'); + check(() => { + if (mixed.samples.get('a') !== 2) throw new Error(`task filter: got ${mixed.samples.get('a')}`); + }); + check(() => { + if (mixed.samples.size !== 1) throw new Error('task filter: a non-test task was sampled'); + }); // THE ONE THAT MATTERS: a cache HIT is skipped, never recorded as ~0s. // The control leg first -- a genuine 40ms MISS is a legitimate measurement, // so only the HIT/MISS pair below proves the skip is about the cache status // and not about the window being short. const shortMiss = samplesFromSummary(summary([testTask('a', 0, 40)]), 'f'); - if (shortMiss.samples.get('a') !== 0.04) throw new Error('cache: a short MISS was not recorded'); + check(() => { + if (shortMiss.samples.get('a') !== 0.04) throw new Error('cache: a short MISS was not recorded'); + }); const withHit = samplesFromSummary(summary([testTask('a', 0, 40, 'HIT'), testTask('b', 0, 60_000)]), 'f'); - if (withHit.samples.has('a')) throw new Error('cache: a HIT was recorded as a measurement'); - if (!withHit.skippedCached.includes('a')) throw new Error('cache: a HIT was not reported as skipped'); - if (withHit.samples.get('b') !== 60) throw new Error('cache: the MISS beside it was lost'); + check(() => { + if (withHit.samples.has('a')) throw new Error('cache: a HIT was recorded as a measurement'); + }); + check(() => { + if (!withHit.skippedCached.includes('a')) throw new Error('cache: a HIT was not reported as skipped'); + }); + check(() => { + if (withHit.samples.get('b') !== 60) throw new Error('cache: the MISS beside it was lost'); + }); // A failed suite stopped early; its window is not the package's cost. const failed = samplesFromSummary(summary([testTask('a', 0, 500, 'MISS', 1)]), 'f'); - if (failed.samples.has('a')) throw new Error('exit: a failed suite was recorded as a duration'); + check(() => { + if (failed.samples.has('a')) throw new Error('exit: a failed suite was recorded as a duration'); + }); const threw = (fn) => { try { @@ -228,13 +310,19 @@ function selfTest() { return true; } }; - if (!threw(() => samplesFromSummary({}, 'f'))) throw new Error('shape: a non-summary was accepted'); - if (!threw(() => samplesFromSummary(summary([{ task: 'test', package: '', cache: { status: 'MISS' } }]), 'f'))) { - throw new Error('shape: a nameless test task was accepted'); - } - if (!threw(() => samplesFromSummary(summary([{ task: 'test', package: 'a', cache: { status: 'MISS' } }]), 'f'))) { - throw new Error('shape: a test task with no execution window was accepted'); - } + check(() => { + if (!threw(() => samplesFromSummary({}, 'f'))) throw new Error('shape: a non-summary was accepted'); + }); + check(() => { + if (!threw(() => samplesFromSummary(summary([{ task: 'test', package: '', cache: { status: 'MISS' } }]), 'f'))) { + throw new Error('shape: a nameless test task was accepted'); + } + }); + check(() => { + if (!threw(() => samplesFromSummary(summary([{ task: 'test', package: 'a', cache: { status: 'MISS' } }]), 'f'))) { + throw new Error('shape: a test task with no execution window was accepted'); + } + }); // Merging: the same package sampled by several shards collapses to its median. const merged = buildDataset({ @@ -246,29 +334,39 @@ function selfTest() { fileCounts: new Map([['a', 10], ['big', 50]]), provenance: { measuredAt: 'test' }, }); - if (merged.packages.a !== 20) throw new Error(`merge: expected the median 20, got ${merged.packages.a}`); + check(() => { + if (merged.packages.a !== 20) throw new Error(`merge: expected the median 20, got ${merged.packages.a}`); + }); // rates: a -> 20/10 = 2, big -> 100/50 = 2 => 2 - if (merged.secondsPerTestFileFallback !== 2) { - throw new Error(`fallback rate: got ${merged.secondsPerTestFileFallback}`); - } + check(() => { + if (merged.secondsPerTestFileFallback !== 2) { + throw new Error(`fallback rate: got ${merged.secondsPerTestFileFallback}`); + } + }); // Packages too small to vote on the rate are excluded from it but still kept. const tiny = buildDataset({ perSummary: [samplesFromSummary(summary([testTask('a', 0, 10_000), testTask('t', 0, 300)]), 'f')], fileCounts: new Map([['a', 5], ['t', 1]]), provenance: {}, }); - if (tiny.secondsPerTestFileFallback !== 2) throw new Error(`rate: a 0.3s/1-file package voted (${tiny.secondsPerTestFileFallback})`); - if (tiny.packages.t !== 0.3) throw new Error('rate: the small package was dropped from the dataset'); + check(() => { + if (tiny.secondsPerTestFileFallback !== 2) throw new Error(`rate: a 0.3s/1-file package voted (${tiny.secondsPerTestFileFallback})`); + }); + check(() => { + if (tiny.packages.t !== 0.3) throw new Error('rate: the small package was dropped from the dataset'); + }); - if (!threw(() => - buildDataset({ - perSummary: [samplesFromSummary(summary([testTask('a', 0, 40, 'HIT')]), 'f')], - fileCounts: new Map(), - provenance: {}, - }) - )) { - throw new Error('an all-cached run produced a dataset instead of refusing'); - } + check(() => { + if (!threw(() => + buildDataset({ + perSummary: [samplesFromSummary(summary([testTask('a', 0, 40, 'HIT')]), 'f')], + fileCounts: new Map(), + provenance: {}, + }) + )) { + throw new Error('an all-cached run produced a dataset instead of refusing'); + } + }); // Workspace resolution, at the depth that actually caught a defect. A // one-level scan resolves `packages/*` and returns null for the ~60% of the @@ -276,13 +374,68 @@ function selfTest() { // seven more roots -- silently, as "this package has no test files", which // then skews the fallback rate toward whichever half sits at depth 1. const nested = packageDirForName('@objectstack/driver-turso'); - if (nested === null) throw new Error('workspace: a package nested under packages/drivers/ did not resolve'); - if (path.relative(REPO_ROOT, nested).split(path.sep).length < 3) { - throw new Error(`workspace: expected a nested path, got ${nested}`); - } - if (countTestFiles(nested) === 0) throw new Error('workspace: the resolved nested package reports no test files'); + check(() => { + if (nested === null) throw new Error('workspace: a package nested under packages/drivers/ did not resolve'); + }); + check(() => { + if (path.relative(REPO_ROOT, nested).split(path.sep).length < 3) { + throw new Error(`workspace: expected a nested path, got ${nested}`); + } + }); + check(() => { + if (countTestFiles(nested) === 0) throw new Error('workspace: the resolved nested package reports no test files'); + }); const flat = packageDirForName('@objectstack/spec'); - if (flat === null || path.basename(flat) !== 'spec') throw new Error('workspace: a depth-1 package stopped resolving'); + check(() => { + if (flat === null || path.basename(flat) !== 'spec') throw new Error('workspace: a depth-1 package stopped resolving'); + }); + + // -- The floor: every declared battery RAN, and ran its cases (#13489) ---- + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered cases EQUALS the set declared. A set difference + // names WHICH battery stopped; a count says only that something did. + // + // It THROWS rather than collecting into a `failures` array because that is + // how every other assertion in this file reports: the dispatch below turns an + // unfinished self-test into a non-zero exit, and a floor breach is exactly + // that -- a self-test that did not run what it claims to run. + const floorFailures = []; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorFailures.push( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} -- a battery deleted from the roster takes its own floor with it.` + ); + } + for (const [name, count] of batterySeen) { + if (declaredBatteries.includes(name)) continue; + floorFailures.push( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES -- a case attributed to no declared battery is one nothing floors.' + ); + } + for (const name of declaredBatteries) { + const count = batterySeen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorFailures.push( + count === 0 + ? `self-test battery "${name}" DID NOT RUN -- 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} -- cases that used to run no longer do.` + ); + } + if (floorFailures.length > 0) { + throw new Error( + `measure-test-shard-timings self-test floor (${floorFailures.length} breach(es)):\n` + + floorFailures.map((f) => ` - ${f}`).join('\n') + + '\n A battery at or below its floor means cases STOPPED RUNNING -- the battery is the bug, ' + + 'not the number. Find what stopped registering (an early return, a deleted block, a guard ' + + 'that now skips) and restore it.' + ); + } console.log('measure-test-shard-timings: self-test OK'); diff --git a/scripts/pm/check-skill-line-ratchet.mjs b/scripts/pm/check-skill-line-ratchet.mjs index 5fd99aef30..1cdaba9efd 100644 --- a/scripts/pm/check-skill-line-ratchet.mjs +++ b/scripts/pm/check-skill-line-ratchet.mjs @@ -1488,6 +1488,209 @@ function run() { if (failed) process.exit(1); } +// -- The self-test's own battery roster and floor (#13489) ------------------ +// +// `--self-test` reaching its verdict used to be this self-test's ONLY success +// condition, so "every case held" and "the cases never ran" printed the same +// line. Closed the PR #13487 way: what is pinned is the registered NAMES, not a +// number. +// +// This self-test is TABLE-DRIVEN -- one `cases` table, one loop over it, and a +// sink (`failed++`) that writes only when a case FAILS. Routing THAT sink +// through `registerCase()` would register a case only when it fails: a fully +// green run would register 0 and every battery would read DID NOT RUN, the +// floor inverted rather than installed. So the roster is the table's own rows. +// Each row LABEL is a declared battery, verbatim, with a floor of 1, and +// `registerCase(name)` is the first statement of the driving loop body -- so the +// case is attributed to the row actually being run. There is no `battery()` +// opener: for a table-driven self-test the ROW is the battery. +// +// ⭐ ALL 155 rows are floored, the four `...(() => { ... })()` spreads included. +// Those spreads were flagged in the batch-8 census as an IIFE-produced block +// whose rows could not take a literal roster key. Measured here, that premise +// does not hold for this file: each IIFE is a SCOPING device that declares +// local fixture consts and then `return [...]`s an array of LITERAL +// `[label, actual, expected]` rows. No row label is a template string, none is +// computed, and no row is produced by a `map`/`push`/loop. Three independent +// readings agree on 155 -- the source labels extracted by indentation, the +// literal row starts, and the `cases.length` the green line prints on a run -- +// so nothing here is the `extra`-call residue of PR #15286, and leaving any row +// outside the roster would have been the lossy reading. +// +// A pinned TOTAL is not the repair, and neither is a roster DERIVED from the +// table: `cases.length` moves with the table, so a deleted row would delete its +// own floor. The roster below is a LITERAL the table is checked against, which +// is what lets a deleted or renamed row name ITSELF in the refusal. +// +// The counts are a FLOOR, not an equality -- a row that grows into several +// registrations must not red. 1 is the honest floor for a table row: the loop +// reaches it exactly once per run. +const SELF_TEST_BATTERIES = Object.freeze({ + 'under the ceiling -> green': 1, + 'at the ceiling -> green': 1, + 'over the ceiling -> red': 1, + 'red message names the file': 1, + 'red message names the remedy': 1, + 'red message names the authoring rule': 1, + 'empty read -> red, not a skip': 1, + 'every covered file has a positive ceiling': 1, + 'SKILL.md is covered': 1, + 'the dev-agent definition is covered': 1, + 'all five compressed references are covered': 1, + 'all eight lane/seat job descriptions are covered': 1, + 'the other four skills are covered (#9473)': 1, + 'root AGENTS.md is covered (#9792)': 1, + 'root CLAUDE.md is covered (#9965)': 1, + 'references/compile-surfaces.md is covered (#12098)': 1, + 'every separator-less ceiling declares a root-file watch hint': 1, + 'and the declaration names no file the map does not cover': 1, + 'both root instruction files are declared': 1, + 'the declared form is NOT a CEILINGS key': 1, + 'the published skills/ catalog is deliberately uncovered': 1, + 'budget is 120 bytes': 1, + 'a short ASCII line -> green': 1, + 'a long ASCII line -> RED': 1, + 'a short CJK line -> green': 1, + 'a long CJK line -> RED': 1, + 'the RED message names the budget': 1, + 'the RED message names the line number': 1, + 'the RED message offers NO allowlist to add a line to': 1, + 'no offenders -> green verdict': 1, + 'a long line inside a fence is exempt': 1, + '...and the SAME line outside one is RED': 1, + 'scan: a fenced long line yields no offender': 1, + 'scan: the fence closes again': 1, + 'a long table row is exempt': 1, + '...and the same cells as prose are RED': 1, + 'a long heading is exempt': 1, + '...and the same text as a paragraph is RED': 1, + 'front matter is exempt': 1, + 'scan: front matter closes at the second ---': 1, + 'an anchored Blocked-by: line is exempt': 1, + 'Restart-when: too': 1, + 'Restart-touch: too': 1, + 'but a MID-PROSE mention is not exempt — the escape hatch is line-anchored only': 1, + 'a long blockquote line is exempt': 1, + '...including one indented inside a list': 1, + '...and the same quotation unquoted is RED': 1, + 'a bare over-long URL is exempt': 1, + 'a single over-long code span is exempt': 1, + '...but prose LEADING to that URL is RED (wrap first, URL lands alone)': 1, + 'wrapLine splits a long CJK line': 1, + 'every wrapped CJK segment is within budget': 1, + 'wrapping a CJK line changes NOTHING but whitespace': 1, + 'wrapping an ASCII line changes NOTHING but whitespace': 1, + 'a list continuation is indented under the marker': 1, + 'no continuation line opens a new markdown block': 1, + 'wrapLine is idempotent — its output is the canonical form': 1, + 'wrapLine leaves a short line untouched': 1, + 'wrapLine never breaks inside a code span': 1, + 'wrapLine never breaks inside a 「…」 ruling quote': 1, + '...nor inside a 『…』 one': 1, + 'a quote longer than the budget makes its line unbreakable, not RED': 1, + 'a line OPENING an unterminated 「 is exempt': 1, + 'a line INSIDE an open quote is exempt': 1, + '...and the same line outside one is RED — the exemption is the quote, not the text': 1, + 'a SHORT line inside a quote is simply green, not counted exempt': 1, + 'advanceState opens on an unmatched 「': 1, + 'advanceState closes on the matching 」': 1, + 'a quote opened and closed on ONE line does not open the state': 1, + 'advanceState does not track quotes inside a fence': 1, + 'scan: a two-line quote yields quotation exemptions, not offenders': 1, + 'wrapLine never strands a closing 。 at a line head': 1, + 'an ASCII , after a Han character is a no-break-after mark': 1, + '...and ; and : are the same mark class': 1, + '...but after a LATIN word it stays an ordinary break point': 1, + '...and a mark with a space before it follows nothing': 1, + '...and an atom not ending in one is never the mark': 1, + 'breakLegal refuses the break after a Han+ASCII mark': 1, + '...a mark with no following space was never a break point to begin with': 1, + '...still allows the CJK-to-CJK break one atom earlier': 1, + '...and still allows an ordinary ASCII space break': 1, + 'wrapLine splits the trap line': 1, + 'no wrapped line ends on a Han+ASCII mark — the break is no longer OFFERED': 1, + '...the line it took instead is still within budget': 1, + '...and it moved only whitespace, as every wrap must': 1, + 'the SAME shape with a Latin word before the mark still breaks there': 1, + 'wrapLine is still idempotent under the new rule': 1, + 'a long table row is EXEMPT from the 120-byte line rule': 1, + '...and the same row is METERED by its file pin': 1, + 'at exactly the pin -> green': 1, + 'one byte wider -> RED (widening a cell is the measured defect)': 1, + 'narrower than the pin -> green': 1, + 'a pin of 0 is a measurement — a file with no table row passes it': 1, + '...and the FIRST table row in such a file is RED': 1, + 'a missing pin is RED, not a skip (#4690)': 1, + '...and says so rather than naming a width': 1, + 'the RED message names the width': 1, + 'the RED message names the line': 1, + 'the RED message names the remedy': 1, + 'the RED message names consolidation as the way to pay it down': 1, + 'the RED message offers NO allowlist': 1, + 'the RED message says raising needs a maintainer ruling': 1, + 'scanTableRows finds the widest row': 1, + '...and reports its line number': 1, + 'a `|` line inside a FENCE is not a table row': 1, + 'a `|` line in FRONT MATTER is not a table row': 1, + 'a file with no table row measures 0': 1, + 'every ceilinged file carries a pin': 1, + 'and the pin map names no file the ceiling map does not cover': 1, + 'every pin is a non-negative integer': 1, + 'the published skills/ catalog is uncovered here too': 1, + 'the citation is the ruling this file was given': 1, + 'cross-file move — a raise covered by its sources\' net decrease PASSES': 1, + '...and the green verdict states the arithmetic it read': 1, + 'cross-file move — a raise whose sources did NOT shrink is RED (the whole defect: a `move` that licenses an ordinary raise would run green forever)': 1, + '...and the RED verdict names the raise and the net decrease it fell short of': 1, + '...and sends the author to the ordinary path rather than to a bigger declaration': 1, + 'cross-file move — a raise EXCEEDING the net decrease is RED, even by one line': 1, + '...while a raise exactly equal to it is legal': 1, + 'cross-file move — a declaration citing no ruling is RED, however sound its arithmetic': 1, + '...and the RED verdict spells the citation it wanted': 1, + 'cross-file move — declarations whose net movement is 0 leave the map total unchanged': 1, + 'cross-file move — a declaration whose participants net POSITIVE fails the total (+2 here)': 1, + '...and the RED total names the lines it grew by': 1, + '...and a net-negative move reports the corpus shrinking': 1, + 'cross-file move — one source may not pay for two destinations': 1, + 'cross-file move — a destination lowered back to or below its pre-move ceiling reads as PAID DOWN, never as red (lowering is always legitimate)': 1, + '...and says so rather than reporting an arithmetic it can no longer measure': 1, + 'cross-file move — a SOURCE grown back past what it paid re-opens the move (the loophole: the destination keeps the lines while the payers grow back)': 1, + 'cross-file move — an unknown destination is RED, not a skip (#4690)': 1, + 'cross-file move — an unknown source is RED, not a skip (#4690)': 1, + 'cross-file move — a file may not pay its own raise': 1, + 'cross-file move — a declaration naming no source at all is RED': 1, + 'every live declaration cites the ruling': 1, + 'every live declaration names its destination\'s pre-move ceiling and at least one source': 1, + 'every live participant is a file this map covers': 1, + 'ruled raise — a destination that later took an ordinary ruled raise PASSES with `was` at its literal pre-move value': 1, + '...and the verdict prices the MOVE, not the ruling: +10 against the sources\' net 11': 1, + '...and names the ruled lines it took out, so the arithmetic can be read back': 1, + '...while the SAME tree with the raise unrecorded is the double red this record ends': 1, + '...whose first half reads the ruling as the move\'s: +44 against 11': 1, + '...and which now sends the author to the record rather than to `was`': 1, + 'ruled raise — the map-wide total subtracts it too, and reads the corpus DOWN 1': 1, + '...where the unrecorded twin reports the corpus growing by 33': 1, + 'ruled raise — a record quoting NO ruling is RED (the licence is the maintainer\'s or it does not exist)': 1, + '...and the RED verdict says what it wanted': 1, + '...while an ENGLISH ruling quoted the way this map already quotes one passes': 1, + 'ruled raise — a record with no line count is RED': 1, + 'ruled raise — a NEGATIVE line count is RED: lowering is always legitimate and is never recorded here': 1, + 'ruled raise — a record with no ruling DATE is RED': 1, + 'ruled raise — records claiming MORE lines than the ceiling stands above `was` are RED': 1, + '...which is the loophole that closes: an inflated record would otherwise read the move as paid down and pass forever': 1, + 'ruled raise — a destination whose whole rise is ruled keeps none of it for the move, and reads as paid down': 1, + 'ruled raise — a declaration carrying no record behaves exactly as before (every case above this group is one)': 1, + 'every live ruled-raise record quotes its ruling, dates it, and names a positive line count': 1, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. This pin is also half of +// the duplicate-label refusal: two rows sharing a label collapse to ONE key in +// the literal above, so the roster falls below this number; the table +// cross-check in the floor block is the other half, and names WHICH label +// collided. +const SELF_TEST_BATTERY_FLOOR = 155; + // Returned by `selfTest()` only after its verdict is printed. The dispatch // refuses anything else: a `return` that leaves the function above that line // prints nothing and still exits 0 — a self-test that never finished, reported @@ -1786,14 +1989,77 @@ function selfTest() { ]; })(), ].map((c) => (Array.isArray(c[1]) || (c[1] && typeof c[1] === 'object') ? [c[0], JSON.stringify(c[1]), JSON.stringify(c[2])] : c)); + // The ledger this self-test's floor is evaluated against (#13489). + const batterySeen = new Map(); + const registerCase = (name) => { + batterySeen.set(name, (batterySeen.get(name) ?? 0) + 1); + }; + let failed = 0; for (const [name, actual, expected] of cases) { + registerCase(name); const ok = actual === expected; if (!ok) failed++; console.log(` ${ok ? '✓' : '✗'} ${name}`); } + // -- The floor: every declared row RAN, and ran its case (#13489) -------- + // + // Evaluated after every row has had its chance and BEFORE the verdict, so the + // success line below can only be printed by a run in which the set of rows + // that registered EQUALS the set declared. A set difference names WHICH row + // stopped; a count says only that something did. + const floorFailure = (message) => { + console.error(`✗ self-test floor: ${message}`); + failed++; + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + const rowLabels = cases.map(([name]) => name); + const duplicated = [...new Set(rowLabels.filter((name, i) => rowLabels.indexOf(name) !== i))]; + if (duplicated.length > 0) { + floorBreached = true; + floorFailure( + `the cases table uses ${duplicated.map((n) => JSON.stringify(n)).join(', ')} as a row label more than once — ` + + 'two rows sharing a label are ONE battery, so the second can stop running while the first keeps the floor met.', + ); + } + for (const [name, count] of batterySeen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — a case attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = batterySeen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed that case holds.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (a deleted row, a renamed label, a loop that no longer ' + + 'reaches it) and restore it.', + ); + } + if (failed) { - console.error(`✗ check-skill-line-ratchet self-test: ${failed} of ${cases.length} case(s) failed.`); + console.error(`✗ check-skill-line-ratchet self-test: ${failed} failure(s) (cases and floor).`); process.exit(1); } console.log(`✓ check-skill-line-ratchet self-test: ${cases.length} cases pass.`);