@@ -1818,13 +1818,22 @@ export { maskComments };
18181818 * Re-derived at 6193e576d across the 169 scripts under `scripts/` that carry
18191819 * one — 185 declarations: 111 `function selfTest(`, 28
18201820 * `export function selfTest(`, 19 `async function selfTest(`, 8
1821- * `export async function selfTest(`, and 19 compound names — one of which is
1822- * this module's own `maskSelfTests`, so this file's masker blanks its own body
1823- * whenever it scans itself. That was true long before the helper follow below
1824- * existed and it costs nothing today, because none of the functions it reaches
1825- * spells a path; it is recorded because it is the kind of thing that stops
1826- * being free the day one of them does. The load-bearing half holds: 185 of 185
1827- * at column 0, and a column-0 scan for the const/arrow spelling finds ZERO.
1821+ * `export async function selfTest(`, and 19 compound names. The load-bearing
1822+ * half holds: 185 of 185 at column 0, and a column-0 scan for the const/arrow
1823+ * spelling finds ZERO.
1824+ *
1825+ * ⚠ A name is not a role, so this anchor also fires on production code whose
1826+ * name merely spells self-test — `maskSelfTests` six hundred lines below is one,
1827+ * which is why this file's masker blanks its own body whenever it scans itself.
1828+ * ⛔ That is NOT one specimen: it is a class with seven live members over four
1829+ * files, and this docblock claimed it was one until a census counted them. The
1830+ * census, what each member costs, why this pattern is deliberately NOT narrowed
1831+ * to exclude them, and the live pin that keeps their cost at zero all live at
1832+ * `COMPOUND_ANCHOR_LEDGER`. Read that table before touching this pattern; the
1833+ * short version is that one spelling, `runSelfTest`, is a genuine entry point in
1834+ * one script and production code in another, so no name predicate can separate
1835+ * the two classes and narrowing this anchor can only trade a silence that costs
1836+ * nothing for a fabricated lead that does.
18281837 * ⚠ The counts in this paragraph read 61/53/7/4 when it was written and had
18291838 * gone stale by a factor of three before anyone re-read them — re-derive them
18301839 * rather than quoting them; only the two PROPERTIES are what this pattern
@@ -2143,6 +2152,188 @@ export function maskSelfTests(source) {
21432152 return blank(source, flags);
21442153}
21452154
2155+ /**
2156+ * The source extensions the name-anchor census below reads. The anchor is a
2157+ * JavaScript declaration shape, so the corpus is the tree's JavaScript and
2158+ * TypeScript, and nothing else.
2159+ */
2160+ const ANCHOR_CENSUS_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/;
2161+
2162+ /**
2163+ * The bare name this tree's self-test convention spells. Every other name the
2164+ * anchor matches is COMPOUND, and a compound name is the only place the anchor
2165+ * can be wrong in either direction.
2166+ */
2167+ const BARE_ENTRY_POINT_NAME = 'selfTest';
2168+
2169+ /**
2170+ * Every COMPOUND-name declaration the self-test anchor matches, tree-wide, and
2171+ * whether that match is what the anchor MEANT.
2172+ *
2173+ * ## The defect this ledger answers
2174+ *
2175+ * `SELF_TEST_DECL` decides "this is a self-test" from the declaration's NAME.
2176+ * A name is not a role, so the anchor also fires on production code whose name
2177+ * merely spells self-test — and when it does, `maskSelfTests` blanks that
2178+ * production body, and `extractWatchHints` never sees the paths in it. The
2179+ * failure direction is SILENCE: a hint that is never extracted cannot be
2180+ * missed, so a gate family quietly stops being derived for a file it really
2181+ * opens.
2182+ *
2183+ * The specimen that opened this is `maskSelfTests` itself, six lines above:
2184+ * `mask` + `Self` + `Test` + `s` matches, so this module's masker blanks its own
2185+ * body whenever the module scans itself.
2186+ *
2187+ * ## The census, re-derived on this tree
2188+ *
2189+ * 209 code-position matches over the tracked JS/TS corpus. 187 are the bare
2190+ * `selfTest`; the remaining 22 carry compound names over 21 distinct spellings,
2191+ * and they are the rows below. Fifteen are genuine self-test batteries — the
2192+ * anchor firing on them is the anchor working. SEVEN are production code:
2193+ *
2194+ * scripts/check-self-test-wired.mjs carriesSelfTest
2195+ * scripts/check-self-test-workflow-commands.mjs runSelfTest
2196+ * scripts/check-step-collectors.mjs selfTestTargets
2197+ * scripts/check-step-collectors.mjs selfTestDiscoveries
2198+ * scripts/measure-self-test-floor.mjs selfTestDefs
2199+ * scripts/pm/dispatch-gates.mjs selfTestOnlyCallables
2200+ * scripts/pm/dispatch-gates.mjs maskSelfTests
2201+ *
2202+ * Every one of them is a gate that REASONS ABOUT self-tests, which is why they
2203+ * cluster: a tool that finds, spawns, counts or masks other scripts' self-tests
2204+ * names its functions after the thing it handles, and the anchor cannot tell
2205+ * "runs a self-test" from "is one".
2206+ *
2207+ * ## What it costs today: nothing, MEASURED, and that is the whole point
2208+ *
2209+ * Neutralising each of the seven one at a time and re-extracting moves no hint
2210+ * in any of the four files. The claim is therefore live rather than recalled —
2211+ * and it is exactly the kind of claim that stops being true without anything
2212+ * going red, which is what the pin in this module's self-test exists to catch.
2213+ *
2214+ * The same measurement over the fifteen genuine rows is NOT zero, and that
2215+ * asymmetry is what makes the classification load-bearing rather than
2216+ * decorative: `fixtureSelfTest` drops `packages/spec/spec-changes.json` and
2217+ * `prePushIsArmedSelfTest` drops `.githooks/pre-push`, both fixture paths in
2218+ * `scripts/check-regen-pending.mjs`, both correctly refused. So "no
2219+ * compound-name match may contribute a hint" is FALSE as a blanket invariant;
2220+ * the invariant holds only over the accidental half, and only a classification
2221+ * can name that half.
2222+ *
2223+ * ## ⛔ Why the anchor is NOT narrowed, and why nothing is special-cased
2224+ *
2225+ * The obvious repairs were both refuted by the census rather than judged:
2226+ *
2227+ * - **Narrowing the name pattern is impossible.** `runSelfTest` is a GENUINE
2228+ * entry point in `scripts/check-turbo-task-graph.mjs`, reached only from
2229+ * that file's `--self-test` guard, and ACCIDENTAL in
2230+ * `scripts/check-self-test-workflow-commands.mjs`, where it is exported and
2231+ * spawns other scripts' self-tests from the gate body. One spelling, both
2232+ * classes. No predicate over the name can separate them, so any narrowing
2233+ * that excludes the accidental one also unmasks a real self-test battery and
2234+ * readmits its fixture paths as hints — the fabricated-lead family this
2235+ * whole masker exists to refuse, traded for a silence that costs nothing.
2236+ * - **Special-casing this module's own path fixes two rows of seven.** The
2237+ * other five live in three other files, so the objection that a rename
2238+ * "fixes one instance and leaves the class" applies to it too, one file
2239+ * wider — and it would make the tool's self-scan differ from every other
2240+ * scan, which is a hazard of its own.
2241+ *
2242+ * ⇒ What ships is neither. The anchor keeps firing on all 22, the mask keeps
2243+ * blanking all 22, and the cost of the seven accidental ones is MEASURED on
2244+ * every run instead of asserted in prose. Silence was the defect; the remedy is
2245+ * noise on the day it starts costing something.
2246+ *
2247+ * ## Maintaining this table
2248+ *
2249+ * A compound-name declaration this table does not list reds
2250+ * `check:pm-dispatch-gates`. Classify it and add a row: `accidental: false` if
2251+ * it is a self-test battery (its fixtures SHOULD be masked away), `true` if it
2252+ * is production code the anchor caught by accident — in which case the pin then
2253+ * measures, and keeps measuring, that masking it costs no hint. ⛔ Do not
2254+ * "repair" a red by renaming the function to dodge the anchor: the row is the
2255+ * record, and the next accidental name is the one nobody will notice.
2256+ */
2257+ const COMPOUND_ANCHOR_LEDGER = [
2258+ ['packages/lint/scripts/check-doc-formula-expressions.mjs', 'specSelfTest', false],
2259+ ['packages/lint/scripts/check-doc-formula-expressions.mjs', 'fieldRuleSelfTest', false],
2260+ ['scripts/check-comment-mask-corpus.mjs', 'runSelfTestCases', false],
2261+ ['scripts/check-doc-authoring.mjs', 'selfTestRule3', false],
2262+ ['scripts/check-doc-authoring.mjs', 'selfTestPackagesProse', false],
2263+ ['scripts/check-durability-degradation-log-level.mjs', 'selfTestReadSeams', false],
2264+ ['scripts/check-platform-checklist.mjs', 'selfTestTrapVocabulary', false],
2265+ ['scripts/check-platform-checklist.mjs', 'selfTestProvisioningUse', false],
2266+ ['scripts/check-platform-checklist.mjs', 'selfTestUnreferencedRecipes', false],
2267+ ['scripts/check-platform-checklist.mjs', 'selfTestMetaCallSpelling', false],
2268+ ['scripts/check-platform-checklist.mjs', 'selfTestSourceLineCitations', false],
2269+ ['scripts/check-regen-pending.mjs', 'fixtureSelfTest', false],
2270+ ['scripts/check-regen-pending.mjs', 'prePushIsArmedSelfTest', false],
2271+ ['scripts/check-regen-pending.mjs', 'decisionTableSelfTest', false],
2272+ ['scripts/check-turbo-task-graph.mjs', 'runSelfTest', false],
2273+ ['scripts/check-self-test-wired.mjs', 'carriesSelfTest', true],
2274+ ['scripts/check-self-test-workflow-commands.mjs', 'runSelfTest', true],
2275+ ['scripts/check-step-collectors.mjs', 'selfTestTargets', true],
2276+ ['scripts/check-step-collectors.mjs', 'selfTestDiscoveries', true],
2277+ ['scripts/measure-self-test-floor.mjs', 'selfTestDefs', true],
2278+ ['scripts/pm/dispatch-gates.mjs', 'selfTestOnlyCallables', true],
2279+ ['scripts/pm/dispatch-gates.mjs', 'maskSelfTests', true],
2280+ ];
2281+
2282+ /**
2283+ * The ledger as `"<file>::<name>"` keys. `runSelfTest` alone proves the key has
2284+ * to carry the file: that one spelling is a genuine entry point in one script
2285+ * and production code in another, so a name-keyed ledger could not hold both
2286+ * verdicts at once — the same reason the anchor itself cannot be narrowed.
2287+ */
2288+ export const COMPOUND_ANCHOR_KEYS = new Map(
2289+ COMPOUND_ANCHOR_LEDGER.map(([file, name, accidental]) => [`${file}::${name}`, accidental]),
2290+ );
2291+
2292+ /**
2293+ * Every COMPOUND-name declaration the self-test anchor matches in `source`, at
2294+ * CODE positions only.
2295+ *
2296+ * Comments are masked first for the same reason `maskSelfTests` composes them
2297+ * first: this tree's docblocks quote declaration shapes at column 0, and a
2298+ * quoted one is prose, not a declaration. A match inside a string literal is
2299+ * excluded by the same means the mask uses — `scanSource`'s literal map — so a
2300+ * self-test fixture that BUILDS a module source cannot enter the census as if
2301+ * it were a declaration of the file holding it.
2302+ */
2303+ export function compoundAnchorDecls(source) {
2304+ const scan = scanSource(source);
2305+ const decommented = maskComments(source);
2306+ const out = [];
2307+ for (const m of decommented.matchAll(SELF_TEST_DECL)) {
2308+ if (scan.comment[m.index] || scan.literal[m.index]) continue;
2309+ const name = m[0].match(/function[ \t]+([A-Za-z0-9_$]+)/)[1];
2310+ if (name === BARE_ENTRY_POINT_NAME) continue;
2311+ out.push({ name, index: m.index, line: decommented.slice(0, m.index).split('\n').length });
2312+ }
2313+ return out;
2314+ }
2315+
2316+ /**
2317+ * `source` with one declaration renamed so the self-test anchor no longer sees
2318+ * it — the counterfactual the accidental half is measured against.
2319+ *
2320+ * The rename replaces the self-test token INSIDE the identifier rather than
2321+ * appending or truncating, because the anchor matches the token anywhere in the
2322+ * name: a mangle that leaves any spelling of it behind is a mutation that does
2323+ * not land, and a mutation that does not land reads exactly like a clean
2324+ * measurement. Both directions are asserted by the caller, which refuses unless
2325+ * the anchored-declaration count drops by exactly one.
2326+ *
2327+ * Word-anchored so a longer identifier sharing the prefix is untouched —
2328+ * renaming `runSelfTest` must not also rewrite `runSelfTestCases`.
2329+ */
2330+ export function withoutAnchor(source, name) {
2331+ const replacement = name.replace(/[Ss]elf[_]?[Tt]est/, 'Probe');
2332+ if (/[Ss]elf[_]?[Tt]est/.test(replacement)) return null;
2333+ if (new RegExp(`\\b${replacement}\\b`).test(source)) return null;
2334+ return source.replace(new RegExp(`\\b${name}\\b`, 'g'), replacement);
2335+ }
2336+
21462337/**
21472338 * The IANA top-level media types. A closed registry, not a heuristic: these ten
21482339 * are the whole of it, so a two-segment literal headed by one of them is a MIME
@@ -9895,6 +10086,85 @@ function selfTest() {
989510086 !transitiveHints.includes('packages/branch/fixture.ts'),
989610087 );
989710088
10089+ // ── The anchor fires on NAMES, so it also fires on production code ───────
10090+ //
10091+ // Live, over the tracked tree, against `COMPOUND_ANCHOR_LEDGER`. The census
10092+ // and the argument for measuring rather than narrowing are at that table;
10093+ // what runs here is the half that can go red.
10094+ //
10095+ // Read the three assertions as one instrument. The first says the population
10096+ // has not moved under the table. The second says masking the accidental half
10097+ // still costs no hint — the claim the ledger makes in prose, re-measured on
10098+ // every run, so the day someone writes a path literal into `maskSelfTests`,
10099+ // `carriesSelfTest` or any other accidental row, THIS goes red and names the
10100+ // hint instead of dropping it in silence. The third is the control that makes
10101+ // the second mean anything: a counterfactual that silently failed to rename
10102+ // would report "no hint moves" for every row, which is indistinguishable from
10103+ // a pass, so at least one GENUINE row must be seen to move a hint.
10104+ {
10105+ const census = new Map();
10106+ for (const rel of trackedFiles()) {
10107+ if (!ANCHOR_CENSUS_EXTENSIONS.test(rel)) continue;
10108+ let text;
10109+ try {
10110+ text = readFileSync(join(ROOT, rel), 'utf8');
10111+ } catch {
10112+ continue;
10113+ }
10114+ if (!/[Ss]elf[_]?[Tt]est/.test(text)) continue;
10115+ for (const decl of compoundAnchorDecls(text)) census.set(`${rel}::${decl.name}`, rel);
10116+ }
10117+ const unlisted = [...census.keys()].filter((k) => !COMPOUND_ANCHOR_KEYS.has(k)).sort();
10118+ const stale = [...COMPOUND_ANCHOR_KEYS.keys()].filter((k) => !census.has(k)).sort();
10119+ t(
10120+ `every compound self-test NAME the anchor matches is classified in COMPOUND_ANCHOR_LEDGER` +
10121+ (unlisted.length ? ` — unlisted: ${unlisted.join(', ')}` : '') +
10122+ (stale.length ? ` — listed but gone: ${stale.join(', ')}` : ''),
10123+ unlisted.length === 0 && stale.length === 0,
10124+ );
10125+
10126+ const costly = [];
10127+ const movers = [];
10128+ for (const [key, accidental] of COMPOUND_ANCHOR_KEYS) {
10129+ const rel = census.get(key);
10130+ if (!rel) continue;
10131+ const name = key.slice(rel.length + 2);
10132+ const src = readFileSync(join(ROOT, rel), 'utf8');
10133+ const alt = withoutAnchor(src, name);
10134+ if (alt === null || compoundAnchorDecls(alt).length !== compoundAnchorDecls(src).length - 1) {
10135+ costly.push(`${key} (the counterfactual rename did not land — this row was NOT measured)`);
10136+ continue;
10137+ }
10138+ const before = extractWatchHints(src, rel);
10139+ const after = extractWatchHints(alt, rel);
10140+ const dropped = after.filter((h) => !before.includes(h));
10141+ if (dropped.length === 0) continue;
10142+ if (accidental) costly.push(`${key} now hides ${JSON.stringify(dropped)}`);
10143+ else movers.push(key);
10144+ }
10145+ t(
10146+ 'masking an ACCIDENTAL name match still costs this tree no watch hint' +
10147+ (costly.length ? ` — ${costly.join('; ')}` : ''),
10148+ costly.length === 0,
10149+ );
10150+ t(
10151+ 'control: at least one GENUINE self-test battery is seen to lose a fixture hint, so the ' +
10152+ 'measurement above is an instrument and not a broken rename reporting zero everywhere',
10153+ movers.length > 0,
10154+ );
10155+ }
10156+
10157+ // The card's own specimen, pinned by identity: this module's masker and the
10158+ // helper predicate beside it are both named into the anchor's population, so a
10159+ // rename that "fixes" either one has to move the ledger row rather than the
10160+ // problem.
10161+ {
10162+ const selfCensus = compoundAnchorDecls(readFileSync(join(ROOT, 'scripts/pm/dispatch-gates.mjs'), 'utf8'));
10163+ const names = selfCensus.map((d) => d.name);
10164+ t("this module's own masker is in the anchor's population", names.includes('maskSelfTests'));
10165+ t('…and so is the reachability helper beside it', names.includes('selfTestOnlyCallables'));
10166+ }
10167+
989810168 // A population DECLARED for this very scanner is referenced by no executing
989910169 // code — being unreferenced is what such a declaration IS. Extending the mask
990010170 // to value declarations was implemented and REFUSED on this evidence: over
0 commit comments