diff --git a/scripts/check-comment-mask-corpus.mjs b/scripts/check-comment-mask-corpus.mjs index a72a54528c..433e0c8419 100644 --- a/scripts/check-comment-mask-corpus.mjs +++ b/scripts/check-comment-mask-corpus.mjs @@ -400,6 +400,138 @@ async function main(argv) { // Self-test -- the comparator, not the corpus // --------------------------------------------------------------------------- +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, +// so "every case held" and "the cases never ran" printed the same line — and +// the `SELF_TEST_CASE_COUNT` the sweep's green line prints is DERIVED from the +// same array, so a deleted case shrinks the printed number with it and the gate +// stays green. Closed the way PR #13487 validated on check-doc-authoring: what +// is pinned is the registered NAMES, not a number. +// +// This file declares ONE battery, opened at the top of `runSelfTestCases()`'s +// 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: that is a judgement per comment this transplant does not make. The +// hoisted single battery is the shape PRs #14896, #15003 and #15217 landed for +// this case. +// +// ── Why the LEDGER is module-level and the CHECK sits at the verdict site ── +// +// This gate splits its self-test in two: `runSelfTestCases()` REGISTERS and +// returns its cases, and `selfTest()` DECIDES — it prints the per-case lines, +// the red line or the green one. There is no verdict site inside the +// registering body, so the floor is evaluated where the green line already is +// (inside `selfTest()`, reached only from the `--self-test` branch of the +// dispatch). The ledger it reads therefore has to outlive +// `runSelfTestCases()`'s frame — hence module scope rather than the local map +// the single-body recipe closes over. Only the CHECK's location moves; +// attribution and scope are untouched. This is the class-3 placement PR #15309 +// settled. +// +// ⚠️ `main()` — the PRODUCTION path — also calls `runSelfTestCases()`, on every +// sweep, so the registrations happen there too. The floor is deliberately NOT +// evaluated on that path: it lives in `selfTest()`, which the sweep never +// calls. Scoping it that way is what keeps a corpus sweep from acquiring a +// refusal that belongs to the `--self-test` verdict. +// +// ⛔ The floor is NOT placed at the end of `runSelfTestCases()` before its +// `return`: an early return anywhere above that line would skip the check +// entirely — the exact defect the #13798 verdict handshake exists to catch — +// coupling hole 1 to hole 2 after the card ruled them orthogonal. It would also +// fire on every production sweep. Evaluated at the verdict site, the same early +// return lands as a count BELOW the floor and reds, in `--self-test` alone. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// 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. +const SELF_TEST_BATTERIES = Object.freeze({ + 'check-comment-mask-corpus self-test': 12, +}); + +// 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 a case 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)'; + +// The battery ledger, read by `batteryFloorFailures()` below from the OTHER +// function. +// +// ⚠️ Named for the roster's role, deliberately NOT with a self-test spelling: +// `check:pm-dispatch-gates` anchors on a top-level declaration whose NAME spells +// self-test and every such name owes a row in its COMPOUND_ANCHOR_LEDGER. This +// machinery holds no fixtures to mask and reads no path literal, so the accurate +// name is the one that says `battery`. +const batterySeen = new Map(); +let openBattery = null; + +/** Open a battery. Every case registered after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by `runSelfTestCases()`'s own case sink, once per case. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batterySeen.set(name, (batterySeen.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Guards the registrations made by **`runSelfTestCases()`** — the body whose + * case sink `ok()` routes through `registerCase()`. It is called from + * `selfTest()` immediately before the success line, so that line can only be + * printed by a run in which the set of batteries that registered cases EQUALS + * the set declared, each at or above its own count. A set difference says WHICH + * battery stopped; a count says only that something did. + * + * @returns {string[]} floor breaches; empty means the floor held + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 (declared.includes(name)) continue; + problems.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 declared) { + const count = batterySeen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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 (problems.length) { + problems.push( + '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.', + ); + } + return problems; +} + /** * What these cases hold: that a disagreement is REPORTED, in both directions, * and that the shebang reconciliation is applied. They run against the real @@ -411,10 +543,16 @@ async function main(argv) { export let SELF_TEST_CASE_COUNT = 0; async function runSelfTestCases(parse) { + // The single hoisted battery this body's cases are attributed to. The floor + // that reads them is evaluated at the verdict site in `selfTest()`. + battery('check-comment-mask-corpus self-test'); const { scanSource } = await import('./js-comment-mask.mjs'); const failures = []; const cases = []; - const ok = (label, condition) => cases.push({ label, condition: Boolean(condition) }); + const ok = (label, condition) => { + registerCase(); + cases.push({ label, condition: Boolean(condition) }); + }; const flagNothing = (source) => ({ comment: new Uint8Array(source.length) }); const flagEverything = (source) => ({ comment: new Uint8Array(source.length).fill(1) }); @@ -492,6 +630,24 @@ export async function selfTest() { console.error(`\n${failures.length}/${cases.length} self-test case(s) failed.`); process.exit(EXIT_DISAGREEMENT); } + + // ── The assertion floor, at the verdict site (#13489) ───────────────────── + // `runSelfTestCases()` registers but does not decide, so the floor over ITS + // registrations is evaluated here, after every case has had its chance and + // immediately before the success line — the only place a run that registered + // nothing can still be stopped from reporting that every case held. It sits + // in `selfTest()`, not in the registering body, so the production sweep in + // `main()` — which calls `runSelfTestCases()` too — never reaches it. + const floorProblems = batteryFloorFailures(); + if (floorProblems.length) { + console.error( + `\n✗ check-comment-mask-corpus self-test: the assertion floor over runSelfTestCases()'s ` + + `registrations was breached (${floorProblems.length} problem(s)); every case that DID run passed.`, + ); + for (const problem of floorProblems) console.error(` - ${problem}`); + process.exit(EXIT_DISAGREEMENT); + } + console.log(`\nAll ${cases.length} self-test cases passed.`); return SELF_TEST_VERDICT; diff --git a/scripts/check-osv-exemptions.mjs b/scripts/check-osv-exemptions.mjs index 95c8fc23bb..2c1cd90461 100644 --- a/scripts/check-osv-exemptions.mjs +++ b/scripts/check-osv-exemptions.mjs @@ -377,7 +377,153 @@ function validateLedger(text, today) { return { problems, count: entries.length }; } -/** @returns {{ passed: boolean, lines: string[] }} */ +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `passed` 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 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's +// `name` 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, so attribution is the loop variable rather than a +// most-recently-opened section. +// +// ⛔ 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. +// +// ── Why the LEDGER is module-level and the CHECK sits at the verdict site ── +// +// This gate splits its self-test in two: `selfTest()` REGISTERS and returns its +// failure count, and `main()` DECIDES — it prints the per-case lines, the red +// line or the green one. There is no verdict site inside the registering body, +// so the floor is evaluated where the green line already is (inside `main()`'s +// `--self-test` branch, so it can never fire on a production run). The ledger it +// reads therefore has to outlive `selfTest()`'s frame — hence module scope +// rather than the local map the single-body recipe closes over. Only the CHECK's +// location moves; attribution and scope are untouched. This is the class-3 +// placement PR #15309 settled. +// +// ⛔ The floor is NOT placed at the end of `selfTest()` before its `return`: an +// early return anywhere above that line would skip the check entirely — the +// exact defect the #13798 verdict handshake exists to catch — coupling hole 1 +// to hole 2 after the card ruled them orthogonal. Evaluated at the verdict site, +// the same early return lands as a count BELOW the floor and reds. +const SELF_TEST_BATTERIES = Object.freeze({ + 'missing/empty ledger → green': 1, + 'comments-only ledger (zero exemptions) → green': 1, + 'well-formed exemption inside the window → green': 1, + 'multi-line reason → green': 1, + 'expired ignoreUntil → red': 1, + 'ignoreUntil == today → red (the scanner already stopped ignoring it)': 1, + 'missing ignoreUntil → red': 1, + 'quoted ignoreUntil → red': 1, + 'ignoreUntil beyond the ceiling → red': 1, + 'reason without an advisory link → red': 1, + 'reason that is only a link → red': 1, + 'untouched template placeholders → red': 1, + 'missing reason → red': 1, + 'unknown key → red': 1, + 'duplicate id → red': 1, + '[[PackageOverrides]] escape hatch → red': 1, + 'top-level key outside a table → red': 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 name collapse to ONE key in +// the literal above, so the roster falls below this number; the table +// cross-check in `batteryFloorFailures()` is the other half, and names WHICH +// label collided. +const SELF_TEST_BATTERY_FLOOR = 17; + +// The ledger `batteryFloorFailures()` reads from the OTHER function, and the +// row labels the table actually presented on this run — both module-level +// because the body that fills them is not the body that reads them. +// +// ⚠️ Named for the roster's role, deliberately NOT with a self-test spelling: +// `check:pm-dispatch-gates` anchors on a top-level declaration whose NAME spells +// self-test and every such name owes a row in its COMPOUND_ANCHOR_LEDGER. This +// machinery holds no fixtures to mask and reads no path literal, so the accurate +// name is the one that says `battery`. +const batterySeen = new Map(); +let batteryRowLabels = []; + +/** Called by `selfTest()`'s driving loop, once per row, before the row runs. */ +function registerCase(name) { + batterySeen.set(name, (batterySeen.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared row RAN, and ran its case (#13489). + * + * Guards the registrations made by **`selfTest()`** — the body whose driving + * loop calls `registerCase()`. It is called from `main()`'s `--self-test` + * branch immediately before the success line, so that line can only be printed + * by a run in which the set of rows that registered EQUALS the set declared, + * each at or above its own count. A set difference says WHICH row stopped; a + * count says only that something did. + * + * @returns {string[]} floor breaches; empty means the floor held + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + const duplicated = [...new Set(batteryRowLabels.filter((name, i) => batteryRowLabels.indexOf(name) !== i))]; + if (duplicated.length > 0) { + problems.push( + `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 (declared.includes(name)) continue; + problems.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 declared) { + const count = batterySeen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.push( + 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 (problems.length) { + problems.push( + '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.', + ); + } + return problems; +} + +/** @returns {{ failures: number, lines: string[] }} */ // Set by `selfTest()` only after its verdict is printed, and read at the // dispatch: a `return` that leaves the function above that line prints nothing // and still exits 0 — a self-test that never finished, reported as one that @@ -479,8 +625,12 @@ function selfTest() { ]; const lines = []; - let passed = true; + // The row labels this run actually presented, for the floor's table + // cross-check at the verdict site (#13489). + batteryRowLabels = cases.map((testCase) => testCase.name); + let failures = 0; for (const testCase of cases) { + registerCase(testCase.name); const { problems } = validateLedger(testCase.text, today); let ok; if (testCase.expect === null) { @@ -488,14 +638,14 @@ function selfTest() { } else { ok = problems.length > 0 && problems.some((p) => testCase.expect.test(p)); } - if (!ok) passed = false; + if (!ok) failures++; lines.push( `${ok ? ' ✓' : ' ✗'} ${testCase.name}` + (ok ? '' : `\n got: ${problems.length === 0 ? '(no problems)' : problems.join('\n ')}`), ); } selfTestReachedVerdict = true; - return { passed, lines }; + return { failures, lines }; } function main() { @@ -512,11 +662,21 @@ function main() { ); process.exit(1); } - const { passed, lines } = selfTestResult; + const { failures, lines } = selfTestResult; console.log('check-osv-exemptions self-test (both directions):'); for (const line of lines) console.log(line); - if (!passed) { - console.error('\n✗ self-test failed — the ledger check does not do what it claims.'); + // ── The assertion floor, at the verdict site (#13489) ───────────────── + // `selfTest()` registers but does not decide, so the floor over ITS + // registrations is evaluated here, after every row has had its chance and + // immediately before the success line — the only place a run that + // registered nothing can still be stopped from reporting that every case + // held. Its breaches share this branch's counted sink, so one red line + // covers cases and floor alike. + const floorProblems = batteryFloorFailures(); + for (const problem of floorProblems) console.error(`✗ self-test floor: ${problem}`); + const total = failures + floorProblems.length; + if (total > 0) { + console.error(`\n✗ check-osv-exemptions self-test: ${total} failure(s) (cases and floor).`); process.exit(1); } console.log('\n✓ self-test passed: valid ledgers accepted, every convention breach rejected.'); diff --git a/scripts/check-workspace-manifest-cycles.mjs b/scripts/check-workspace-manifest-cycles.mjs index 4e9133ef99..8061d583a6 100644 --- a/scripts/check-workspace-manifest-cycles.mjs +++ b/scripts/check-workspace-manifest-cycles.mjs @@ -420,6 +420,146 @@ function main() { ); } +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` 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. The floor requires the OPENED set to equal the +// DECLARED set with each battery at or above its own count. +// +// This file carries SEVEN named section banners, so it takes the Tier B +// multi-battery roster verbatim: one battery per banner, opened immediately +// under it, and every case after that line attributed to it. ⛔ No comment is +// promoted to a section head — the seven names below are the seven banners the +// file already had, verbatim. +// +// ── Why the LEDGER is module-level and the CHECK sits at the verdict site ── +// +// This gate splits its self-test in two: `selfTest()` REGISTERS and returns its +// failures, and `runSelfTest()` DECIDES — it prints the `OK:` line or exits 1. +// There is no verdict site inside the registering body, so the floor is +// evaluated where the green line already is (inside `runSelfTest()`, reached +// only from the `--self-test` branch of the dispatch, so it can never fire on a +// production run). The ledger it reads therefore has to outlive `selfTest()`'s +// frame — hence a module-scope `batterySeen` rather than the local map the +// single-body recipe closes over. Roster, sink and ledger are module-level in +// the landed recipes already; only the CHECK's location moves, and attribution +// and scope are untouched. This is the class-3 placement PR #15309 settled. +// +// ⛔ The floor is NOT placed at the end of `selfTest()` before its `return`: an +// early return anywhere above that line would skip the check entirely — the +// exact defect the #13798 verdict handshake exists to catch — coupling hole 1 +// to hole 2 after the card ruled them orthogonal. Evaluated at the verdict site, +// the same early return lands as a count BELOW the floor and reds, and the set +// difference names the FIRST banner that stopped registering. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. That is precisely what +// seven batteries buy over one number. +// +// The counts are 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. +// +// ⚠️ The `catch` arm of the live-tree section pushes its failure straight into +// `failures` without going through `t()`, so it registers no case. That is the +// honest residue: it is a refusal, not a case, and it is deliberately outside +// the roster — the same class as #15286's `extra` and #15288's instrument +// checks. +const SELF_TEST_BATTERIES = Object.freeze({ + 'Direction 1: the rule must go RED on every cyclic shape': 13, + 'Direction 2: the rule must stay GREEN on every legitimate shape': 10, + 'formatCycle, pinned directly': 2, + "Refusals must refuse (#4690's family)": 1, + 'The derivation half, pinned as bytes': 2, + "The MEMBER manifests, held against the enumerator's live answer": 4, + 'Non-vacuity, on the LIVE tree': 2, +}); + +// 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 = 7; + +// The key a case 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)'; + +// The battery ledger, read by `batteryFloorFailures()` below from the OTHER +// function. `battery()` opens a battery; every case registered after that line +// 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. +// +// ⚠️ Named for the roster's role, deliberately NOT with a self-test spelling: +// `check:pm-dispatch-gates` anchors on a top-level declaration whose NAME spells +// self-test and every such name owes a row in its COMPOUND_ANCHOR_LEDGER. This +// machinery holds no fixtures to mask and reads no path literal, so the accurate +// name is the one that says `battery`. +const batterySeen = new Map(); +let openBattery = null; + +/** Open a battery. Every case registered after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by `selfTest()`'s own case sink, once per case. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batterySeen.set(name, (batterySeen.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Guards the registrations made by **`selfTest()`** — the body whose case sink + * `t()` routes through `registerCase()`. It is called from `runSelfTest()` + * immediately before the success line, so that line can only be printed by a run + * in which the set of batteries that registered cases EQUALS the set declared, + * each at or above its own count. A set difference says WHICH battery stopped; a + * count says only that something did. + * + * @returns {string[]} floor breaches; empty means the floor held + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 (declared.includes(name)) continue; + problems.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 declared) { + const count = batterySeen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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 (problems.length) { + problems.push( + '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.', + ); + } + return problems; +} + /** * The instrument for a gate whose defect class is its MATCHING RULE: a clean * tree cannot tell a working rule from one that stopped matching, because both @@ -434,10 +574,12 @@ function main() { export function selfTest() { const failures = []; const t = (label, ok) => { + registerCase(); if (!ok) failures.push(label); }; // ── Direction 1: the rule must go RED on every cyclic shape ──────────────── + battery('Direction 1: the rule must go RED on every cyclic shape'); // (b) a plain 2-cycle via devDependencies -- both edges named with their class. const devCycle = verdict( @@ -526,6 +668,7 @@ export function selfTest() { t('two disjoint cycles are both reported', twoCycles.cycles.length === 2); // ── Direction 2: the rule must stay GREEN on every legitimate shape ──────── + battery('Direction 2: the rule must stay GREEN on every legitimate shape'); // (a) an acyclic chain. const acyclic = verdict( @@ -595,6 +738,7 @@ export function selfTest() { ); // ── formatCycle, pinned directly ──────────────────────────────────────── + battery('formatCycle, pinned directly'); t( 'formatCycle closes the walk back to the starting node', formatCycle({ nodes: ['A', 'B'], edges: ['devDependencies', 'peerDependencies'] }) === @@ -606,6 +750,7 @@ export function selfTest() { ); // ── Refusals must refuse (#4690's family) ────────────────────────────────── + battery("Refusals must refuse (#4690's family)"); const refuses = (label, fn) => { try { fn(); @@ -632,6 +777,7 @@ export function selfTest() { ); // ── The derivation half, pinned as bytes ────────────────────────────────── + battery('The derivation half, pinned as bytes'); // A reword back to a bare filename or a `SCAN_ROOT`-shaped constant is // invisible in every other signal this gate emits: production stays green, // CI stays green, and the only thing lost is that a manifest-graph card can @@ -647,6 +793,7 @@ export function selfTest() { ); // ── The MEMBER manifests, held against the enumerator's live answer ──────── + battery("The MEMBER manifests, held against the enumerator's live answer"); // Both directions, mirroring `check-turbo-task-graph.mjs`'s own discipline: a // pattern that covers nothing is a fabricated lead, and a member manifest no // pattern covers is the undeclared read this gate would otherwise ship with. @@ -684,6 +831,7 @@ export function selfTest() { ); // ── Non-vacuity, on the LIVE tree ───────────────────────────────────────── + battery('Non-vacuity, on the LIVE tree'); // The cases above are all synthetic; this is the one that fails when the // gate is wired to a workspace it cannot actually reach. try { @@ -710,6 +858,22 @@ function runSelfTest() { for (const f of failures) console.error(` - ${f}`); process.exit(1); } + + // ── The assertion floor, at the verdict site (#13489) ────────────────── + // `selfTest()` registers but does not decide, so the floor over ITS + // registrations is evaluated here, after every battery has had its chance and + // immediately before the success line — the only place a run that registered + // nothing can still be stopped from reporting that every case held. + const floorBreaches = batteryFloorFailures(); + if (floorBreaches.length) { + console.error( + `FAIL: check-workspace-manifest-cycles --self-test — the assertion floor over selfTest()'s ` + + `registrations was breached (${floorBreaches.length} problem(s)); every case that DID run passed.`, + ); + for (const b of floorBreaches) console.error(` - ${b}`); + process.exit(1); + } + console.log('OK: check-workspace-manifest-cycles --self-test — all cases passed.'); return SELF_TEST_VERDICT; diff --git a/scripts/measure-self-test-floor.mjs b/scripts/measure-self-test-floor.mjs index bf55400cc1..6dd664e8d8 100644 --- a/scripts/measure-self-test-floor.mjs +++ b/scripts/measure-self-test-floor.mjs @@ -302,6 +302,10 @@ export const ENTRY_BY_HAND = Object.freeze({ 'scripts/check-self-test-wired.mjs': 'selfTest', 'scripts/check-self-test-workflow-commands.mjs': 'selfTest', 'scripts/check-turbo-task-graph.mjs': 'runSelfTest', + // Two self-test-shaped functions: `selfTest()` returns a failure list and + // `runSelfTest()` is what the dispatch calls. Probing the inner one records a + // TypeError as a handshake; probing `runSelfTest` reads the real one (#14842). + 'scripts/check-workspace-manifest-cycles.mjs': 'runSelfTest', // The dispatch calls FOUR self-test functions and combines their statuses; // there is no single entry an early return leaves, so a one-function probe // measures a sub-battery and reads a downstream crash as a handshake. diff --git a/scripts/typecheck-configs.mjs b/scripts/typecheck-configs.mjs index 2d08e6951b..ff0026a71b 100644 --- a/scripts/typecheck-configs.mjs +++ b/scripts/typecheck-configs.mjs @@ -155,6 +155,156 @@ const CHAIN_CASES = [ { label: 'a cycle terminates instead of recursing', scripts: { typecheck: 'pnpm a', a: 'pnpm typecheck' }, expect: 2 }, ]; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, +// so "every case held" and "the cases never ran" printed the same line — and +// the `SELF_TEST_CASE_COUNT` the verdict prints is DERIVED from the two tables, +// so a deleted row shrinks the printed number with it and the gate stays green. +// Closed the way PR #13487 validated on check-doc-authoring: what is pinned is +// the registered NAMES, not a number. +// +// This self-test is TABLE-DRIVEN — two literal tables, one driving loop each, +// and a failure-only sink. 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 tables' own rows. Each row's `label` is a +// declared battery, verbatim, with a floor of 1, and `registerCase(label)` is +// the FIRST statement of each 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, so +// attribution is the loop variable rather than a most-recently-opened section. +// +// ONE roster spans BOTH tables. The two loops are two halves of one battery of +// cases, and a row that moved between the tables would otherwise leave and +// re-enter a roster without either half noticing. +// +// ⛔ A pinned TOTAL is not the repair, and neither is a roster DERIVED from the +// tables — that is exactly what `SELF_TEST_CASE_COUNT` already is. The roster +// below is a LITERAL the tables are 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. +// +// ── Why the LEDGER is module-level and the CHECK sits at the verdict site ── +// +// `selfTest()` REGISTERS and returns its failures; the dispatch block below +// DECIDES — it prints the red line or the green one. There is no verdict site +// inside the registering body, so the floor is evaluated where the green line +// already is. The ledger it reads therefore has to outlive `selfTest()`'s frame +// — hence module scope rather than the local map the single-body recipe closes +// over. Only the CHECK's location moves; attribution and scope are untouched. +// This is the class-3 placement PR #15309 settled. +// +// ⚠️ This module is a LIBRARY: `check-type-check-coverage.mjs` folds this +// `selfTest()` into its own, and `check-type-source-resolution.mjs` imports the +// predicates. Those importers call `selfTest()`, which registers into the +// ledger below — harmlessly, because the FLOOR is evaluated only in this file's +// own `--self-test` dispatch, which an importer never reaches. Scoping the check +// to the dispatch is what keeps a fold-in from inheriting a refusal it cannot +// act on. +// +// ⛔ The floor is NOT placed at the end of `selfTest()` before its `return`: an +// early return anywhere above that line would skip the check entirely — the +// exact defect the #13798 verdict handshake exists to catch — coupling hole 1 +// to hole 2 after the card ruled them orthogonal. It would also fire inside +// every importer's run. Evaluated at the verdict site, the same early return +// lands as a count BELOW the floor and reds, here and nowhere else. +const SELF_TEST_BATTERIES = Object.freeze({ + 'a bare tsc credits the default config only': 1, + 'an explicitly named sibling config counts': 1, + 'one level of `pnpm