From 7c690ef6f5e8965287ca7fa4982944136fad56c4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 13:34:34 +0000 Subject: [PATCH 1/2] fix(pm): key a package-local gate invocation to its real path (#15342) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `check-self-test-wired` anchored `INVOCATION_RE` on the bare literal `scripts/`, so lint.yml's `node packages/lint/scripts/check-reference- carrier-shape.mjs --self-test` matched as a SUBSTRING and was filed under `scripts/check-reference-carrier-shape.mjs` — a key with no file behind it. Both directions were silent: the real file sits outside the root walk, so it was in no population and never audited, and the phantom key could never be reconciled against a carrier either. Two anchors move, and only these: - `INVOCATION_RE` consumes the directory prefix into the key and gains a left boundary, so a path is keyed WHOLE. A leading `./` is stripped (cut-rc.yml and release.yml import from `"./scripts/…"`, and keying those elsewhere would drop two live invocations); `..` is not a prefix segment, because a path this ROOT cannot resolve is how the phantom was minted. - the population gains a second source: a script CI NAMES that lives outside the root walk, admitted on the root walk's own terms (must exist, must carry the literal in code). Not a second walk — see the comment in main(). Measured on the live corpus: 186 named paths, 0 without a file on disk (before: exactly 1). Carriers 182 -> 183, members 168 -> 169, wired 164 -> 165. Self-test grows two batteries — `left boundary` (6) and `live corpus` (3); 50 cases -> 59, floor 7 -> 9. No existing case is weakened. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --- scripts/check-self-test-wired.mjs | 168 +++++++++++++++++++++++++++++- 1 file changed, 164 insertions(+), 4 deletions(-) diff --git a/scripts/check-self-test-wired.mjs b/scripts/check-self-test-wired.mjs index c7774657c2..0f080ca06a 100644 --- a/scripts/check-self-test-wired.mjs +++ b/scripts/check-self-test-wired.mjs @@ -152,14 +152,50 @@ const SCRIPT_EXT = /\.(mjs|mts|js|sh)$/; const ROOT_DIR_WATCH_HINTS = ['scripts/**/*.mjs', 'scripts/**/*.mts', 'scripts/**/*.sh']; /** - * A `scripts/...` path, optionally followed by `--self-test`. + * A `scripts/...` path, optionally prefixed by the directories it lives under + * and optionally followed by `--self-test`. * * The trailing `(?![\w-])` is the right boundary: without it `--self-test-extra` * reads as an invocation of `--self-test`, which is #10534's defect wearing this * gate's hat. + * + * ## The LEFT boundary, and why the prefix is read rather than cut off (#15342) + * + * This repo has a package-local gate lane, and `lint.yml` really does run one + * of its gates by path: `node packages/lint/scripts/check-reference-carrier- + * shape.mjs --self-test`. The pattern used to open on the bare literal + * `scripts/` with nothing to its left, so it matched that path as a SUBSTRING + * and filed the gate under `scripts/check-reference-carrier-shape.mjs` — a key + * with no file behind it. Both directions of that were silent: the real file + * was never audited (it is not under the root walk, so it is in no population), + * and the phantom key could never be reconciled against a carrier either, so + * neither `auditPopulation` nor `auditLedger` had anything to say. A gate whose + * whole subject is "the self-test CI ships is the self-test CI runs" was + * answering about a different script than the one CI executes. + * + * So the directory prefix is CONSUMED into the key rather than cut away, and + * `(? named.has(s)); const wired = members.filter((s) => selfTested.has(s)); + const packageLocal = [...carriers].filter((s) => !s.startsWith('scripts/')); const scope = ` scope: ${files.length} file(s) under scripts/, ${carriers.size} carrying \`--self-test\` in code ` + - `(comments masked); ${members.length} of those are run by ${workflows.length} workflow(s); ` + + `(comments masked, ${packageLocal.length} of them package-local gate(s) CI names by path); ` + + `${members.length} of those are run by ${workflows.length} workflow(s); ` + `${wired.length} have their self-test run through the flag, ${SELF_TEST_RUN_OTHERWISE.length} through a recorded route.`; if (findings.length > 0) { @@ -496,9 +565,11 @@ function main() { const SELF_TEST_BATTERIES = Object.freeze({ 'comment mask': 7, 'right boundary': 4, + 'left boundary': 6, 'alias resolution': 4, 'population verdict': 4, 'population declaration': 7, + 'live corpus': 3, 'ledger hygiene': 9, 'live ledger': 4, }); @@ -506,7 +577,7 @@ const SELF_TEST_BATTERIES = Object.freeze({ // DELETING an entry silences that battery's floor exactly as effectively as // zeroing it, so the registry's own size is pinned too. Adding a battery raises // this number; removing one is the same ⛔ deliberate edit as lowering a count. -const SELF_TEST_BATTERY_FLOOR = 7; +const SELF_TEST_BATTERY_FLOOR = 9; // 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 @@ -588,6 +659,60 @@ function selfTest() { ); } + // ── Left boundary: a path is keyed WHOLE, never as a tail of one (#15342) ─ + // + // Every case here fails against the pre-#15342 anchor (`/(scripts\/…)/`, no + // left boundary, no prefix), which is what makes them an instrument rather + // than a restatement of the operators. Measured by applying this battery to + // the base tree's pattern in a scratch ablation: cases 1-4 red there, 5-6 + // stay green — 5 and 6 are the controls that prove the widening did not buy + // its new answers by dropping the old ones. + battery('left boundary'); + { + const PKG = 'packages/lint/scripts/check-reference-carrier-shape.mjs'; + const got = collectInvocations(wf(` - run: node ${PKG} --self-test\n`), {}); + ok( + got.named.has(PKG), + 'a gate CI invokes by a PACKAGE-LOCAL path was not keyed to its real path — it is then in no ' + + 'population and its self-test wiring is audited for nobody (#15342)', + ); + ok( + got.selfTested.has(PKG), + 'the package-local path was named but its `--self-test` was not credited to it', + ); + ok( + !got.named.has('scripts/check-reference-carrier-shape.mjs'), + 'the package-local path was ALSO filed under a root path with no file behind it — the phantom key ' + + 'a substring match mints, which no carrier can ever reconcile (#15342)', + ); + } + { + // A path that is a tail of a longer one must not be minted on its own. + const got = collectInvocations(wf(' - run: node vendor/tools/scripts/g.mjs --self-test\n'), {}); + ok( + !got.named.has('scripts/g.mjs'), + 'a `scripts/…` SUBSTRING of a longer path was minted as a key in its own right', + ); + } + { + // Control 1 — the plain root spelling is unchanged. + const got = collectInvocations(wf(' - run: node scripts/g.mjs --self-test\n'), {}); + ok(got.selfTested.has('scripts/g.mjs'), 'the ordinary root spelling stopped being keyed to itself'); + } + { + // Control 2 — `./scripts/…` still normalises onto the walk's key. Live + // spelling: `cut-rc.yml` and `release.yml` import from `"./scripts/…"`, + // and keying those anywhere else silently drops two real invocations. + const got = collectInvocations( + wf(' - run: node --input-type=module -e \'import { X } from "./scripts/g.mjs";\'\n'), + {}, + ); + ok( + got.named.has('scripts/g.mjs'), + 'a leading `./` was kept in the key, so the same file under two spellings stopped comparing equal', + ); + } + // ── Aliases: reached only when a workflow actually names them ──────────── battery('alias resolution'); { @@ -702,6 +827,41 @@ function selfTest() { ); } + // ── The live corpus: the anchor is measured on specimens, not on fixtures ─ + // + // A widening measured on zero specimens is not measured. The fixtures above + // prove the pattern; these three read the tree CI actually runs, so the day + // the package-local lane moves, this reds here instead of going quiet. + battery('live corpus'); + { + let corpus = null; + try { + const dir = join(ROOT, WORKFLOW_DIR); + const workflows = readdirSync(dir) + .filter((f) => /\.ya?ml$/.test(f)) + .sort() + .map((name) => ({ name, text: readFileSync(join(dir, name), 'utf8') })); + const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).scripts ?? {}; + corpus = collectInvocations(workflows, pkg); + } catch { + corpus = null; + } + ok(corpus !== null && corpus.named.size > 0, 'the live workflow corpus could not be read — the pins below would prove nothing (#4690)'); + const keys = corpus === null ? [] : [...corpus.named.keys()]; + ok( + keys.length > 0 && keys.every((p) => existsSync(join(ROOT, p))), + 'a path this gate keys an invocation to has NO file behind it. Every audit downstream then runs ' + + 'against a script that does not exist, and passes for the wrong reason, in both directions (#15342)', + ); + // Named BY NAME on purpose: it is this tree's only package-local gate + // invocation, so it is the whole specimen set for the widening above. + ok( + keys.includes('packages/lint/scripts/check-reference-carrier-shape.mjs'), + "lint.yml's package-local gate is not in the live population. Either the lane moved — re-point this " + + 'pin at the new specimen — or the anchor regressed to a root-only one and the widening is untested', + ); + } + // ── Ledger hygiene: every row must still be true, and still be needed ──── battery('ledger hygiene'); { From 2c22f5716c28dcf8429708341c487d2d41f01b48 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 14:08:49 +0000 Subject: [PATCH 2/2] fix(pm): discover a package-local gate invocation and key it to its real path (#15342) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `lint.yml` runs one gate by a package-local path — `node packages/lint/ scripts/check-reference-carrier-shape.mjs --self-test`, plus the bare production invocation on the next line. Both tools that read that corpus anchored on the literal `scripts/`, and failed in opposite ways. `dispatch-gates` required `node ` to be followed IMMEDIATELY by `scripts/`, so neither `DIRECT_CHECK_INVOCATION` nor `SELF_TEST_INVOCATION` matched the step at all: the gate was in NO family, and `--residue` could not report it either — it is absent from the universe the matched / silent / undetermined buckets partition. Measured on `65846bc46` for a card editing that gate's own file: `--commands` named it zero times, all 27 `--residue` mentions were the dev's own changed path echoed back, and the step surfaced only in the always-runs STEP tail, as one of the 33 CI steps the derivation names no family for. Both patterns gain the same directory prefix, spelled identically. A segment must start with an alphanumeric or `_`, so `..` is refused — a path this ROOT cannot resolve is how a phantom identity key gets minted. A `scripts/` segment is still required, so no new species is admitted (`node packages/cli/bin/ run.js` stays out). Over 7472 tracked files: check families discovered 252 -> 254 (+2, ZERO lost) gate files 201 -> 202 (+1, ZERO lost) watch-hint (gate, file) pairs 240947 -> 254181 (+13234, ZERO lost) existing families re-attributed 0 — every pre-existing family's pair count is byte-identical before and after always-runs steps naming NO family 33 -> 32 The gate declares four subtree hints, so it lands in the derivation for cards touching `packages/**`, `examples/**`, `scripts/**` and `apps/**`, and by identity for a card editing its own file — not in the residue. Two existing tail cases pinned the defect as a property. Neither is weakened: each is inverted and gains a positive control, so the step's absence from the tail cannot go green through a tail that stopped walking. check-self-test-wired keyed the same path by SUBSTRING, filing it under `scripts/check-reference-carrier-shape.mjs` — a key with no file behind it, audited by nobody. `INVOCATION_RE` consumes the prefix and gains a left boundary; the population gains a second source, admitted on the root walk's own terms. Its two new skip predicates are membership in the walk's OWN OUTPUT rather than `startsWith('scripts/')`: that re-spelling reaches the dispatch derivation as the bare literal `scripts` and joined the shrink-only escapable-literal species FRESH on both of this gate's families. Self-tests grow four batteries: `left boundary` (6) and `live corpus` (3) in check-self-test-wired (50 -> 59 cases, floor 7 -> 9), and eight fixture cases plus two live pins in dispatch-gates. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --- scripts/check-self-test-wired.mjs | 15 ++- scripts/pm/dispatch-gates.mjs | 177 +++++++++++++++++++++++++++++- 2 files changed, 185 insertions(+), 7 deletions(-) diff --git a/scripts/check-self-test-wired.mjs b/scripts/check-self-test-wired.mjs index 0f080ca06a..c07b8b3765 100644 --- a/scripts/check-self-test-wired.mjs +++ b/scripts/check-self-test-wired.mjs @@ -484,8 +484,19 @@ function main() { // phantom this card is about, and admitting one would re-create it one layer // down. It is not a refusal either: this gate does not own what a workflow is // allowed to name, and the anchor above already declines to invent keys. + // + // ⛔ The skip is membership in the WALK'S OWN OUTPUT, never `startsWith` on a + // re-spelling of its root. That spelling is a bare top-level word wearing a + // separator, so it reaches the dispatch derivation's hint set as the plain + // literal `scripts` and joins the SHRINK-ONLY escapable-literal species + // (#10705) -- a population no `hintCovers` can name, declared by a gate that + // already declares the nameable spelling three lines up. Measured when this + // landed: the `startsWith` form added exactly that row, FRESH, to both of + // this gate's families. The set form says what the predicate means -- "the + // root walk did not already produce this path" -- and declares nothing. + const walked = new Set(files); for (const relPath of named.keys()) { - if (relPath.startsWith('scripts/')) continue; + if (walked.has(relPath)) continue; const source = sourceOf(relPath); if (source === null) continue; if (carriesSelfTest(relPath, source)) carriers.add(relPath); @@ -498,7 +509,7 @@ function main() { const members = [...carriers].filter((s) => named.has(s)); const wired = members.filter((s) => selfTested.has(s)); - const packageLocal = [...carriers].filter((s) => !s.startsWith('scripts/')); + const packageLocal = [...carriers].filter((s) => !walked.has(s)); const scope = ` scope: ${files.length} file(s) under scripts/, ${carriers.size} carrying \`--self-test\` in code ` + `(comments masked, ${packageLocal.length} of them package-local gate(s) CI names by path); ` + diff --git a/scripts/pm/dispatch-gates.mjs b/scripts/pm/dispatch-gates.mjs index 8b4c02702d..79b4a2e508 100644 --- a/scripts/pm/dispatch-gates.mjs +++ b/scripts/pm/dispatch-gates.mjs @@ -1109,6 +1109,73 @@ export function jobPathPopulations(workflowText, workflowFile) { return out; } +/** + * ## The DIRECTORY PREFIX both matchers below carry, and why it is shared (#15342) + * + * This repo has a package-local gate lane, and `lint.yml` really runs one of + * its gates by path — `node packages/lint/scripts/check-reference-carrier- + * shape.mjs --self-test`, with the bare production invocation on the next line. + * Both patterns used to open on the literal `scripts/` immediately after + * `node `, so neither matched that step AT ALL. The gate was in no family, and + * `--residue` could not report it either: it is absent from the universe the + * matched / silent / undetermined buckets partition, which is the #11397 state + * one lane over. Measured on the base tree for a card editing that gate's OWN + * file: `--commands` named it zero times, every one of the 27 `--residue` + * mentions was the dev's own CHANGED PATH echoed back as an input, and the step + * surfaced only in the always-runs STEP tail, as one of the 33 unconditional CI + * steps this derivation names no family for. + * + * ## What the prefix admits, and what it deliberately refuses + * + * - A prefix segment must START with an alphanumeric or `_`, so `..` is not + * one. A climbing spelling cannot be resolved against this ROOT without + * knowing what it is relative to, and `entry.files` would then carry a key + * with no file behind it — the phantom `resolveCheckToFiles`' docblock + * prices at "strictly worse than the bug being fixed". Zero occur in the + * workflow corpus today; refusing to match is the safe direction, and the + * sibling anchor in `scripts/check-self-test-wired.mjs` refuses it in the + * same spelling for the same reason. + * - A `scripts/` segment is still REQUIRED, so no new file species is + * admitted. `node packages/cli/bin/run.js` is the only other non-root path + * any workflow invokes with `node`, and it stays out. + * - The prefix cannot RE-ATTRIBUTE an existing match. For a path that already + * began at `scripts/` the group matches empty and the remainder of each + * pattern is byte-identical, so every key this tool minted before it is + * minted after it. Asserted below rather than left as this sentence. + * + * ## Priced in both directions, over 7472 tracked files (base `65846bc46`) + * + * check families discovered 252 -> 254 (+2, ZERO lost) + * gate files 201 -> 202 (+1, ZERO lost) + * watch-hint (gate, file) pairs 240947 -> 254181 (+13234, ZERO lost) + * existing families re-attributed 0 — every pre-existing family's pair count + * is byte-identical before and after + * always-runs steps naming NO family 33 -> 32 (18 -> 17 distinct commands) + * + * The +13234 is exactly 2 x 6617: the gate declares four subtree hints + * (`packages/**`, `examples/**`, `scripts/**`, `apps/**`) and CI runs it twice, + * so it arrives as two families under two keys — the split #14880 made, working + * as designed. Becoming a GATE FILE is the one direction of this change that + * could SUBTRACT (`discoverFamilies` excludes gate files from import-following), + * so it is measured rather than argued: no existing family's pair count moved, + * in either direction, so the net subtraction is zero. + * + * ## Why `SELF_TEST_INVOCATION` is widened too, on ZERO specimens + * + * Today's one package-local specimen carries a `check-` basename, so the direct + * matcher takes it and the self-test matcher skips it by design — that half + * gains no recall at all. It is widened anyway because the two matchers' + * coordination contract is that they mint BYTE-IDENTICAL keys for one script, + * and two adjacent patterns with two different path grammars cannot hold it: a + * package-local gate NOT named `check-*` would then be discovered by neither, + * which is this card's silence wearing a different filename. Same standard as + * the extension right-boundary in `resolveCheckToFiles` — zero recall today, + * zero cost today (both numbers above are the direct matcher's alone), class + * closed before the first specimen arrives. ⛔ Keep the two prefixes spelled + * identically; the self-test asserts that both accept the same package-local + * path, so a drift reds rather than going quiet. + */ + /** * A `run:` step that invokes a repo script with `--self-test`. The flag is the * SCRIPT'S OWN declaration that this invocation verifies the script rather than @@ -1130,7 +1197,7 @@ export function jobPathPopulations(workflowText, workflowFile) { * its own, which is correct: the inner script is the gate. */ const SELF_TEST_INVOCATION = - /node[ \t]+(scripts\/[\w./-]+\.mjs)(?:[ \t]+-{1,2}[A-Za-z0-9][\w-]*)*[ \t]+--self-test\b/g; + /node[ \t]+((?:[A-Za-z0-9_][\w.-]*\/)*scripts\/[\w./-]+\.mjs)(?:[ \t]+-{1,2}[A-Za-z0-9][\w-]*)*[ \t]+--self-test\b/g; /** * A `run:` step that invokes a `check-`named repo script directly, WITH the @@ -1150,7 +1217,7 @@ const SELF_TEST_INVOCATION = * outcomes. Joining is how that hazard is removed rather than merely refused. */ const DIRECT_CHECK_INVOCATION = - /node[ \t]+(scripts\/[\w./-]*check-[\w.-]+\.mjs)([^\n;|&<>()]*)/g; + /node[ \t]+((?:[A-Za-z0-9_][\w.-]*\/)*scripts\/[\w./-]*check-[\w.-]+\.mjs)([^\n;|&<>()]*)/g; /** * Splice a shell line-continuation back into ONE line, so a matcher reading a @@ -11519,6 +11586,67 @@ function selfTest() { t('extracts direct node scripts/check-*.mjs', invs.some((i) => i.check === 'scripts/check-nul-bytes.mjs' && i.direct)); t('ignores non-check runs', !invs.some((i) => String(i.check).includes('build'))); + // ── The package-local gate lane: a path is keyed WHOLE (#15342) ─────────── + // + // `lint.yml` invokes `packages/lint/scripts/check-reference-carrier-shape.mjs` + // by path, twice. Before the directory prefix documented beside the patterns, + // `node ` had to be followed IMMEDIATELY by `scripts/`, so neither matcher saw + // that step at all: no family, no hints, and nothing for `--residue` to place. + // + // Cases 1-3 and 6 FAIL against the base spelling — measured by applying this + // battery to `/node[ \t]+(scripts\/…)/` in a scratch ablation — which is what + // makes them an instrument rather than a restatement of the operators. Cases + // 4, 5, 7 and 8 hold on BOTH spellings: they are the controls that prove the + // widening did not buy its new answers by dropping the old ones. + { + const PKG = 'packages/lint/scripts/check-reference-carrier-shape.mjs'; + const pkgInvs = extractCheckInvocations( + ['jobs:', ' lint:', ' steps:', ' - name: package-local gate', ' run: |', + ` node ${PKG} --self-test`, ` node ${PKG}`].join('\n'), + 'lint.yml', + ); + t( + 'a gate CI invokes by a PACKAGE-LOCAL path is discovered at all — unfound, it is in no family, so no ' + + 'card can be told to run it and --residue has no bucket to place it in either (#15342)', + pkgInvs.some((i) => i.check === `${PKG} --self-test` && i.direct), + ); + t( + '…and its bare production invocation arrives as the SECOND family under its own key, the #14880 split', + pkgInvs.some((i) => i.check === PKG && i.direct), + ); + t( + '…both keyed by the REAL path, which is what `entry.files` carries and `existsSync` then opens', + pkgInvs.length === 2 && pkgInvs.every((i) => i.script === PKG), + ); + t( + 'control — no PHANTOM root key is minted beside them: a `scripts/…` TAIL keyed as a path in its own ' + + 'right names a file this ROOT does not hold, and every audit downstream then passes for the wrong reason', + !pkgInvs.some((i) => String(i.script ?? i.check).startsWith('scripts/')), + ); + t( + 'control — a CLIMBING spelling is refused rather than resolved: `..` is not a prefix segment, because ' + + 'a path this ROOT cannot resolve is exactly how a phantom identity key gets minted', + extractCheckInvocations(' - run: node ../scripts/check-x.mjs\n', 'x.yml').length === 0, + ); + t( + 'the SELF-TEST matcher carries the same prefix grammar — a package-local gate not named `check-*` ' + + 'would otherwise be discovered by neither matcher, which is this silence one filename over', + extractCheckInvocations(' - run: node packages/lint/scripts/carrier-census.mjs --self-test\n', 'x.yml') + .some((i) => i.check === 'packages/lint/scripts/carrier-census.mjs --self-test' && i.direct), + ); + t( + 'control — the prefix widens the DIRECTORY a gate may sit under and never the SPECIES of file: a path ' + + 'with no `scripts/` segment is still admitted by neither matcher (`packages/cli/bin/run.js` is live)', + extractCheckInvocations(' - run: node packages/cli/bin/run.js check\n', 'x.yml').length === 0, + ); + t( + 'control — a root-spelled invocation is keyed BYTE-IDENTICALLY to before, so the prefix re-attributes ' + + 'nothing: it matches empty there and the remainder of each pattern is unchanged', + extractCheckInvocations(' - run: node scripts/check-nul-bytes.mjs\n', 'x.yml') + .every((i) => i.check === 'scripts/check-nul-bytes.mjs' && i.script === 'scripts/check-nul-bytes.mjs'), + ); + } + // Block-scalar bodies (#8410). A step written `run: |` keeps its commands on // the following lines; reading only the `run:` line collected "|" and missed // every gate invoked this way. Both scalar styles and both invocation shapes @@ -11989,6 +12117,20 @@ function selfTest() { } const direct = liveInvs.filter((i) => i.direct); const valueBearing = direct.filter((i) => (i.argvVariables ?? []).length > 0); + // The live half of the package-local lane (#15342). The fixtures above prove + // the pattern; these two read the tree CI actually runs, so the day the lane + // moves this reds here instead of going quiet — and the second one holds the + // phantom class over EVERY direct invocation, not only over the specimen. + t( + '⭐ the live corpus really carries the package-local lane the directory prefix exists for, so the ' + + 'fixtures above judge a live class. If this reds, the lane moved — re-point it, never widen further', + direct.some((i) => i.script === 'packages/lint/scripts/check-reference-carrier-shape.mjs'), + ); + t( + `every one of the ${direct.length} direct invocation(s) in the live tree resolves to a file that EXISTS ` + + 'on disk — a key with no file behind it reads as a confident gate identity in both directions (#15342)', + direct.length > 0 && direct.every((i) => existsSync(nodePath.join(ROOT, i.script))), + ); t( `the live tree really carries ${valueBearing.length} value-bearing invocation(s) across ${new Set(valueBearing.map((i) => i.script)).size} script(s), so the cases above judge a live class`, valueBearing.length > 0, @@ -18749,7 +18891,20 @@ function selfTest() { const tail = alwaysRunSteps([{ file: 'fixture.yml', text: tailWf }]); const tailNames = tail.rows.map((r) => r.step); t('a step whose family the derivation names is NOT in the tail', !tailNames.includes('A discoverable family')); - t('a package-local gate invoked by path IS in the tail', tailNames.includes('A package-local gate invoked by path')); + // #15342 retired this member from the tail by giving the derivation the anchor + // to discover it, so what is pinned is the RETIREMENT WITH ITS CAUSE: the step + // has left the tail AND the derivation names a family for it. Either half + // alone goes green for the wrong reason — a tail that stopped walking, or a + // family list that claims the step while CI's step sits unaccounted for. The + // tail's own class ("a step the derivation names NOTHING for is listed") is + // unweakened: the interpreter member below still holds it, on this fixture and + // on the live tree. + t( + 'a package-local gate invoked by path is NOT in the tail any more (#15342) — the derivation names it', + !tailNames.includes('A package-local gate invoked by path') + && extractCheckInvocations(tailWf, 'fixture.yml') + .some((i) => i.script === 'packages/lint/scripts/check-fixture-shape.mjs'), + ); t('a gate run by another interpreter IS in the tail', tailNames.includes('A gate run by another interpreter')); t('a conditional STEP is excluded and counted', !tailNames.includes('Conditional, so no claim is made about it') && tail.counts.conditionalSteps === 1); t('a conditional JOB is excluded and counted', !tailNames.includes('Never claimed as always-run') && tail.counts.conditionalJobs === 1); @@ -18806,9 +18961,21 @@ function selfTest() { ); const liveCommands = liveTail.rows.flatMap((r) => r.commands); t('the live tail is not empty — an empty one would mean the walk broke, not that CI runs nothing', liveTail.rows.length > 0); + // #13333's first live instance was a package-local gate invoked by path. It is + // no longer here, and that is #15342 landing rather than this pin rotting — + // both halves are asserted for the reason the fixture case above states. + const liveDirectScripts = new Set( + readdirSync(nodePath.join(ROOT, '.github/workflows')) + .filter((f) => /\.ya?ml$/.test(f)) + .flatMap((f) => extractCheckInvocations(readFileSync(nodePath.join(ROOT, '.github/workflows', f), 'utf8'), f)) + .filter((i) => i.direct) + .map((i) => i.script), + ); t( - 'the live tail names the INSTANCE the card was filed about: a package-local gate invoked by path', - liveCommands.some((c) => /^node\s+packages\/\S+\/check-[\w.-]+\.mjs/.test(c)), + 'the INSTANCE the card was filed about has LEFT the live tail (#15342), and the derivation now names a ' + + 'family for it — the tail shrank because the step became accounted for, not because the walk broke', + !liveCommands.some((c) => /^node\s+packages\/\S+\/check-[\w.-]+\.mjs/.test(c)) + && liveDirectScripts.has('packages/lint/scripts/check-reference-carrier-shape.mjs'), ); // The class assertion. The instance above is invisible because its path is // not under `scripts/`; this one is invisible because its INTERPRETER is not