Found while implementing #8478 (module-body-only watch hints), measured rather than reasoned.
The observation
derive() in scripts/pm/dispatch-gates.mjs resolves each discovered check:* family to the script FILES that implement it, and stores them on entry.files. It then throws that away as a matching key: the only thing compared against the card's input paths is entry.hints, the path literals scanned out of those files' contents.
So the most direct relationship the tool has access to is the one it never uses — this gate IS this file. A card that edits scripts/check-empty-changeset.mjs should derive pnpm check:changeset-gate-self-tests, the gate whose entire job is running that script's self-test. It does not, because the gate's invocation names the script in package.json, not in the script's own source.
Measured, on main at 3208222 and on the #8478 branch
"Does editing a gate script derive at least one gate family?", over all 66 discoverable gate scripts:
| tree | scripts that derive a gate when edited |
|---|
main (before #8478) | 14 of 66 |
| #8478 branch (module-body hints) | 7 of 66 |
The 14 were never a feature — they are the scripts that happen to quote their own filename somewhere in their source (a usage line, a subprocess spawn in the self-test). #8478 narrows the scan to the module body, so the accidental half mostly goes away and the structural gap is what is left. Concretely, on the branch:
node scripts/pm/dispatch-gates.mjs scripts/check-empty-changeset.mjs
-> no check family names the given paths in its own source
scripts/pm/dispatch-gates.mjs itself is one of the seven survivors, and only because a separate file — scripts/pm/check-dispatch-gates.mjs — names it as its one module-body constant. That is the shape all 66 would need, one thin gate file each, which is not a proposal.
Why it is worth a card
This is the one derivation that needs no heuristic at all. Every other match is a judgment about whether a path literal covers an input; this one is an identity the tool computed itself, three lines earlier, and then discarded. The blind spot is also self-shaped: it is exactly the class of card that edits gate tooling, which is the work most likely to break a gate.
Direction (not a recommendation)
In derive(), treat each of entry.files as a watch hint for its own family, alongside the scanned ones — the matched via column would then read scripts/check-empty-changeset.mjs => 'scripts/check-empty-changeset.mjs' with the file path as provenance. Nothing needs listing: entry.files is already resolved at runtime from package.json, the same derived-never-listed contract the rest of the script keeps. The self-test would pin both directions (a gate script derives its own family; an unrelated script does not).
Deliberately NOT attempted in #8478: that card's scope is extractWatchHints and its self-test, and this fix lives in derive.
Related
Generated by Claude Code
Found while implementing #8478 (module-body-only watch hints), measured rather than reasoned.
The observation
derive()inscripts/pm/dispatch-gates.mjsresolves each discoveredcheck:*family to the script FILES that implement it, and stores them onentry.files. It then throws that away as a matching key: the only thing compared against the card's input paths isentry.hints, the path literals scanned out of those files' contents.So the most direct relationship the tool has access to is the one it never uses — this gate IS this file. A card that edits
scripts/check-empty-changeset.mjsshould derivepnpm check:changeset-gate-self-tests, the gate whose entire job is running that script's self-test. It does not, because the gate's invocation names the script in package.json, not in the script's own source.Measured, on
mainat 3208222 and on the #8478 branch"Does editing a gate script derive at least one gate family?", over all 66 discoverable gate scripts:
main(before #8478)The 14 were never a feature — they are the scripts that happen to quote their own filename somewhere in their source (a usage line, a subprocess spawn in the self-test). #8478 narrows the scan to the module body, so the accidental half mostly goes away and the structural gap is what is left. Concretely, on the branch:
scripts/pm/dispatch-gates.mjsitself is one of the seven survivors, and only because a separate file —scripts/pm/check-dispatch-gates.mjs— names it as its one module-body constant. That is the shape all 66 would need, one thin gate file each, which is not a proposal.Why it is worth a card
This is the one derivation that needs no heuristic at all. Every other match is a judgment about whether a path literal covers an input; this one is an identity the tool computed itself, three lines earlier, and then discarded. The blind spot is also self-shaped: it is exactly the class of card that edits gate tooling, which is the work most likely to break a gate.
Direction (not a recommendation)
In
derive(), treat each ofentry.filesas a watch hint for its own family, alongside the scanned ones — thematched viacolumn would then readscripts/check-empty-changeset.mjs => 'scripts/check-empty-changeset.mjs'with the file path as provenance. Nothing needs listing:entry.filesis already resolved at runtime from package.json, the same derived-never-listed contract the rest of the script keeps. The self-test would pin both directions (a gate script derives its own family; an unrelated script does not).Deliberately NOT attempted in #8478: that card's scope is
extractWatchHintsand its self-test, and this fix lives inderive.Related
Generated by Claude Code