Skip to content

Commit f01adfa

Browse files
baozhoutaoclaude
andauthored
fix(pm): dispatch-gates and check-self-test-wired discover package-local gate invocations by their real path (#15342) (#15415)
* fix(pm): key a package-local gate invocation to its real path (#15342) `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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk * fix(pm): discover a package-local gate invocation and key it to its real path (#15342) `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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6ca9f93 commit f01adfa

2 files changed

Lines changed: 347 additions & 9 deletions

File tree

scripts/check-self-test-wired.mjs

Lines changed: 175 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,50 @@ const SCRIPT_EXT = /\.(mjs|mts|js|sh)$/;
152152
const ROOT_DIR_WATCH_HINTS = ['scripts/**/*.mjs', 'scripts/**/*.mts', 'scripts/**/*.sh'];
153153

154154
/**
155-
* A `scripts/...` path, optionally followed by `--self-test`.
155+
* A `scripts/...` path, optionally prefixed by the directories it lives under
156+
* and optionally followed by `--self-test`.
156157
*
157158
* The trailing `(?![\w-])` is the right boundary: without it `--self-test-extra`
158159
* reads as an invocation of `--self-test`, which is #10534's defect wearing this
159160
* gate's hat.
161+
*
162+
* ## The LEFT boundary, and why the prefix is read rather than cut off (#15342)
163+
*
164+
* This repo has a package-local gate lane, and `lint.yml` really does run one
165+
* of its gates by path: `node packages/lint/scripts/check-reference-carrier-
166+
* shape.mjs --self-test`. The pattern used to open on the bare literal
167+
* `scripts/` with nothing to its left, so it matched that path as a SUBSTRING
168+
* and filed the gate under `scripts/check-reference-carrier-shape.mjs` — a key
169+
* with no file behind it. Both directions of that were silent: the real file
170+
* was never audited (it is not under the root walk, so it is in no population),
171+
* and the phantom key could never be reconciled against a carrier either, so
172+
* neither `auditPopulation` nor `auditLedger` had anything to say. A gate whose
173+
* whole subject is "the self-test CI ships is the self-test CI runs" was
174+
* answering about a different script than the one CI executes.
175+
*
176+
* So the directory prefix is CONSUMED into the key rather than cut away, and
177+
* `(?<![\w.\-/])` is the left boundary that makes the key a whole path instead
178+
* of a tail of one. Both halves are load-bearing and were measured on this
179+
* tree's own corpus before landing:
180+
*
181+
* - a leading `./` is stripped rather than kept, because it is the same file
182+
* under a different spelling and paths are compared by EXACT equality here.
183+
* `cut-rc.yml` and `release.yml` both import from `"./scripts/check-docs-
184+
* image-tag.mjs"`; keying those to `./scripts/…` would silently drop two
185+
* live invocations the old pattern did credit.
186+
* - a segment must START with an alphanumeric or `_`, so `..` is not a
187+
* prefix segment. A `../…` path cannot be resolved against this ROOT
188+
* without knowing what it is relative to, and inventing a key for it is how
189+
* the phantom above was minted in the first place. Zero occur in the
190+
* workflow corpus today; refusing to match is the safe direction, and the
191+
* population half below never admits a path it cannot read.
192+
*
193+
* Measured after this change over the live corpus: 186 named paths, ZERO of
194+
* which lack a file on disk (before: exactly one, the phantom above). The
195+
* `live corpus` battery holds both halves of that reading.
160196
*/
161197
const INVOCATION_RE =
162-
/(scripts\/[A-Za-z0-9_.\-/]+\.(?:mjs|mts|js|sh))(\s+--self-test(?![\w-]))?/g;
198+
/(?<![\w.\-/])(?:\.\/)*((?:[A-Za-z0-9_][A-Za-z0-9_.\-]*\/)*scripts\/[A-Za-z0-9_.\-/]+\.(?:mjs|mts|js|sh))(\s+--self-test(?![\w-]))?/g;
163199

164200
/**
165201
* ⛔ SHRINK-ONLY. Scripts CI runs whose self-test IS run by CI, but not through
@@ -424,16 +460,60 @@ function main() {
424460
const { named, selfTested } = collectInvocations(workflows, pkgScripts);
425461
if (named.size === 0) refuse('no workflow names any scripts/ file — the workflow reader is broken (#4690).');
426462

463+
// The population's SECOND source: the package-local gate lane (#15342).
464+
//
465+
// `walkScripts` is anchored at the repo-root `scripts/` dir, so a gate CI
466+
// invokes by a package-local path is outside `carriers` no matter how the
467+
// anchor above keys it — and a script that is in no population is audited by
468+
// neither `auditPopulation` (it iterates carriers) nor `auditLedger`. Fixing
469+
// the key alone would have left that half exactly as silent as before.
470+
//
471+
// ⛔ NOT a second walk. The subject of this gate is "a script CI RUNS whose
472+
// self-test CI must run too", so what CI names is the honest population
473+
// boundary out here; a walk over every `packages/*/scripts` dir would admit
474+
// files CI never runs, which `auditPopulation` discards on the next line
475+
// anyway, and would owe `ROOT_DIR_WATCH_HINTS` a declaration that is no
476+
// longer set-equal to the root walk it is documented to mirror. The root walk
477+
// stays the ROOT half — it is what the `#4690` floors above are written
478+
// against ("this tree has dozens"), and a naming-derived population could not
479+
// carry those.
480+
//
481+
// Admitted on exactly the terms the root walk uses, and no looser: the file
482+
// must EXIST and its CODE (comments masked) must carry the literal. A named
483+
// path with nothing behind it is left OUT rather than admitted — that is the
484+
// phantom this card is about, and admitting one would re-create it one layer
485+
// down. It is not a refusal either: this gate does not own what a workflow is
486+
// allowed to name, and the anchor above already declines to invent keys.
487+
//
488+
// ⛔ The skip is membership in the WALK'S OWN OUTPUT, never `startsWith` on a
489+
// re-spelling of its root. That spelling is a bare top-level word wearing a
490+
// separator, so it reaches the dispatch derivation's hint set as the plain
491+
// literal `scripts` and joins the SHRINK-ONLY escapable-literal species
492+
// (#10705) -- a population no `hintCovers` can name, declared by a gate that
493+
// already declares the nameable spelling three lines up. Measured when this
494+
// landed: the `startsWith` form added exactly that row, FRESH, to both of
495+
// this gate's families. The set form says what the predicate means -- "the
496+
// root walk did not already produce this path" -- and declares nothing.
497+
const walked = new Set(files);
498+
for (const relPath of named.keys()) {
499+
if (walked.has(relPath)) continue;
500+
const source = sourceOf(relPath);
501+
if (source === null) continue;
502+
if (carriesSelfTest(relPath, source)) carriers.add(relPath);
503+
}
504+
427505
const findings = [
428506
...auditPopulation({ carriers, named, selfTested, ledger: SELF_TEST_RUN_OTHERWISE }),
429507
...auditLedger({ ledger: SELF_TEST_RUN_OTHERWISE, carriers, named, selfTested, sourceOf }),
430508
];
431509

432510
const members = [...carriers].filter((s) => named.has(s));
433511
const wired = members.filter((s) => selfTested.has(s));
512+
const packageLocal = [...carriers].filter((s) => !walked.has(s));
434513
const scope =
435514
` scope: ${files.length} file(s) under scripts/, ${carriers.size} carrying \`--self-test\` in code ` +
436-
`(comments masked); ${members.length} of those are run by ${workflows.length} workflow(s); ` +
515+
`(comments masked, ${packageLocal.length} of them package-local gate(s) CI names by path); ` +
516+
`${members.length} of those are run by ${workflows.length} workflow(s); ` +
437517
`${wired.length} have their self-test run through the flag, ${SELF_TEST_RUN_OTHERWISE.length} through a recorded route.`;
438518

439519
if (findings.length > 0) {
@@ -496,17 +576,19 @@ function main() {
496576
const SELF_TEST_BATTERIES = Object.freeze({
497577
'comment mask': 7,
498578
'right boundary': 4,
579+
'left boundary': 6,
499580
'alias resolution': 4,
500581
'population verdict': 4,
501582
'population declaration': 7,
583+
'live corpus': 3,
502584
'ledger hygiene': 9,
503585
'live ledger': 4,
504586
});
505587

506588
// DELETING an entry silences that battery's floor exactly as effectively as
507589
// zeroing it, so the registry's own size is pinned too. Adding a battery raises
508590
// this number; removing one is the same ⛔ deliberate edit as lowering a count.
509-
const SELF_TEST_BATTERY_FLOOR = 7;
591+
const SELF_TEST_BATTERY_FLOOR = 9;
510592

511593
// The key an assertion is filed under when no battery is open. It is not a
512594
// declared battery, so it reds by the same set difference rather than silently
@@ -588,6 +670,60 @@ function selfTest() {
588670
);
589671
}
590672

673+
// ── Left boundary: a path is keyed WHOLE, never as a tail of one (#15342) ─
674+
//
675+
// Every case here fails against the pre-#15342 anchor (`/(scripts\/…)/`, no
676+
// left boundary, no prefix), which is what makes them an instrument rather
677+
// than a restatement of the operators. Measured by applying this battery to
678+
// the base tree's pattern in a scratch ablation: cases 1-4 red there, 5-6
679+
// stay green — 5 and 6 are the controls that prove the widening did not buy
680+
// its new answers by dropping the old ones.
681+
battery('left boundary');
682+
{
683+
const PKG = 'packages/lint/scripts/check-reference-carrier-shape.mjs';
684+
const got = collectInvocations(wf(` - run: node ${PKG} --self-test\n`), {});
685+
ok(
686+
got.named.has(PKG),
687+
'a gate CI invokes by a PACKAGE-LOCAL path was not keyed to its real path — it is then in no '
688+
+ 'population and its self-test wiring is audited for nobody (#15342)',
689+
);
690+
ok(
691+
got.selfTested.has(PKG),
692+
'the package-local path was named but its `--self-test` was not credited to it',
693+
);
694+
ok(
695+
!got.named.has('scripts/check-reference-carrier-shape.mjs'),
696+
'the package-local path was ALSO filed under a root path with no file behind it — the phantom key '
697+
+ 'a substring match mints, which no carrier can ever reconcile (#15342)',
698+
);
699+
}
700+
{
701+
// A path that is a tail of a longer one must not be minted on its own.
702+
const got = collectInvocations(wf(' - run: node vendor/tools/scripts/g.mjs --self-test\n'), {});
703+
ok(
704+
!got.named.has('scripts/g.mjs'),
705+
'a `scripts/…` SUBSTRING of a longer path was minted as a key in its own right',
706+
);
707+
}
708+
{
709+
// Control 1 — the plain root spelling is unchanged.
710+
const got = collectInvocations(wf(' - run: node scripts/g.mjs --self-test\n'), {});
711+
ok(got.selfTested.has('scripts/g.mjs'), 'the ordinary root spelling stopped being keyed to itself');
712+
}
713+
{
714+
// Control 2 — `./scripts/…` still normalises onto the walk's key. Live
715+
// spelling: `cut-rc.yml` and `release.yml` import from `"./scripts/…"`,
716+
// and keying those anywhere else silently drops two real invocations.
717+
const got = collectInvocations(
718+
wf(' - run: node --input-type=module -e \'import { X } from "./scripts/g.mjs";\'\n'),
719+
{},
720+
);
721+
ok(
722+
got.named.has('scripts/g.mjs'),
723+
'a leading `./` was kept in the key, so the same file under two spellings stopped comparing equal',
724+
);
725+
}
726+
591727
// ── Aliases: reached only when a workflow actually names them ────────────
592728
battery('alias resolution');
593729
{
@@ -702,6 +838,41 @@ function selfTest() {
702838
);
703839
}
704840

841+
// ── The live corpus: the anchor is measured on specimens, not on fixtures ─
842+
//
843+
// A widening measured on zero specimens is not measured. The fixtures above
844+
// prove the pattern; these three read the tree CI actually runs, so the day
845+
// the package-local lane moves, this reds here instead of going quiet.
846+
battery('live corpus');
847+
{
848+
let corpus = null;
849+
try {
850+
const dir = join(ROOT, WORKFLOW_DIR);
851+
const workflows = readdirSync(dir)
852+
.filter((f) => /\.ya?ml$/.test(f))
853+
.sort()
854+
.map((name) => ({ name, text: readFileSync(join(dir, name), 'utf8') }));
855+
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).scripts ?? {};
856+
corpus = collectInvocations(workflows, pkg);
857+
} catch {
858+
corpus = null;
859+
}
860+
ok(corpus !== null && corpus.named.size > 0, 'the live workflow corpus could not be read — the pins below would prove nothing (#4690)');
861+
const keys = corpus === null ? [] : [...corpus.named.keys()];
862+
ok(
863+
keys.length > 0 && keys.every((p) => existsSync(join(ROOT, p))),
864+
'a path this gate keys an invocation to has NO file behind it. Every audit downstream then runs '
865+
+ 'against a script that does not exist, and passes for the wrong reason, in both directions (#15342)',
866+
);
867+
// Named BY NAME on purpose: it is this tree's only package-local gate
868+
// invocation, so it is the whole specimen set for the widening above.
869+
ok(
870+
keys.includes('packages/lint/scripts/check-reference-carrier-shape.mjs'),
871+
"lint.yml's package-local gate is not in the live population. Either the lane moved — re-point this "
872+
+ 'pin at the new specimen — or the anchor regressed to a root-only one and the widening is untested',
873+
);
874+
}
875+
705876
// ── Ledger hygiene: every row must still be true, and still be needed ────
706877
battery('ledger hygiene');
707878
{

0 commit comments

Comments
 (0)