From 8036161946cc3ed007ccfe9ade82f974cd248024 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 07:00:08 +0000 Subject: [PATCH 1/3] fix(pm): read a package-root-anchored literal against the root its writer binds extractWatchHints resolves a path literal against exactly two anchors: the repo root, and the writer's own directory when the author spelled a ./ or ../ prefix. A gate that lives inside a package binds a third at module top -- join(dirname(fileURLToPath(import.meta.url)), '..') -- and every literal it writes against that binding was read from the REPO root, where nothing of the sort exists. check:generated's five artifact-ledger literals are the confirmed instance: all five alive under packages/spec/ and all five printed by the residue as dead leads, under a reason text asserting they were never repo paths. Measured first, as the card required. Over the 201 discovered families' 195 scannable gate scripts, 8 bind a package root this way, 2 of them carry a hint dead at the repo root, 6 such hints exist, 5 re-anchor onto something the tree has and 1 is refused because it reaches nothing. Price: 142089 -> 142139 watch-hint (gate, file) pairs (+50, 0 lost, 0 re-attributed), one family's hint set moves, and 5 of 5 admitted hints are true leads verified at their declaration site. The admitted class is exactly the one whose residue sentence was false: a hint is re-anchored only when no tracked path begins with even its first segment. The layout-moved class is left to triage, admission is untouched, and a re-anchoring that reaches nothing is dropped, so the rule cannot invent a path. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV --- scripts/pm/dispatch-gates.mjs | 338 +++++++++++++++++++++++++++++++++- 1 file changed, 334 insertions(+), 4 deletions(-) diff --git a/scripts/pm/dispatch-gates.mjs b/scripts/pm/dispatch-gates.mjs index 5a1df1b5af..77f0fd3dc7 100644 --- a/scripts/pm/dispatch-gates.mjs +++ b/scripts/pm/dispatch-gates.mjs @@ -2664,6 +2664,194 @@ export function moduleRelativeDirectoryHint(literal, scriptPath, tree, { root = return resolved; } +/** A binding worth resolving is seeded from the running module's own location. */ +const MODULE_ANCHOR_SEED = /import\.meta\.(?:url|dirname|filename)/; + +/** + * ── The THIRD anchor: the PACKAGE ROOT a gate binds from its own module URL + * (#14208) ────────────────────────────────────────────────────────────────── + * + * The repo root is one anchor and the writer's own directory + * (`resolveModuleRelativeHint`) is the second. A gate that lives INSIDE a + * package writes against a third, bound once at module top and used everywhere + * after: + * + * const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); + * const PKG_DIR = resolve(fileURLToPath(new URL('.', import.meta.url)), '..'); + * + * This function answers WHERE that binding points, and answers nothing else. + * Whether any literal should be read against it is the caller's question + * (`packageRootAnchoredHint`), which is what keeps this one a fact of the tree + * rather than a reading of intent. + * + * ## Why it routes through `resolvePathExpression` and builds no walker + * + * `anchoredReadTargets` already resolves exactly these expressions, from the + * same three-part context, for the read follow. A second regex over + * `fileURLToPath` spellings would be a copy of that resolver, and a copy that + * drifts is the defect this file exists to refuse — so the ctx is built the + * way that scan builds it and the resolution is the resolver's. + * + * ## Narrowings, and why each one is structural + * + * SEEDED FROM `import.meta` ONLY. A binding computed from the repo root + * (`join(ROOT, 'packages/spec')`) is already spelled from the root and needs no + * anchor; admitting it would make every join base in the file a candidate + * anchor, which is a widening nothing here measured. + * + * A TRACKED DIRECTORY, read off `tree.prefixes`/`tree.files` — the same two + * sets `moduleRelativeDirectoryHint` reads, so this cannot disagree with what + * the reachability half of the tool believes the tree holds. + * + * THAT CARRIES A TRACKED `package.json`. This is the narrowing that makes the + * anchor a PACKAGE root rather than "any directory above the writer": the + * manifest is the tree's own declaration that this directory is a root things + * are addressed from, and it is the same declaration the manifest follow + * (`packageManifestTargets`) reads one edge over. Without it the rule would + * re-anchor at every intermediate directory a script happens to climb to. + * + * NEVER THE REPO ROOT: a binding that resolves to zero segments is the anchor + * the extractor already applies, so admitting it would be a no-op wearing a + * rule's clothes. `dispatch-gates.mjs`' own `ROOT` is that shape. + * + * The FIRST such binding in source order wins. Measured on this tree the + * question does not arise — every script in the population binds exactly one — + * and a file that bound two package roots would be describing two packages, + * which is a card to file rather than a guess to make here. + */ +export function packageRootBinding(scriptPath, moduleBody, tree) { + if (!scriptPath || !tree) return null; + const ctx = { + fileSegs: scriptPath.split('/'), + names: nameInitialisers(moduleBody), + returns: singleReturnExpressions(moduleBody), + params: singleCallSiteParameters(moduleBody), + seen: new Set(), + }; + for (const inits of ctx.names.values()) { + for (const init of inits) { + if (!MODULE_ANCHOR_SEED.test(init)) continue; + ctx.seen.clear(); + const at = resolvePathExpression(init, ctx); + if (at.kind !== 'in-tree' || at.segs.length === 0) continue; + const dir = at.segs.join('/'); + if (!tree.prefixes.has(dir) || tree.files.has(dir)) continue; + if (!tree.files.has(`${dir}/package.json`)) continue; + return dir; + } + } + return null; +} + +/** + * One hint, re-read against the package root its writer binds — or `null` when + * that reading is not available or does not reach the tree (#14208). + * + * ## The defect, and the one class it is allowed to touch + * + * `extractWatchHints` takes a literal with no `./` prefix as spelled FROM THE + * REPO ROOT. For a gate inside a package that is the wrong base and the tree + * says so out loud: `check:generated` declares `api-surface/`, + * `export-origins/`, `declaration-map/`, `liveness/state-counts.md` and + * `src/meta-spelling/meta-url-data.generated.ts`, all five alive under + * `packages/spec/` and all five printed by the residue as dead leads. + * + * The admitted class is exactly the one whose residue sentence was FALSE, and + * that coupling is deliberate rather than convenient: a hint is re-anchored + * only when `deepestTrackedPrefix` is empty — no tracked path begins with even + * its first segment — which is the branch `unreachableReason` renders as "never + * was a repo path". A hint that stops at a SHORTER prefix is the "layout moved" + * class and is left alone: its first segment is a real repo directory, so the + * root is the base the author most plausibly meant and re-anchoring it would + * replace a triage lead with a fabricated one. + * + * ## Why this is not the widening the two priced refusals refuse + * + * `moduleRelativeDirectoryHint`'s docblock prices a naive widening of the + * RESOLVED form at +53 hints of which 1 was a true lead, and `hintCovers`' + * bare-top-level-word admission at +139084 pairs. Neither is relaxed here, and + * neither can be reached from here: + * + * - ADMISSION is untouched. A literal that is not already a hint — a bare + * single-segment word, a sibling specifier, a package name — never arrives + * at this function. What is re-anchored is a literal the scan already + * admitted and the tree already refused; + * - the candidate must REACH THE TREE, through `hintReachesTree`, which is + * `hintCovers` applied to the corpus. A re-anchoring that names nothing is + * dropped and the hint keeps the spelling it had, so the rule cannot invent + * a path — measured live on `check:browser-reachable-entries`' `'zod'`, + * which stays dead because `packages/spec/zod` is not there; + * - it REPLACES rather than adds. The re-anchored form is the same claim + * about the same literal under the base its writer actually bound, so the + * family's declared-literal count does not move and no second hint is + * manufactured beside a dead one. + * + * ## The POPULATION, swept before the rule was written (ad54eb342) + * + * The card that filed this asked for the sweep first and the repair only if it + * survived. Over the 201 discovered families' 195 scannable gate scripts: + * + * scripts binding a package root this way 8 (all `packages/spec`) + * ...of which carry a hint dead at the repo root 2 + * dead-at-root hints in that population 6 + * ...that re-anchor onto something the tree HAS 5 + * ...refused because the re-anchoring reaches nothing 1 (`'zod'`, a package + * specifier, in + * check:browser-reachable-entries) + * + * ⚠️ The card described the class as literals passed to `resolve(PKG_DIR, …)`. + * That shape is real — 6 such literals across 6 of the 8 scripts — but it is + * NOT where the confirmed instance comes from, and a rule restricted to it + * would have repaired none of the three dead leads the card names: all five of + * check:generated's are `artifact:` values in its GATED ledger, describing what + * each gate verifies, resolved by nothing. The syntactic restriction was + * measured and dropped for that reason; the reachability test above is what + * keeps the wider reading honest. + * + * ## Blast radius, measured before the change and after it (ad54eb342) + * + * A rule that moves rows moves them for EVERY family, so the price is the + * deliverable and not a footnote. Over 201 discovered families and 7900 tracked + * files: + * + * watch-hint (gate, file) pairs 142089 -> 142139 (+50, and ZERO lost) + * families whose hint set moves 1 — check:generated, and no other + * pairs RE-ATTRIBUTED (same pair, new via) 0 — every replaced + * spelling was DEAD, so it covered nothing + * that could be re-attributed + * (check, hint) live / inert 1108/152 -> 1113/147 + * distinct hints in the fleet 725 -> 722, because three of the five + * re-anchored forms are already spelled from + * the root by other gates + * families carrying dead literals 46 -> 45 + * dead literals 152 -> 147 + * ...of them in the "never was a repo path" class 128 -> 123 + * + * PROVENANCE, which is the criterion this file prices and never volume: 5 of 5 + * admitted hints are TRUE leads and 50 of 50 pairs with them, each verified at + * its declaration site — they are `check:generated`'s own artifact ledger, and + * the gate's whole job is to verify those artifacts against their sources: + * + * packages/spec/api-surface 17 files packages/spec/export-origins 17 + * packages/spec/declaration-map 14 .../meta-url-data.generated.ts 1 + * packages/spec/liveness/state-counts.md 1 + * + * 0 fabricated. For scale, `hintCovers`' docblock prices the bare-top-level-word + * admission it refuses at +139084 pairs on this corpus; this is 0.04% of it. + * + * @param {string} hint a hint `extractWatchHints` has already admitted + * @param {string} base the package root, from `packageRootBinding` + * @param {{files: Set, prefixes: Set}} tree + * @param {string[]} files the same corpus, as the array `hintReachesTree` walks + * @returns {string|null} the re-anchored hint, or null + */ +export function packageRootAnchoredHint(hint, base, tree, files) { + if (!base || !tree || !files) return null; + if (deepestTrackedPrefix(hint, tree.prefixes) !== '') return null; + const candidate = `${base}/${hint}`; + return hintReachesTree(candidate, files) ? candidate : null; +} + /** * Scan a check script's MODULE BODY for the path-ish string literals it * operates on. A hint is a quoted string that contains a `/` (or names a @@ -2817,7 +3005,52 @@ export function extractWatchHints(scriptSource, scriptPath = null, { tree = null } hints.add(trimmed); } - return [...hints]; + return anchorAtPackageRoot(hints, scriptPath, moduleBody, tree); +} + +/** + * The THIRD anchor, applied to a finished hint set (#14208). + * + * It runs LAST and over the assembled set rather than inside the admission + * loop, because it is a re-reading of hints and not a second admission rule: + * everything it can touch is already a hint, so the loop above keeps being the + * one place that decides what a hint IS. + * + * The order of the returned list is preserved to the byte for every hint it + * does not touch, and a re-anchored hint keeps its ORIGINAL POSITION rather + * than moving to the end. `discoverFamilies` puts a gate's own hints at the + * front so an already-matched path keeps the exact key and via label it had, + * and `residueNames` prints the first three — a rule that reshuffled the list + * would move rows in output that nothing in this change is entitled to move. + * + * The two cheap refusals come first, so the corpus is materialised and the + * source is parsed for bindings ONLY for a script that has both a dead-at-root + * hint and something to re-anchor it to. Over the live fleet that is 2 scripts + * of 201 families' worth of files. + */ +function anchorAtPackageRoot(hints, scriptPath, moduleBody, tree) { + const spelled = [...hints]; + if (!scriptPath || !tree) return spelled; + const orphans = spelled.filter((h) => deepestTrackedPrefix(h, tree.prefixes) === ''); + if (orphans.length === 0) return spelled; + const base = packageRootBinding(scriptPath, moduleBody, tree); + if (!base) return spelled; + const files = [...tree.files]; + const anchored = new Map(); + for (const h of orphans) { + const at = packageRootAnchoredHint(h, base, tree, files); + if (at) anchored.set(h, at); + } + if (anchored.size === 0) return spelled; + const out = []; + const seen = new Set(); + for (const h of spelled) { + const v = anchored.get(h) ?? h; + if (seen.has(v)) continue; + seen.add(v); + out.push(v); + } + return out; } /** @@ -10752,15 +10985,112 @@ function selfTest() { for (const h of entry.hints ?? []) if (!before.has(h)) dirGained.push(`${check} +${h}`); for (const h of before) if (!(entry.hints ?? []).includes(h)) dirLost.push(`${check} -${h}`); } + // ⚠️ WITH-tree against WITHOUT-tree measures every TREE-COUPLED rule at once, + // and there are TWO of them now: this one, and the package-root anchor + // (#14208), which refuses without a tree for the same reason. The rows are + // PARTITIONED rather than filtered — a filter would let a third rule's rows + // disappear from the one pin whose job is to notice them. + const dirRuleGained = dirGained.filter((r) => r.endsWith('+packages/spec/src')); + const parseRow = (row, sign) => { + const at = row.indexOf(` ${sign}`); + return { check: row.slice(0, at), hint: row.slice(at + 2) }; + }; + const anchorGained = dirGained.filter((r) => !dirRuleGained.includes(r)).map((r) => parseRow(r, '+')); + const anchorLost = dirLost.map((r) => parseRow(r, '-')); t( 'the rule changes exactly the two gates that walk packages/spec/src, and adds exactly the directory they walk', - dirGained.join(' · ') === 'check:docs +packages/spec/src · check:skill-refs +packages/spec/src', + dirRuleGained.join(' · ') === 'check:docs +packages/spec/src · check:skill-refs +packages/spec/src', dirGained.join(' · '), ); + // It takes nothing away, and every remaining tree-coupled row belongs to the + // anchor — asserted as a PAIRING rather than as a list of names, so the pin + // survives packages/spec's artifact ledger moving and still reds the day the + // anchor starts ADDING a hint beside a dead one instead of replacing it. t( 'and it takes NOTHING away — a widening that also subtracted would read exactly like this one', - dirLost.length === 0, - dirLost.join(' · '), + anchorLost.length === anchorGained.length && + anchorGained.every((g) => + anchorLost.some( + (l) => + l.check === g.check && + g.hint.endsWith(`/${l.hint}`) && + hintTree.files.has(`${g.hint.slice(0, g.hint.length - l.hint.length - 1)}/package.json`), + ), + ), + `${anchorGained.map((g) => `${g.check} +${g.hint}`).join(' · ')} || ${dirLost.join(' · ')}`, + ); + + // ── The THIRD anchor: a literal bound to the writer's PACKAGE ROOT (#14208) + // + // `packageRootAnchoredHint` carries the population sweep, the pricing against + // the two refusals it must not become, and the provenance split. Pinned here + // as the four things a reader has to be able to break: that it FIRES, that it + // is REFUSED without a tree, that it cannot invent a path, and that it leaves + // the layout-moved class alone. + const pkgAnchorSrc = + "const PKG_DIR = resolve(fileURLToPath(new URL('.', import.meta.url)), '..');\n" + + "const GATED = [{ check: 'check:api-surface', artifact: 'api-surface/' }];\n"; + t( + 'a literal dead at the repo root is re-read against the package root its writer binds', + extractWatchHints(pkgAnchorSrc, 'packages/spec/scripts/check-x.ts', { tree: hintTree }).join() === + 'packages/spec/api-surface', + extractWatchHints(pkgAnchorSrc, 'packages/spec/scripts/check-x.ts', { tree: hintTree }).join(), + ); + t( + '…and without a tree the same call keeps the root spelling — a missing lead, never a fabricated one', + extractWatchHints(pkgAnchorSrc, 'packages/spec/scripts/check-x.ts').join() === 'api-surface', + ); + t( + 'a re-anchoring that reaches nothing is REFUSED, so the rule cannot invent a path', + extractWatchHints( + `${pkgAnchorSrc}const P = 'no-such-dir/no-such-file.ts';`, + 'packages/spec/scripts/check-x.ts', + { tree: hintTree }, + ).includes('no-such-dir/no-such-file.ts'), + ); + // The layout-moved class is NOT this rule's: its first segment is a real repo + // directory, so the root is the base its author most plausibly meant, and + // re-anchoring it would replace a triage lead with a fabricated one. + t( + 'a hint the tree stops short of keeps the ROOT spelling — the layout-moved class is left to triage', + extractWatchHints( + `${pkgAnchorSrc}const P = 'scripts/gone-from-here.mjs';`, + 'packages/spec/scripts/check-x.ts', + { tree: hintTree }, + ).includes('scripts/gone-from-here.mjs'), + ); + t( + 'a module-anchored binding that carries no package.json is no anchor', + packageRootBinding( + 'scripts/pm/x.mjs', + "const D = resolve(fileURLToPath(new URL('.', import.meta.url)), '..');", + hintTree, + ) === null, + ); + t( + '…and the repo root is never the anchor — that base is the one already applied', + packageRootBinding('scripts/pm/x.mjs', "const R = new URL('../..', import.meta.url).pathname;", hintTree) === null, + ); + // LIVE, on this tree: the specimen the card was filed for. ARRIVAL, not + // departure — the family is named, and the re-anchored hint is shown to reach + // a real file that NO other hint of that family reaches, so the pair is this + // rule's rather than a coincidence of some other hint already covering it. + t( + 'the live spec gate binds the package root the card names', + packageRootBinding( + 'packages/spec/scripts/check-generated.ts', + readFileSync(join(ROOT, 'packages/spec/scripts/check-generated.ts'), 'utf8'), + hintTree, + ) === 'packages/spec', + ); + const generatedEntry = dirLanding.get('check:generated'); + const apiSurfaceFile = trackedFiles().find((f) => f.startsWith('packages/spec/api-surface/')); + t( + 'check:generated carries its artifact ledger re-anchored, and that hint is what reaches it', + Boolean(apiSurfaceFile) && + (generatedEntry?.hints ?? []).filter((h) => hintCovers(h, apiSurfaceFile)).join() === + 'packages/spec/api-surface', + (generatedEntry?.hints ?? []).filter((h) => hintCovers(h, apiSurfaceFile)).join(), ); // REFUSAL 1 — module-relative ONLY. `resolve` treats a bare word exactly like From 7165c6ebc59fe4a9d3ea9468511ea0fed19f05ef Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 07:09:03 +0000 Subject: [PATCH 2/3] fix(pm): stop the residue claiming a literal never was a repo path unreachableReason's by-construction branch ended "never was a repo path" -- a claim about EVERY base, made from evidence about ONE. check:generated's api-surface, export-origins and src/meta-spelling/... all printed under it while sitting on disk under packages/spec/: a dead-lead label on a live path, telling the reader there is nothing to chase about three files they could open. The sentence now names what the sweep actually established (no tracked path under the literal's first segment) and the two ways that happens, without asserting the literal never existed anywhere. The three docblocks that stated the same universal as current fact are corrected with it; the two that quote it as history keep the quote and say it is retired, so a grep for it still lands on the incident. The pins move with the text rather than staying on the retired phrase: pinned to a string nothing renders, the dist-freshness case would pass on any tree at all -- a green over the exact regression it exists to catch -- so it now asserts against the sentence that branch prints today, and the sweep case asserts both halves. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV --- scripts/pm/dispatch-gates.mjs | 75 ++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/scripts/pm/dispatch-gates.mjs b/scripts/pm/dispatch-gates.mjs index 77f0fd3dc7..6740dbbcfd 100644 --- a/scripts/pm/dispatch-gates.mjs +++ b/scripts/pm/dispatch-gates.mjs @@ -2825,7 +2825,7 @@ export function packageRootBinding(scriptPath, moduleBody, tree) { * the root by other gates * families carrying dead literals 46 -> 45 * dead literals 152 -> 147 - * ...of them in the "never was a repo path" class 128 -> 123 + * ...of them in the empty-`deepest` class 128 -> 123 * * PROVENANCE, which is the criterion this file prices and never volume: 5 of 5 * admitted hints are TRUE leads and 50 of 50 pairs with them, each verified at @@ -2895,7 +2895,9 @@ export function packageRootAnchoredHint(hint, base, tree, files) { * the hint `lib/dist-freshness`, a string that never was a repo path while the * file it names exists. That is a hint set stating something FALSE about the * source, and the residue said so out loud: `unreachableReason` printed "never - * was a repo path" about targets that are on disk. + * was a repo path" about targets that are on disk. ⚠️ That sentence is retired + * — `unreachableReason` no longer claims a base it did not test (#14208) — so + * it is quoted here as history and will not be found in today's output. * * So the prefix is resolved against `scriptPath`'s own directory instead * (`resolveModuleRelativeHint`). For a literal already spelled from the root by @@ -3899,7 +3901,8 @@ export function judgedAsPattern(hint) { * With the form corrected the three branches mean what they say for BOTH * comparison modes: `deepest === form` is "the population's root is right * there", a strictly shorter `deepest` is a genuine move under a surviving - * parent, and an empty one is a literal that never was a repo path. + * parent, and an empty one is a literal no tracked path begins with — a + * specifier, or a path anchored at a base this scan did not resolve (#14208). */ export function comparedForm(hint) { if (!judgedAsPattern(hint)) return collapseHint(hint); @@ -5839,9 +5842,11 @@ export function watchHintTree(files = trackedFiles()) { * tree can actually answer (#9883 H2). It separates three populations that * would otherwise print identically as "matched nothing": * - * '' the literal was never a repo path — a MIME type, a - * remote ref, a package or repo specifier that survived - * the extractor. Nothing moved; it was never live; + * '' no tracked path begins with the literal — a MIME type, + * a remote ref, a package or repo specifier that survived + * the extractor, or a path anchored at a base this scan + * did not resolve (#14208). Nothing moved under it; as + * spelled it was never live; * a shorter prefix the tree HAS the parent and stops there — the layout * moved under a gate that still names the old spelling. * This is the class that is usually a real miss; @@ -5928,7 +5933,9 @@ export const MODULE_SPECIFIER_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js' * * These nine families used to print "never was a repo path" — false, but filed * BY CONSTRUCTION, i.e. under the heading that tells a reader there is nothing - * to chase. Resolving the literal against its writing script (the producer-side + * to chase. (That sentence is retired; the branch now names what the sweep + * established without claiming a base it did not test — #14208.) + * Resolving the literal against its writing script (the producer-side * repair in `resolveModuleRelativeHint`) was right and stays; what it also did * was move the falsehood from an inert bucket into the actionable one: * @@ -6207,10 +6214,11 @@ export function unreachableFamilies(entries, files, sweep = null) { * The three cases `deepestTrackedPrefix` distinguishes split two-to-one on the * question a reader actually has, which is "is this a miss I should chase?": * - * by construction the literal was never a repo path (no tracked path under + * by construction no tracked path begins with the literal (nothing under * even its first segment — a package specifier, a cross-repo - * slug), or the tree HAS the population and the covering - * rule refuses the literal as too generic. Neither is a + * slug, or a base this scan did not resolve), or the tree + * HAS the population and the covering rule refuses the + * literal as too generic. Neither is a * defect in the gate or in the tree, and NO change to either * makes the derivation reach it. This is the class that is * merely a standing fact. @@ -6289,7 +6297,17 @@ export function unreachableReason(dead, cap = 3) { if (target) { return `'${hint}' — the tree HAS ${target}; the literal is that file's extensionless module spelling, which no whole-segment comparison reaches`; } - if (!deepest) return `'${hint}' — no tracked path under its first segment; never was a repo path`; + // ⚠️ This sentence used to end "never was a repo path" — a claim about + // EVERY base, made from evidence about ONE (#14208). check:generated's + // `api-surface`, `export-origins` and `src/meta-spelling/…` all printed + // under it while sitting on disk under `packages/spec/`, which is a + // dead-lead label on a live path: the reader is told there is nothing to + // chase, about three files they could open. What the sweep actually + // established is the first clause, and the second now names the two ways + // that happens instead of asserting the literal never existed. + if (!deepest) { + return `'${hint}' — no tracked path under its first segment; a specifier, or a path anchored at a base this scan did not resolve`; + } // Every branch below reasons about the form the COMPARISON used, so the // pattern-judged shapes get their own sentence instead of borrowing the // collapse's. Borrowing it is what made the residue assert a directory @@ -11176,8 +11194,13 @@ function selfTest() { !hintCovers('src/data', 'packages/spec/src/data/field.zod.ts'), ); // The residue's claim stops being false: a resolved literal HAS a tracked - // prefix, so `unreachableReason` no longer files it as "never was a repo path" - // about a file that exists. + // prefix, so `unreachableReason` no longer files it under the by-construction + // sentence about a file that exists. + // + // ⚠️ The assertion is written against the sentence that branch prints TODAY, + // not against a phrase it used to print. Pinned to a retired phrase this pin + // would pass on any tree at all — the branch cannot emit a string nothing + // renders — which is a green over the exact regression it exists to catch. // // ⚠️ This case asserted ONLY `Boolean(deepest)` — that the hint had LEFT the // "never" branch — and leaving that branch is precisely what puts a hint into @@ -11197,8 +11220,9 @@ function selfTest() { }, ]; t( - 'a resolved dead hint has a tracked prefix, so the residue stops calling it "never a repo path"', - Boolean(distFreshnessDead[0].deepest) && !/never was a repo path/.test(unreachableReason(distFreshnessDead)), + 'a resolved dead hint has a tracked prefix, so the residue stops filing it by construction', + Boolean(distFreshnessDead[0].deepest) && + !/no tracked path under its first segment/.test(unreachableReason(distFreshnessDead)), ); t( '...and it does NOT arrive at "the layout moved" instead — the tree HAS the file, named', @@ -14698,7 +14722,11 @@ function selfTest() { t('...and leaves the real hint beside it unmarked', plantedNames.startsWith('packages/spec/src,') && !plantedNames.includes('packages/spec/src ✗')); const plantedNote = deadNamesNote(plantedRow); t('the note counts the dead against the declared total', plantedNote.includes('1 of 2 declared literal(s)')); - t("...and names WHY in the unreachable listing's own voice", /never was a repo path/.test(plantedNote)); + t( + "...and names WHY in the unreachable listing's own voice", + /a base this scan did not resolve/.test(plantedNote), + plantedNote, + ); t( 'the note is capped like the listing it sits under, never an inventory', /…$/.test(deadNamesNote({ declared: 9, dead: [1, 2, 3, 4].map((n) => ({ hint: `no/such/p-${n}`, deepest: '' })) })), @@ -14708,7 +14736,18 @@ function selfTest() { 'a family that declares NO population is NOT unreachable — that is the undetermined verdict, a different fact', !sweptNames.includes('check:declares-nothing'), ); - t('the sweep names WHY: a literal no tracked path begins with was never a repo path', /never was a repo path/.test(reasonOf('check:never-was'))); + // ⚠️ The claim asserted here is the FIRST clause — no tracked path begins + // with the literal. The second used to read "never was a repo path", which + // is a claim about every base made from evidence about one, and it printed + // over three live files (#14208). Both halves are pinned so a regression to + // the universal reds rather than passing on the surviving prefix. + t( + 'the sweep names WHY: no tracked path begins with the literal, without claiming it never was one', + /no tracked path under its first segment/.test(reasonOf('check:never-was')) && + /a base this scan did not resolve/.test(reasonOf('check:never-was')) && + !/never was a repo path/.test(reasonOf('check:never-was')), + reasonOf('check:never-was'), + ); t('the sweep names WHY: the tree stops at a shorter prefix, so the layout moved under it', /stops at packages\/spec\/src/.test(reasonOf('check:moved'))); // The third cause is the one a bare "matched nothing" would send a reader // hunting a directory that is sitting in front of them: the population is @@ -14936,7 +14975,7 @@ function selfTest() { t('and the by-construction families under theirs', /unreachable BY CONSTRUCTION/.test(listedText)); t('the real miss sorts BEFORE the standing facts, never buried among them', listedText.indexOf('THE LAYOUT MOVED') < listedText.indexOf('BY CONSTRUCTION')); t('every swept family is named in the listing, runnably', sweep.every((u) => listedText.includes(runnableInvocation(u.entry)))); - t('each entry still carries the reason it could not reach', /never was a repo path/.test(listedText) && /the tree HAS it/.test(listedText)); + t('each entry still carries the reason it could not reach', /a base this scan did not resolve/.test(listedText) && /the tree HAS it/.test(listedText)); // The empty case must not print as a missing section, and the corpus size is // required for the #4690 reason one level down. const emptyListing = unreachableLines([], 4).join('\n'); From 7d75ea9837e283b13dea1aada83e0aef2ce5011f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 07:09:13 +0000 Subject: [PATCH 3/3] docs(pm): the READ edge contributes an identity key, not a population The fourth-edge docblock above packageManifestTargets opened "The three follows above all end at a PROGRAM ... and inherit that program's declared population". The middle one does not: readProgramTargetsInSource fills entry.reads / entry.readOrigin and nothing feeds those into hintsOfModule. The read edge reaches a card only through the LAST key in coveringKey, which is an identity claim about a single FILE and never a population. Re-derived at this PR's base: the read edge is 12 families, 16 (family,target) pairs over 15 distinct targets, and it carries 0 inherited hints -- against 373 for the import edge, 7 for the manifest edge and 1 for the run edge. Reading a program's TEXT is not performing its work, so identity-only is the correct behaviour and the sentence was the defect. No behaviour change. The two numberings inside the file also disagreed: discoverFamilies called the spawn follow the SECOND edge and the manifest follow the THIRD, while the docblock called the manifest one the FOURTH. They count different things -- source-order edges, and population follows -- so each now says which, and the claim that there are three population follows all ending at a program is gone. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV --- scripts/pm/dispatch-gates.mjs | 44 ++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/scripts/pm/dispatch-gates.mjs b/scripts/pm/dispatch-gates.mjs index 6740dbbcfd..9743275c28 100644 --- a/scripts/pm/dispatch-gates.mjs +++ b/scripts/pm/dispatch-gates.mjs @@ -5549,9 +5549,27 @@ export function spawnedProgramTargets(rel, source, isTracked) { * ── The PACKAGE a gate re-derives from: the fourth edge, and the one that is * not a program (#13518) ──────────────────────────────────────────────────── * - * The three follows above all end at a PROGRAM — a module the gate imports, a - * file whose program text it opens, a program it spawns — and inherit that - * program's declared population. This one ends at DATA: a workspace package's + * The three scans above all end at a PROGRAM — a module the gate imports, a + * file whose program text it opens, a program it spawns — but only TWO of them + * inherit that program's declared population. The import and spawn scans feed + * `hintsOfModule` in `discoverFamilies`; the READ scan does not, and nothing + * feeds `entry.reads` into it. It reaches a card through the LAST key in + * `coveringKey`, which is an identity claim about a single FILE (#13000) and + * never a population. Measured on this tree: the read edge is 12 families, 16 + * (family, target) pairs over 15 distinct targets, and it carries 0 inherited + * hints — against 373 for the import edge, 7 for the manifest edge and 1 for + * the run edge (#14289). Reading a program's TEXT is not performing its work, + * and identity-only is the correct behaviour, so the claim was the defect + * rather than the code. + * + * ⚠️ TWO numberings live in this file and they count different things. Here the + * edges are numbered in SOURCE ORDER, all four of them, which makes this one + * the fourth. `discoverFamilies` numbers only the POPULATION FOLLOWS — import, + * spawn, manifest — which makes the same edge its third. Neither is wrong; the + * sentence claiming three population follows all ending at a program was, and + * that is what is repaired above. + * + * This one ends at DATA: a workspace package's * `package.json`, whose `exports` map is the tree's own declaration of what a * package's public entry points ARE. * @@ -5596,8 +5614,9 @@ export function spawnedProgramTargets(rel, source, isTracked) { * `hintsOfManifest` (in `discoverFamilies`) answers with the package's tracked * SOURCE subtree, and it answers only for a manifest that declares an * `exports` map. That test is a fact of the tree, read from the followed file's - * own declaration — the `declaredInheritedPopulation` discipline the other - * three follows take, one file kind over — and never a regex over the gate's + * own declaration — the `declaredInheritedPopulation` discipline the other TWO + * follows take, one file kind over (the read scan takes none of it: it + * inherits no population to narrow, #14289) — and never a regex over the gate's * prose about what it thinks it reads. * * It is also what makes the edge precise rather than merely broad. Measured on @@ -8334,8 +8353,12 @@ export function discoverFamilies({ tree = watchHintTree() } = {}) { if (gateFiles.has(mod) || entry.imports.includes(mod)) continue; entry.imports.push(mod); } - // The SECOND edge to another program, under the same two refusals as the - // first and for the same reasons (#13511). It sits BELOW the self-test + // The SECOND POPULATION FOLLOW, under the same two refusals as the first + // and for the same reasons (#13511). ⚠️ "Second" counts FOLLOWS, not + // edges: the read scan a few lines up is an edge too, and it is the + // second of those in source order, but it inherits no population and so + // is not one of these (#14289 — `packageManifestTargets`' docblock holds + // both numberings side by side). It sits BELOW the self-test // guard deliberately: the narrowing above is invocation-shaped — an // inherited population describes the gate's WORK — and that argument does // not change with the edge it arrives over. One rule, not two. Cost of @@ -8345,8 +8368,11 @@ export function discoverFamilies({ tree = watchHintTree() } = {}) { if (gateFiles.has(ran) || entry.runs.includes(ran)) continue; entry.runs.push(ran); } - // The THIRD edge under the same self-test guard, for the same - // invocation-shaped reason the spawn follow states (#13518). The + // The THIRD AND LAST POPULATION FOLLOW, under the same self-test guard + // and for the same invocation-shaped reason the spawn follow states + // (#13518). Its own docblock calls it the FOURTH EDGE, numbering all four + // scans in source order; here the count is of follows, and there are + // three (#14289). The // gate-file refusal the other two make does not apply and is not spelled: // a `package.json` is never a discovered gate file, so there is nothing // to refuse.