From 41b7845359848f831649213d5ede964454adb44d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 00:50:16 +0000 Subject: [PATCH 1/2] fix(scripts): reconcile the dual-build ledger in the orphan direction, and floor its vacuous pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An exemption row whose id left the population was read by nothing: the only lookup was `ledger[r.id]` from inside the walk over discovered rows. Measured on 8cb96ec41b — a row exempting a package that does not exist left the pass line byte-identical, exit 0, the id unmentioned. Adds the ledger -> population pass, and four vacuity floors (entries, packages, emitted CommonJS files, behaviour probes) that REFUSE (exit 2) rather than report the clean tree when the sweep read nothing. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw --- scripts/check-dual-build-cjs-loads.mjs | 173 +++++++++++++++++++++++-- 1 file changed, 164 insertions(+), 9 deletions(-) diff --git a/scripts/check-dual-build-cjs-loads.mjs b/scripts/check-dual-build-cjs-loads.mjs index 12d8504055..6206e22753 100644 --- a/scripts/check-dual-build-cjs-loads.mjs +++ b/scripts/check-dual-build-cjs-loads.mjs @@ -93,9 +93,38 @@ * whole gate: a runtime load failure is a fact about a dependency, a parse * failure is always a fact about what WE emitted. Pinned in `--self-test`. * - * The ledger reconciles in both directions -- an entry that now loads must be - * deleted in the same PR that fixes it, so a stale exemption is an error - * rather than dead text. + * The ledger reconciles in BOTH directions, and "both" is two checks rather + * than one, because a row goes stale two different ways: + * + * * a ledgered entry that now LOADS is a finding -- the exemption must be + * deleted in the same PR that fixes it, so a stale row is an error rather + * than dead text; + * * a ledgered entry that is no longer in the POPULATION at all -- the + * subpath stopped declaring a `require` condition, the package was renamed + * or unpublished -- is a finding too, and until #13014 it was not. Nothing + * else could see it: the only read of the ledger was `ledger[r.id]` from + * inside the walk over DISCOVERED rows, so a key no row names was never + * looked up, and a lookup that never happens cannot report. Measured on + * `8cb96ec41b` before the fix -- a row exempting a package that does not + * exist left the pass line byte-identical, exit 0, the id unmentioned. + * + * The second direction is the shape the card is about rather than a detail of + * this file: a lookup that comes back empty must be an ERROR, never silence. + * The same file already had it right one invariant over -- `runBehaviourProbes` + * refuses a probe naming an entry point that no longer exists, because it "is + * asserting nothing" -- so the ledger was the odd one out, not a new idea. + * + * ## Vacuity floors -- an empty sweep reports what a clean tree reports + * + * Every count in this gate's pass line is also a way for it to pass having read + * nothing: a manifest walk that discovers nothing, an `exports` resolver that + * reads no `require` condition, a CommonJS collector that matches no file, a + * probe table that was emptied. Each produces zero findings, and zero findings + * is exactly what success looks like. So each carries a floor measured on + * `8cb96ec41b` and held with margin, and below any of them the gate REFUSES + * (`exit 2`) rather than passing. Same idiom and same reason as + * `check-keyed-text-bounds.mjs` (five floors) and + * `check-undeclared-dep-imports.mjs` (three). * * ## Where it runs, and why not in the lint job * @@ -215,8 +244,70 @@ const DUAL_FORMAT_BEHAVIOUR_PROBES = [ const EXIT_OK = 0; const EXIT_FINDINGS = 1; +const EXIT_REFUSE = 2; const EXIT_PREREQ = 3; +// --------------------------------------------------------------------------- +// Vacuity floors -- see the header. Measured on `8cb96ec41b` (a full `pnpm +// build`, then this gate), each floor held with margin. Re-measuring UP is +// free; LOWERING one to make a run pass is the move this block exists to make +// visible in a diff. + +const MEASURED = Object.freeze({ entries: 103, packages: 67, cjsFiles: 613, probes: 1 }); +const MIN_ENTRIES = 90; +const MIN_PACKAGES = 58; +const MIN_CJS_FILES = 520; +const MIN_PROBES = 1; + +/** + * The first floor a run falls below, as a refusal message -- or `null` when + * every count clears. Pure, so `--self-test` drives every floor with no tree. + * + * @param {{entries?: number, packages?: number, cjsFiles?: number, probes?: number}} counts + * @returns {string | null} + */ +export function floorProblem(counts) { + const rows = [ + [counts?.entries ?? 0, MIN_ENTRIES, MEASURED.entries, 'published `require` entry point(s)', + 'The manifest walk or the `exports` resolver broke. With no entries nothing is required, nothing is parsed, and the gate prints what a clean tree prints.'], + [counts?.packages ?? 0, MIN_PACKAGES, MEASURED.packages, 'publishable package(s)', + 'Entries were found but collapsed onto a fraction of the tree — the walk is reading part of `packages/`, not the whole of it.'], + [counts?.cjsFiles ?? 0, MIN_CJS_FILES, MEASURED.cjsFiles, 'emitted CommonJS file(s)', + 'This is the PARSES population. `commonJsFilesUnder` matched (almost) nothing, so `node --check` ran over an empty set and every byte we emit went unread.'], + [counts?.probes ?? 0, MIN_PROBES, MEASURED.probes, 'cross-format behaviour probe(s) run', + 'AGREES is the invariant loading alone cannot give you, and an empty probe table satisfies it vacuously.'], + ]; + for (const [got, min, measured, what, why] of rows) { + if (got >= min) continue; + return `measured only ${got} ${what}, below the floor of ${min} (${measured} on 8cb96ec41b).\n` + + ` ${why}\n` + + ' ⛔ NOT a pass: nothing, or nearly nothing, was read.'; + } + return null; +} + +/** + * Ledger rows naming an id the discovered population does not contain. Pure. + * + * A separate pass over `Object.keys(ledger)` rather than another branch inside + * the row walk, and that is the whole point: the row walk can only ever reach a + * key some row NAMES, so the orphan direction is unreachable from there. See + * the header for the measurement. + * + * @param {Record} ledger + * @param {{id: string}[]} rows + * @returns {string[]} + */ +export function orphanLedgerRows(ledger, rows) { + const ids = new Set((rows ?? []).map((r) => r.id)); + return Object.keys(ledger ?? {}) + .filter((id) => !ids.has(id)) + .sort() + .map((id) => `${id} — ${BASELINE_PATH} exempts an entry point that is not in the population: ` + + 'no published `require` condition resolves to it. Delete the row — it is exempting nothing, ' + + 'and a reader takes it for coverage that was never checked.'); +} + const PARSE_CONCURRENCY = 8; // --------------------------------------------------------------------------- @@ -451,7 +542,7 @@ export function isParseFailure(stderr) { // --------------------------------------------------------------------------- /** - * @returns {Promise<{rows: any[], findings: string[], prereq: string[], ledgerHits: string[], staleLedger: string[], cjsFileCount: number, probesRun: number}>} + * @returns {Promise<{rows: any[], findings: string[], prereq: string[], ledgerHits: string[], staleLedger: string[], orphanLedger: string[], cjsFileCount: number, probesRun: number}>} */ export async function scan(root, ledger, probes = DUAL_FORMAT_BEHAVIOUR_PROBES) { const rows = collectEntries(root); @@ -459,6 +550,10 @@ export async function scan(root, ledger, probes = DUAL_FORMAT_BEHAVIOUR_PROBES) const prereq = []; const ledgerHits = []; const staleLedger = []; + // Computed off the population alone, so it survives the prerequisite early + // return below: an orphaned exemption is a fact about the ledger, not about + // whether anything was built. + const orphanLedger = orphanLedgerRows(ledger, rows); let cjsFileCount = 0; for (const r of rows) { @@ -472,7 +567,7 @@ export async function scan(root, ledger, probes = DUAL_FORMAT_BEHAVIOUR_PROBES) r.cjsFiles = commonJsFilesUnder(r.distDir, r.isModuleType); cjsFileCount += r.cjsFiles.length; } - if (prereq.length) return { rows, findings, prereq, ledgerHits, staleLedger, cjsFileCount, probesRun: 0 }; + if (prereq.length) return { rows, findings, prereq, ledgerHits, staleLedger, orphanLedger, cjsFileCount, probesRun: 0 }; // PARSES — over the union of emitted CommonJS files, deduped across the // several entries a package may declare. @@ -512,7 +607,7 @@ export async function scan(root, ledger, probes = DUAL_FORMAT_BEHAVIOUR_PROBES) const probeResults = await runBehaviourProbes(root, rows, probes); findings.push(...probeResults.findings); - return { rows, findings, prereq, ledgerHits, staleLedger, cjsFileCount, probesRun: probeResults.ran }; + return { rows, findings, prereq, ledgerHits, staleLedger, orphanLedger, cjsFileCount, probesRun: probeResults.ran }; } /** Read the expectation a probe declares. Only `spec-version` exists today. */ @@ -575,7 +670,7 @@ function readLedger(root) { async function main(argv) { const root = REPO_ROOT; const ledger = readLedger(root); - const { rows, findings, prereq, ledgerHits, staleLedger, cjsFileCount, probesRun } = await scan(root, ledger); + const { rows, findings, prereq, ledgerHits, staleLedger, orphanLedger, cjsFileCount, probesRun } = await scan(root, ledger); if (argv.includes('--list')) { for (const r of rows) console.log(`${r.id.padEnd(48)} ${r.target}`); @@ -591,7 +686,21 @@ async function main(argv) { return EXIT_PREREQ; } - const problems = [...findings, ...staleLedger]; + // ⛔ Before any verdict: a run that read (almost) nothing must refuse, not + // report the clean tree. Ordered after the prerequisite check so an unbuilt + // tree still answers 3 — "nothing was measured" has its own code. + const floor = floorProblem({ + entries: rows.length, + packages: new Set(rows.map((r) => r.pkg)).size, + cjsFiles: cjsFileCount, + probes: probesRun, + }); + if (floor !== null) { + console.error(`check:dual-build-cjs-loads REFUSES — ${floor}`); + return EXIT_REFUSE; + } + + const problems = [...findings, ...staleLedger, ...orphanLedger]; if (problems.length) { console.error(`✗ check:dual-build-cjs-loads — ${problems.length} finding(s) across ${rows.length} published require entry point(s):`); for (const f of problems) console.error(` ✗ ${f}`); @@ -705,6 +814,25 @@ export async function selfTest() { const r3 = await scan(root, { '@t/good#.': { reason: 'stale' } }, []); t('a ledger entry that now loads is a finding (shrink-only)', r3.staleLedger.some((s) => s.startsWith('@t/good#.')), JSON.stringify(r3.staleLedger)); + // THE #13014 case: a row whose id left the population. Unreachable from + // the row walk by construction, so it needs its own pass and its own pin. + const rOrphan = await scan(root, { '@t/vanished#./gone': { reason: 'exempts an entry point that no longer exists' } }, []); + t('THE #13014 case: a ledger row naming an entry NOT in the population is a finding', + rOrphan.orphanLedger.some((s) => s.startsWith('@t/vanished#./gone')), JSON.stringify(rOrphan.orphanLedger)); + t('…and the finding says the row is exempting nothing', + rOrphan.orphanLedger.some((s) => s.includes('exempting nothing'))); + t('…and it is reported even with the rest of the tree clean', + rOrphan.orphanLedger.length === 1, JSON.stringify(rOrphan.orphanLedger)); + // GREEN CONTROL — the half that proves the new pass is not just "always + // red". A row that DOES name a live entry point must stay silent here. + t('GREEN CONTROL — a ledger row naming a live entry point is not an orphan', + r2.orphanLedger.length === 0, JSON.stringify(r2.orphanLedger)); + t('GREEN CONTROL — an empty ledger produces no orphans', r1.orphanLedger.length === 0); + // Pure, so it can be driven without a tree at all. + t('orphanLedgerRows() reads the ids, not the order', + orphanLedgerRows({ 'b#.': {}, 'a#.': {} }, [{ id: 'a#.' }]).length === 1 + && orphanLedgerRows({ 'b#.': {}, 'a#.': {} }, [{ id: 'a#.' }])[0].startsWith('b#.')); + // ── AGREES: the cross-format behaviour probe, both directions ──────────── writePkg(root, 'agree', { name: '@t/agree', version: '0.0.0', ...dual }, { 'dist/index.js': "export const v = () => 'same';\n", @@ -757,10 +885,37 @@ export async function selfTest() { rmSync(root, { recursive: true, force: true }); } + // ── the vacuity floors, each driven to zero ────────────────────────────── + // + // A floor that cannot refuse is the joke this card is about, so every one is + // driven down individually AND the measured tuple is asserted to clear them + // all — a floor accidentally set above its own measurement would red every + // real run, which is the opposite failure and just as invisible in review. + const full = { entries: MEASURED.entries, packages: MEASURED.packages, cjsFiles: MEASURED.cjsFiles, probes: MEASURED.probes }; + t('FLOOR — the values measured on 8cb96ec41b clear every floor', floorProblem(full) === null, JSON.stringify(floorProblem(full))); + t('FLOOR — a dead manifest walk refuses', floorProblem({ ...full, entries: 0 }) !== null); + t('FLOOR — entries collapsed onto too few packages refuses', floorProblem({ ...full, packages: 0 }) !== null); + t('FLOOR — a dead CommonJS collector refuses (PARSES over an empty set)', floorProblem({ ...full, cjsFiles: 0 }) !== null); + t('FLOOR — an emptied probe table refuses (AGREES satisfied vacuously)', floorProblem({ ...full, probes: 0 }) !== null); + t('FLOOR — a missing count is zero, not "unmeasured but fine"', floorProblem({}) !== null); + t('FLOOR — the refusal names the count, the floor and the measurement', + /measured only 0 .* below the floor of \d+ \(613 on 8cb96ec41b\)/s.test(floorProblem({ ...full, cjsFiles: 0 }) ?? ''), + JSON.stringify(floorProblem({ ...full, cjsFiles: 0 }))); + t('FLOOR — every floor sits at or below the value it was measured from', + MIN_ENTRIES <= MEASURED.entries && MIN_PACKAGES <= MEASURED.packages + && MIN_CJS_FILES <= MEASURED.cjsFiles && MIN_PROBES <= MEASURED.probes); + t('FLOOR — the refusal code is distinct from findings and prerequisite', + EXIT_REFUSE !== EXIT_FINDINGS && EXIT_REFUSE !== EXIT_PREREQ && EXIT_REFUSE !== EXIT_OK); + // ── the real ledger is well-formed and shrink-only in shape ─────────────── const realLedger = readLedger(REPO_ROOT); t('every real ledger entry carries a reason', Object.values(realLedger).every((v) => typeof v?.reason === 'string' && v.reason.length > 20)); t('every real ledger key is `#`', Object.keys(realLedger).every((k) => /^[^#]+#(\.|\.\/.+|\(main\))$/.test(k))); + // The shipped ledger against the REAL population — the shipped half of the + // orphan direction, and the one a self-test over fixtures cannot give. + t('every shipped ledger row names a live require entry point', + orphanLedgerRows(realLedger, collectEntries(REPO_ROOT)).length === 0, + JSON.stringify(orphanLedgerRows(realLedger, collectEntries(REPO_ROOT)))); const failed = cases.filter((c) => !c.ok); for (const c of failed) console.error(` ✗ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`); @@ -768,7 +923,7 @@ export async function selfTest() { console.error(`✗ check-dual-build-cjs-loads self-test: ${failed.length} of ${cases.length} case(s) failed.`); return 1; } - console.log(`✓ check-dual-build-cjs-loads self-test: ${cases.length} cases pass (real emitted bytes, real spawns; both ledger directions, and the parse failure the ledger may never silence).`); + console.log(`✓ check-dual-build-cjs-loads self-test: ${cases.length} cases pass (real emitted bytes, real spawns; both stale-ledger directions including the orphan one, every vacuity floor driven to zero with its green control, and the parse failure the ledger may never silence).`); return EXIT_OK; } From 868c5768777e0429db03cfeb73ba9f9cb555e71c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 00:55:49 +0000 Subject: [PATCH 2/2] docs(scripts): correct check-stack-collection-maps' account of how SECURITY_FIELDS went unread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The header said the flat extractor 'could not read it and the site was skipped instead of failing'. Re-measured on 8cb96ec41b: false. This gate has refused an empty extraction since #7032, and swapping the site's extractor back to stringArrayItems exits 1 by name. The site was skipped because it was never a SITE — SECURITY_FIELDS occurred 0 times in this file before #13009 (control ARTIFACT_FIELD_TO_TYPE: 5). The hole was the hand-written SITES population, not the extractor set. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw --- scripts/check-stack-collection-maps.mjs | 40 ++++++++++++++++++++----- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/scripts/check-stack-collection-maps.mjs b/scripts/check-stack-collection-maps.mjs index 6ac93cbd94..b60402b71b 100644 --- a/scripts/check-stack-collection-maps.mjs +++ b/scripts/check-stack-collection-maps.mjs @@ -26,10 +26,33 @@ // the argument for this gate in one line.) // // `SECURITY_FIELDS` joined as the eighth at #12894, and how it was missing is -// the point rather than a footnote: it is the ONLY one of the eight that pairs -// its keys as `[collection, kind]` TUPLES, so the two extractors this gate -// already had (object keys, flat string arrays) could not read it and the site -// was skipped instead of failing. It carried a `policies` dead pointer -- the +// the point rather than a footnote. ⚠️ It is ALSO the sentence this comment +// first got wrong, and the correction is the more useful half. The original +// wording said the flat extractor "could not read it and the site was skipped +// instead of failing"; #13014 re-measured that on `8cb96ec41b` and it is FALSE. +// This gate has refused an empty extraction since #7032 -- an extract that +// returns nothing is a FAILURE in the runner below, never a pass -- so a tuple +// site read with the flat extractor goes RED, measured by swapping this site's +// extractor back to `stringArrayItems`: exit 1, "SECURITY_FIELDS -- could not +// extract the enumeration from packages/runtime/src/app-plugin.ts". +// +// The site was skipped because it was never a SITE. `SECURITY_FIELDS` occurred +// ZERO times in this file before #13009 (positive control in the same probe: +// `ARTIFACT_FIELD_TO_TYPE`, 5 occurrences), so no extractor ever ran on it and +// there was nothing to come back empty. The hole was the hand-written SITES +// population -- which is answerable to nothing and cannot be derived -- not the +// extractor set. Worth keeping straight in both directions: a reader who +// believes the original wording concludes the empty-extraction floor is missing +// and adds a second one, and a reader who trusts the SITES list to be complete +// repeats #12894. It is the eighth site because someone went looking, and that +// is still the only way a ninth gets found. (#13014 measured the mechanical +// alternative too: sweeping the tree for literals naming 8+ stack collections +// returns 45 files, 38 of them not sites -- an allowlist that relocates the +// hand-written list rather than deleting it.) +// +// The tuple shape is still why the site needed a THIRD extractor rather than +// reusing one: it is the ONLY one of the eight that pairs its keys as +// `[collection, kind]` TUPLES. It carried a `policies` dead pointer -- the // same retired kind waived on two other sites -- and removing that entry from // the artifact door while leaving this one unpinned would have left exactly one // place where the fourth instance of a twice-retired pattern could land back @@ -272,10 +295,11 @@ export function stringArrayItems(body) { * disagree about what "the site enumerates" means: the flat form's strings ARE * the keys, and here only the tuple's head is, with the tail naming the metadata * kind. Reading a tuple site with the flat extractor returns an empty list at - * depth 0 -- which reconciles against everything and reports no drift, the - * silent-no-op shape this gate exists to refuse. (The caller turns an empty - * result into a FAILURE for that reason; this extractor makes the non-empty - * answer available instead.) + * depth 0 -- and the caller REFUSES that, loudly, by name: an empty extraction + * is a failure in `run()`, never a reading. ⛔ Do not read this paragraph as + * "the flat extractor would have passed vacuously" -- it would not, and #13014 + * measured it (exit 1). This extractor exists so the site has a non-empty + * answer at all, not to rescue the gate from a silent pass it never had. */ export function tupleFirstItems(body) { const mask = maskLiterals(body);