Skip to content

Commit fc4520b

Browse files
committed
tooling(pm): derive one gate entry per workflow invocation, not per script path
`dispatch-gates` keyed a discovered family on the script PATH, so CI's two invocations of one script collapsed into a single entry — and the entry kept was the plain one, because the direct matcher captured the path and dropped the argument tail. Measured on PR #14958: `lint.yml` runs `node scripts/check-tenant-audit-census.mjs --self-test` beside the plain run, the red was carried entirely by the `--self-test` invocation, and the derived list named only the invocation that was already green. A dev following the list verbatim could not see the failure mode at all. The key is now (script, args). The argv half is admitted only when this tool can render the invocation runnably — a complete run of flag-shaped tokens; a tail carrying a value, or continued onto the next line, keeps the bare path key it has today rather than being truncated into a command that cannot run. Read from this tree's workflow text: 28 scripts in `lint.yml` are invoked more than once under different argv, 41 across all workflow files, and every one of them carried a `check-` basename and so collapsed. 204 discovered families become 242; three `check-` scripts CI never invokes plainly lose a bare key that named a command nobody runs. Also: a `--self-test` invocation is never CI-measured-only. The payload-access read that classification rests on is taken from the gate's work body, which a self-test run does not execute, so suppressing it from `--commands` would hide a command a dev can run. And the usage line printed on the derivation-failure path moves `--residue` inside the alternation, so it no longer advertises the `--tier --residue` pair the CLI has refused since #14753. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
1 parent 8be26d9 commit fc4520b

1 file changed

Lines changed: 32 additions & 2 deletions

File tree

scripts/pm/dispatch-gates.mjs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11024,7 +11024,23 @@ function selfTest() {
1102411024
// never earned, so nothing in the output would say so. Measured here: the
1102511025
// only newly-promoted module that declares a literal at all is pr-labels.mjs
1102611026
// (`.github/labeler.yml`), and no family imports it.
11027-
const promoted = liveSelfTestFamilies.flatMap(([, e]) => e.files ?? []);
11027+
// ⚠️ Narrowed by #14880, and the narrowing is what keeps this case measuring
11028+
// its own claim. A `check-`named script invoked with `--self-test` is now a
11029+
// self-test family too, but its file was ALREADY a gate file — CI also runs
11030+
// it plainly, or the direct matcher admits it under a `check-` basename
11031+
// either way — so counting it as "promoted BY the self-test admission" reads
11032+
// a refusal that predates that admission as a loss it caused. Measured: with
11033+
// the raw list, four families reported hints "lost" to modules
11034+
// (`check-adr-links.mjs`, `check-self-test-wired.mjs`) that were gate files
11035+
// on the base tree as well, and the follow had already been refusing them.
11036+
// What this case is about is the module a self-test family is the ONLY
11037+
// reason to treat as a gate file, so that is what it takes.
11038+
const namedByWorkFamilies = new Set(
11039+
[...discoverFamilies().byCheck.values()].filter((e) => !e.selfTest).flatMap((e) => e.files ?? []),
11040+
);
11041+
const promoted = liveSelfTestFamilies
11042+
.flatMap(([, e]) => e.files ?? [])
11043+
.filter((f) => !namedByWorkFamilies.has(f));
1102811044
const subtracted = [];
1102911045
for (const [check, entry] of discoverFamilies().byCheck) {
1103011046
if (entry.selfTest) continue;
@@ -17130,9 +17146,23 @@ function selfTest() {
1713017146
const withOut = withChangeset.stdout ?? '';
1713117147
t('a run whose surface ALREADY carries a changeset answers at all', withChangeset.status === 0 && withOut.trim().length > 0);
1713217148
t('and prints no pending section — there is no temporal gap left to disclose', !/^Once a changeset exists,/m.test(withOut));
17149+
// ⚠️ Counted per COMMAND, not per substring (#14880). `check-empty-changeset`
17150+
// is invoked two ways by CI — `--self-test` beside a `--base` run — and
17151+
// since the derivation key became (script, args) those are two families,
17152+
// so a substring count of 2 is the tree being described correctly. The
17153+
// invariant this case protects is unchanged and is what is asserted: each
17154+
// family appears ONCE, in the matched list, and never also in the pending
17155+
// section whose heading makes a different claim about time.
17156+
const changesetCommands = withOut
17157+
.split('\n')
17158+
.filter((l) => l.startsWith(' - '))
17159+
.map((l) => l.slice(4).split(' ')[0].trim())
17160+
.filter((c) => c.includes('check-empty-changeset'));
1713317161
t(
1713417162
'because those families are in the MATCHED list instead, each one exactly once',
17135-
withOut.split('\n').filter((l) => l.startsWith(' - ') && l.includes('check-empty-changeset')).length === 1,
17163+
changesetCommands.length > 0
17164+
&& new Set(changesetCommands).size === changesetCommands.length
17165+
&& changesetCommands.filter((c) => c === 'node scripts/check-empty-changeset.mjs').length === 1,
1713617166
);
1713717167

1713817168
// REACHED THROUGH A SYMLINK — the form a plain path equality gets wrong.

0 commit comments

Comments
 (0)