diff --git a/scripts/check-position-name-fold-loaders.mjs b/scripts/check-position-name-fold-loaders.mjs index 5e94de5be75..578e5a68607 100644 --- a/scripts/check-position-name-fold-loaders.mjs +++ b/scripts/check-position-name-fold-loaders.mjs @@ -111,7 +111,27 @@ const DECLARED_INSTRUMENTS = [ 'scripts/check-position-name-fold-loaders.mjs', ]; -const SCAN_ROOTS = ['packages', 'examples', 'apps', 'scripts', 'tools', '.github']; +/** + * The roots the walk visits, spelled as the subtree population this gate + * DECLARES to `scripts/pm/dispatch-gates.mjs` (the `ROOT_DIR_WATCH_HINTS` + * idiom; `check:watch-hint-literal` holds it to a literal array the extractor + * admits). A literal is how the declaration reaches the derivation: the + * extractor reads source text, and a bare single-segment word (`'packages'`) + * yields no hint at all, so while the walk read a private list of bare words a + * card editing any file under these roots was told nothing about this gate and + * met it first in CI (#24037). + * + * ONE list, two readers: {@link SCAN_ROOTS} is DERIVED from it, so the walk and + * the declaration cannot drift apart. ⛔ Never hand-copy the roots back into a + * second literal — the declaration would then be free to say one thing while + * the walk does another, which is the silent-green shape the derivation exists + * to refuse. Every root is live in the tree: a root the tree does not have + * scans nothing and would be printed by the derivation as a dead lead. + */ +export const ROOT_DIR_WATCH_HINTS = ['packages/**', 'examples/**', 'apps/**', 'scripts/**', '.github/**']; + +/** The directories the walk descends from, read off {@link ROOT_DIR_WATCH_HINTS}. */ +const SCAN_ROOTS = ROOT_DIR_WATCH_HINTS.map((hint) => hint.replace(/\/\*\*$/, '')); const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', '.turbo', '.next', 'coverage', '.git']); diff --git a/scripts/pm/bare-root-worklist.mjs b/scripts/pm/bare-root-worklist.mjs index 99ddf7245bf..4f6af7069d4 100644 --- a/scripts/pm/bare-root-worklist.mjs +++ b/scripts/pm/bare-root-worklist.mjs @@ -942,31 +942,16 @@ const TRIAGE = new Map([ + 'skills, IS declared beside the constant: that one is small, is the surface the gate ' + 'exists for, and naming a skills-only card is the discoverability this idiom is for', }], - ['scripts/check-position-name-fold-loaders.mjs SCAN_ROOTS packages', { - verdict: 'REFUSE-WIDE', - why: 'every text file under the root is walked and any one of them could name the #13419 ' - + 'fixture — 5562 of 5625 (99%), measured 2026-08-31. The population is not a subset of the ' - + 'root that a narrower glob could describe; it IS the root. A true declaration here would ' - + 'name this gate on every card touching a package, which is the precision trade this ' - + 'verdict refuses', - }], - ['scripts/check-position-name-fold-loaders.mjs SCAN_ROOTS examples', { - verdict: 'REFUSE-WIDE', - why: '240 of 243 (99%). Refused with its packages half rather than split from it: the loader ' - + 'this gate hunts is a COMPOSITION site, and nothing says one lands under examples/ rather ' - + 'than packages/ — declaring only the small root would read as a claim about where that ' - + 'happens, which is exactly what is not known', - }], - ['scripts/check-position-name-fold-loaders.mjs SCAN_ROOTS apps', { - verdict: 'REFUSE-WIDE', - why: '36 of 40 (90%) — same trade, same reason as the examples half', - }], - ['scripts/check-position-name-fold-loaders.mjs SCAN_ROOTS scripts', { - verdict: 'REFUSE-WIDE', - why: '295 of 298 (99%). This root is where the gate own allowed readers live ' - + '(DECLARED_INSTRUMENTS), so it is walked wholesale for the same reason as the others: a ' - + 'new reader is found, then classified, never assumed absent', - }], + // WITHDRAWN 2026-10-18 (#24037): the four REFUSE-WIDE rows of + // check-position-name-fold-loaders.mjs SCAN_ROOTS (packages 99%, examples + // 99%, apps 90%, scripts 99%, measured 2026-08-31). Ruling 6243472660 (the + // maintainer's letter B on #24171, term 3) orders that gate's roots made the + // ROOT_DIR_WATCH_HINTS declaration the derivation reads, on a MEASURED + // consumer — PR #24028 reconciled its local gate union complete and met this + // gate red in CI. The gate now declares the whole roots and derives + // SCAN_ROOTS from that literal, so the bare words these rows judged no longer + // exist and the verdicts left with them (the STALE remedy below). The + // measured reasons stand in this file's history at f5ef054a. ['scripts/check-single-authz-resolver.mjs SCAN_ROOTS packages', { verdict: 'REFUSE-WIDE', why: 'walks every non-test TS source under the root — 1898 of 4903 (39%). True, and it would ' @@ -1349,7 +1334,8 @@ const TRIAGE = new Map([ * `check:type-check-coverage`/`check:type-check-debt` 55.8%). Each is measured on this tree by the SAME method as * every other row here, and the ratio does not change the verdict either * direction — existing `TRIAGE` rows already span 39% (`check:authz-resolver`) - * to 99% (`scripts/check-position-name-fold-loaders.mjs`) under the identical + * to 99% (`scripts/check-position-name-fold-loaders.mjs`, a row since withdrawn + * when that gate declared its roots — #24037) under the identical * REFUSE-WIDE trade, wide or narrow within that span is still the same * width-alone trade. Recorded as measured rather than clipped to the summary * range; the discrepancy against the issue's own "32–52%" framing is stated @@ -2113,7 +2099,10 @@ const SELF_TEST_BATTERIES = Object.freeze({ // ⭐ 43 → 128 (#15926): the loop now runs over EVERY recorded row rather than // the eleven DECLARED-NARROWER ones, and the RED end of the new direction is // driven on constructed gates beside it. A FLOOR, measured on a run. - "The DECLARATION a row describes, read from the gate's own SOURCE": 128, + // 128 → 126 (#24037): the loop registers two cases per recorded row, and the + // four check-position-name-fold-loaders rows were withdrawn (134 → 126 on a + // run) — fewer rows to judge, not cases that stopped registering. + "The DECLARATION a row describes, read from the gate's own SOURCE": 126, 'CENSUS_REFUSE_WIDE, pinned on its OWN terms (#14695)': 5, }); diff --git a/scripts/pm/dispatch-gates.self-test.mjs b/scripts/pm/dispatch-gates.self-test.mjs index 61d68d432f8..93bfd52fb43 100644 --- a/scripts/pm/dispatch-gates.self-test.mjs +++ b/scripts/pm/dispatch-gates.self-test.mjs @@ -3851,6 +3851,52 @@ export function selfTest({ tier = 'full' } = {}) { ); } + // ── The name-fold loader gate's WALKED roots, declared (#24037) ─────────── + // + // `check-position-name-fold-loaders.mjs` reads every text file under its + // roots, and kept them as a private list of bare words, which the extractor + // admits as nothing. A card editing a file under those roots therefore + // derived no command for it: PR #24028 reconciled its local union complete + // and met this gate red in CI, over a line it added to + // `scripts/cross-package-test-inputs.mjs`. The gate now declares the roots + // with the ROOT_DIR_WATCH_HINTS idiom and derives its walk from that literal, + // with no change to the extractor. + // + // Pinned against the LIVE tree, for the reason the scripts/** case above + // states. The probes are read off the family's own extracted hints rather + // than retyped here, so every root the gate declares gets one, and the first + // probe is the exact path the measured miss edited. The ablation strips the + // subtree hints and the verdict must leave MATCHED, so these cases cannot pass + // through another literal that happens to cover a probe. + const NAME_FOLD = 'scripts/check-position-name-fold-loaders.mjs'; + const nameFoldFamilies = discoverFamilies().byCheck; + for (const key of [NAME_FOLD, `${NAME_FOLD} --self-test`]) { + const entry = nameFoldFamilies.get(key); + t(`${key} is discovered at all — the pins below mean nothing without this`, Boolean(entry)); + const roots = (entry?.hints ?? []).filter((h) => h.endsWith('/**')).map((h) => h.slice(0, -'/**'.length)); + // The floor: the five roots the walk reads at this commit. A loop over an + // emptied hint set runs zero cases and prints nothing, which reads as a pass. + t(`${key} declares at least the five subtree roots its walk reads — read: [${roots.join(', ')}]`, roots.length >= 5); + const probes = ['scripts/cross-package-test-inputs.mjs', ...roots.map((r) => `${r}/the-one-nobody-has-written-yet.ts`)]; + for (const probe of probes) { + const verdict = entry ? classifyEntry(entry, [probe]).verdict : null; + t(`a change to ${probe}, under a root this gate walks, derives ${key} — read: ${verdict}`, verdict === 'matched'); + } + const undeclared = entry ? { ...entry, hints: entry.hints.filter((h) => !h.endsWith('/**')) } : null; + const residual = undeclared + ? ['scripts/cross-package-test-inputs.mjs', 'packages/the-one-nobody-has-written-yet/src/index.ts'] + .map((p) => classifyEntry(undeclared, [p]).verdict) + : []; + t( + `…and it is the subtree declaration doing it: strip it and ${key} is NOT MATCHED for the measured miss ` + + `or a brand-new package file — read: [${residual.join(', ')}]`, + Boolean(undeclared) + && undeclared.hints.length < entry.hints.length + && residual.length === 2 + && residual.every((v) => ['silent', 'undetermined'].includes(v)), + ); + } + // ── The MANIFEST population of check:merge-driver (#15501) ──────────────── // // That family's `git-merge-regen --self-test` refuses a generator with no