From b3cc2cd57e019f38aa8dd80d499cddf3266d883d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 14:40:39 +0000 Subject: [PATCH] test(scripts): floor workspace-enumerator's folded self-test, adopted by its seven callers `selfTest()` carried 22 static `t(name, ok)` sites and a failure-only sink, so "every case held" and "the cases never ran" returned the same empty list -- and the seven gates that fold it in decided on that list alone. None of them could floor the module's cases, because a green run contributes zero registrations to any of their ledgers. Pin the registered NAMES in the module. The body's four named section banners are four batteries, opened by `battery()` on the banner's own line; the existing `t()` sink routes through `registerCase()` with no case rewritten and no assertion changed. Floors are MEASURED on a run -- 18 / 3 / 1 / 3 -- and the roster's own size is pinned at 4. The check is EXPORTED rather than placed: this module is deliberately not a gate, so it has no `--self-test` dispatch and no verdict site of its own. Each of the seven folding callers adopts `workspaceEnumeratorFloorFailures()` exactly where it already adopts the module's failures -- five spread it beside the existing spread, two loop it through `expect(false, failure)`. Adopting AFTER the call is what makes an early return above the cases read as a breach; evaluated before `selfTest()`'s own `return` it would be skipped by the same early return it exists to catch. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --- scripts/check-dev-prereqs.mjs | 2 + scripts/check-published-files.mjs | 2 + scripts/check-published-readme-exports.mjs | 2 + scripts/check-test-source-alias.mjs | 2 + scripts/check-type-check-coverage.mjs | 2 + scripts/check-type-source-resolution.mjs | 2 + scripts/pnpm-filter-targets.mjs | 2 + scripts/workspace-enumerator.mjs | 142 +++++++++++++++++++++ 8 files changed, 156 insertions(+) diff --git a/scripts/check-dev-prereqs.mjs b/scripts/check-dev-prereqs.mjs index a6cd2a43c3..4b19abcc05 100644 --- a/scripts/check-dev-prereqs.mjs +++ b/scripts/check-dev-prereqs.mjs @@ -240,6 +240,7 @@ import { fileURLToPath } from 'url'; import { WorkspaceEnumerationError, selfTest as workspaceEnumeratorSelfTest, + workspaceEnumeratorFloorFailures, workspaceMemberDirs, } from './workspace-enumerator.mjs'; @@ -898,6 +899,7 @@ function selfTest() { // its own (#11510 — being a gate is exactly what it must not be); every gate // that consolidated onto it folds in its checks. failures.push(...workspaceEnumeratorSelfTest({ root: ROOT })); + failures.push(...workspaceEnumeratorFloorFailures()); // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── // diff --git a/scripts/check-published-files.mjs b/scripts/check-published-files.mjs index 43fa396a64..932e67bdb2 100644 --- a/scripts/check-published-files.mjs +++ b/scripts/check-published-files.mjs @@ -106,6 +106,7 @@ import { join, posix, resolve } from 'node:path'; import { readWorkspaceGlobs, selfTest as workspaceEnumeratorSelfTest, + workspaceEnumeratorFloorFailures, workspacePackageDirs, } from './workspace-enumerator.mjs'; @@ -654,6 +655,7 @@ function selfTest() { // self-test ran and returned nothing. expect(enumeratorFailures.length === 0, `the shared workspace enumerator reported ${enumeratorFailures.length} failure(s)`); failures.push(...enumeratorFailures); + failures.push(...workspaceEnumeratorFloorFailures()); // -- The floor: every declared battery RAN, and ran its cases (#13489) ----- // diff --git a/scripts/check-published-readme-exports.mjs b/scripts/check-published-readme-exports.mjs index 4026112f2b..703195b479 100644 --- a/scripts/check-published-readme-exports.mjs +++ b/scripts/check-published-readme-exports.mjs @@ -296,6 +296,7 @@ import { createProgramChecked } from './ts-parse.mjs'; import { WORKSPACE_FILE, selfTest as workspaceEnumeratorSelfTest, + workspaceEnumeratorFloorFailures, workspacePackageDirs, } from './workspace-enumerator.mjs'; @@ -3545,6 +3546,7 @@ function selfTest() { // The shared workspace enumerator is a plain module with no CI invocation of // its own (#11510); every gate that consolidated onto it folds in its checks. failures.push(...workspaceEnumeratorSelfTest({ root: ROOT })); + failures.push(...workspaceEnumeratorFloorFailures()); // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── // diff --git a/scripts/check-test-source-alias.mjs b/scripts/check-test-source-alias.mjs index 2bc52d27d7..ca5ff149bd 100644 --- a/scripts/check-test-source-alias.mjs +++ b/scripts/check-test-source-alias.mjs @@ -318,6 +318,7 @@ import { isExclusionGlob, readWorkspaceGlobs, selfTest as workspaceEnumeratorSelfTest, + workspaceEnumeratorFloorFailures, } from './workspace-enumerator.mjs'; import { tmpdir } from 'node:os'; import process from 'node:process'; @@ -2959,6 +2960,7 @@ function selfTest() { // (#11510 — being a gate is exactly what it must not be); every script that // consolidated onto it folds in its checks. for (const failure of workspaceEnumeratorSelfTest({ root: REPO_ROOT })) expect(false, failure); + for (const failure of workspaceEnumeratorFloorFailures()) expect(false, failure); } finally { rmSync(root, { recursive: true, force: true }); } diff --git a/scripts/check-type-check-coverage.mjs b/scripts/check-type-check-coverage.mjs index 8c67093de7..45bdda702d 100644 --- a/scripts/check-type-check-coverage.mjs +++ b/scripts/check-type-check-coverage.mjs @@ -484,6 +484,7 @@ import { join, posix, resolve } from 'node:path'; import { getHeapStatistics } from 'node:v8'; import { selfTest as workspaceEnumeratorSelfTest, + workspaceEnumeratorFloorFailures, workspacePackageDirs, } from './workspace-enumerator.mjs'; // `typecheck`-script -> tsconfig program set. Shared with @@ -5929,6 +5930,7 @@ function selfTest() { // above: its cases are defined and run in `workspace-enumerator.mjs`, and a // floor written here would pin a count that module is free to change. failures.push(...workspaceEnumeratorSelfTest({ root: ROOT })); + failures.push(...workspaceEnumeratorFloorFailures()); if (failures.length) { console.error(`✗ check:type-check-coverage --self-test — ${failures.length} failure(s)\n`); diff --git a/scripts/check-type-source-resolution.mjs b/scripts/check-type-source-resolution.mjs index 6cad51d984..4613787e68 100644 --- a/scripts/check-type-source-resolution.mjs +++ b/scripts/check-type-source-resolution.mjs @@ -152,6 +152,7 @@ import { isExclusionGlob, readWorkspaceGlobs, selfTest as workspaceEnumeratorSelfTest, + workspaceEnumeratorFloorFailures, } from './workspace-enumerator.mjs'; // The `typecheck`-script -> tsconfig program set, shared with // `check-type-check-coverage.mjs` (#11490). Imported rather than re-derived: @@ -2364,6 +2365,7 @@ function selfTest() { // (#11510 — being a gate is exactly what it must not be); every script that // consolidated onto it folds in its checks. for (const failure of workspaceEnumeratorSelfTest({ root: REPO_ROOT })) expect(false, failure); + for (const failure of workspaceEnumeratorFloorFailures()) expect(false, failure); } finally { rmSync(root, { recursive: true, force: true }); } diff --git a/scripts/pnpm-filter-targets.mjs b/scripts/pnpm-filter-targets.mjs index 42aad7ccef..a0c982b46c 100644 --- a/scripts/pnpm-filter-targets.mjs +++ b/scripts/pnpm-filter-targets.mjs @@ -92,6 +92,7 @@ import { WorkspaceEnumerationError, parseWorkspaceGlobs, selfTest as workspaceEnumeratorSelfTest, + workspaceEnumeratorFloorFailures, workspacePackageDirs, } from './workspace-enumerator.mjs'; @@ -675,6 +676,7 @@ export async function selfTest() { // The shared workspace enumerator is a plain module with no CI invocation of // its own (#11510); every script that consolidated onto it folds in its checks. failures.push(...workspaceEnumeratorSelfTest({ root: root ?? HERE })); + failures.push(...workspaceEnumeratorFloorFailures()); if (failures.length === 0) { console.log( diff --git a/scripts/workspace-enumerator.mjs b/scripts/workspace-enumerator.mjs index e89a924234..3da68d894a 100644 --- a/scripts/workspace-enumerator.mjs +++ b/scripts/workspace-enumerator.mjs @@ -314,6 +314,143 @@ export function workspacePackages(root) { return out; } +// ── 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 +// here that line is printed by SEVEN other files, none of which could tell the +// difference either. Closed the way PR #13487 validated on check-doc-authoring: +// what is pinned is the registered NAMES, not a number. +// +// This body carries FOUR named section banners, so each banner is one battery, +// opened by a `battery()` call on the banner's own line — the sectioned shape +// PR #15327 landed, rather than the single hoisted battery PR #15217 landed for +// bodies carrying fewer than two. Sectioning is what lets the parse cases stop +// running while the expansion cases keep going and still have something name +// the half that went quiet. +// +// 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. +// +// ── Why the LEDGER is module-level and the CHECK sits in the CALLERS ─────── +// +// This module is deliberately NOT a gate (see the header), so unlike every +// other file in this recipe it has no `--self-test` dispatch and no verdict +// site of its own: `selfTest()` REGISTERS and returns its failures, and the +// seven folding callers DECIDE. So the check is exported instead of placed — +// `workspaceEnumeratorFloorFailures()` below — and each caller adopts it +// exactly where it already adopts the failures, immediately after the call. +// 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. +// +// ⛔ The floor is NOT evaluated at the end of `selfTest()` before its `return`: +// an early return anywhere above that line would skip the check and the cases +// TOGETHER, which is the one defect this card exists to catch. Adopted AFTER +// the call, in a caller, the same early return lands as a count below the floor +// and reds — in all seven callers at once, which is the accepted cost of a +// module that seven gates fold in and none of them owns. +// +// ⛔ A pinned TOTAL is not the repair: this body's 25 registrations split 18 / +// 3 / 1 / 3, so the parse battery could drop from 18 cases to 3 and keep a +// total "right" the moment a sibling grows. +const SELF_TEST_BATTERIES = Object.freeze({ + 'the parse, one case per divergence the consolidation settled': 18, + 'the expansion': 3, + 'the property this module exists to keep: NO path population': 1, + 'the live half, when a caller supplies the repo root': 3, +}); + +// 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 = 4; + +// 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)'; + +// The battery ledger, read by `workspaceEnumeratorFloorFailures()` below from +// the OTHER function. `battery()` opens a battery; every assertion 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 assertion registered after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by `selfTest()`'s own assertion sink, once per assertion. */ +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 assertion + * sink `t()` routes through `registerCase()`. Every folding caller calls this + * immediately after it adopts `selfTest()`'s failures and BEFORE its own + * verdict line, so that line can only be printed by a run in which the set of + * batteries that registered assertions 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. + * + * ⚠️ EXPORTED, and prefixed with this module's name, because the callers are + * the verdict sites: seven gates import it, and each of them already declares + * a `batteryFloorFailures` of its OWN for its OWN roster. The unprefixed name + * this recipe uses everywhere else would collide in all seven import lists — + * the same reason they all import `selfTest as workspaceEnumeratorSelfTest`. + * + * @returns {string[]} floor breaches; empty means the floor held + */ +export function workspaceEnumeratorFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `workspace-enumerator: 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( + `workspace-enumerator: self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion 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 + ? `workspace-enumerator: self-test battery "${name}" DID NOT RUN — 0 cases registered, ` + + `${SELF_TEST_BATTERIES[name]} pinned. The verdict below would have claimed those cases hold.` + : `workspace-enumerator: 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( + 'workspace-enumerator: 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 shared assertions, returned rather than printed so each importing gate * can fold them into its own `--self-test` report. @@ -328,6 +465,7 @@ export function workspacePackages(root) { export function selfTest({ root = null } = {}) { const failures = []; const t = (name, ok) => { + registerCase(); if (!ok) failures.push(`workspace-enumerator: ${name}`); }; // Every fixture path is ASSEMBLED, never spelled. A path-shaped literal @@ -352,6 +490,7 @@ export function selfTest({ root = null } = {}) { const flat = JSON.stringify([PKGS]); // ── the parse, one case per divergence the consolidation settled ────────── + battery('the parse, one case per divergence the consolidation settled'); t('a plain list parses', answer(`packages:\n - ${PKGS}\n - ${APPS}\n`) === both); t('quotes are stripped', answer(`packages:\n - '${PKGS}'\n - "${APPS}"\n`) === both); t('CRLF parses the same', answer(`packages:\r\n - ${PKGS}\r\n - ${APPS}\r\n`) === both); @@ -384,6 +523,7 @@ export function selfTest({ root = null } = {}) { t('the flow-sequence form is REFUSED rather than read as empty', answer(`packages: [${PKGS}, ${APPS}]\n`) === 'REFUSED'); // ── the expansion ───────────────────────────────────────────────────────── + battery('the expansion'); const NOWHERE = P('', 'nonexistent'); const expandRefused = (glob) => { try { @@ -401,6 +541,7 @@ export function selfTest({ root = null } = {}) { ); // ── the property this module exists to keep: NO path population ─────────── + battery('the property this module exists to keep: NO path population'); // // Pinned mechanically, not by review, and read off THIS FILE's own bytes so // a stale copy cannot satisfy it. A path-shaped literal added here — in the @@ -448,6 +589,7 @@ export function selfTest({ root = null } = {}) { } // ── the live half, when a caller supplies the repo root ─────────────────── + battery('the live half, when a caller supplies the repo root'); if (root !== null) { let live = null; try {