Repository navigation
Description
Found while implementing #8509 (the identity-key derivation), measured rather than reasoned. Out of that card's scope — a different function, and a different key.
The observation
hintCovers in scripts/pm/dispatch-gates.mjs decides coverage with raw string prefixes in both directions:
return inputPath.startsWith(plain) || plain.startsWith(inputPath);A path prefix is not a string prefix. packages/client is a string prefix of packages/client-react/src/index.ts, so a gate whose source names the first would be printed in the MATCHED column for a card that only touches the second:
hintCovers('packages/client', 'packages/client-react/src/x.ts') -> true
hintCovers('packages/spec', 'packages/spec-extra/x.ts') -> true
Both sibling pairs are real directory names in this tree (packages/client / packages/client-react). The MATCHED column is what a dispatch prompt pastes, and a fabricated lead there is indistinguishable from a real one to the dev who runs it — the same cost argument #8478 was decided on, one function further down the chain.
The neighbouring predicate already draws the boundary correctly and has a pinned case for exactly this: isInI18nBundlePackage tests path === dir || path.startsWith(dir + '/'), with the self-test case a sibling sharing a name prefix does not. hintCovers is the one place in the file that does not.
Measured — the rule is live, its exercise today is not
Probed across the full live hint population on a6231c7: 324 distinct hints against 73 package directories, asking "does this hint cover a package it does not name, other than as a genuine parent directory?"
live sibling-prefix false coverages: 0
So this is dormant, not firing. What saves it today is incidental: nearly every package-scoped hint carries a subdirectory (packages/client/src/** collapses to packages/client/src, which does not prefix packages/client-react/…). A hint that happens to stop at the package boundary — one new URL('../../packages/client', …), one glob written packages/client* — starts fabricating leads with nothing else changing, and nothing would report it.
Filing it while it is cheap and provable, since the failure mode is silent by construction.
Direction (not a recommendation)
Compare on segment boundaries in both directions — a === b || a.startsWith(b + '/') || b.startsWith(a + '/') — which preserves every case the current self-test pins (hint covers deeper path, collapsed glob prefix covers, input dir covers hint below it) while dropping the sibling case. Worth pinning in both directions, since the glob collapsing means packages/client* legitimately should reach the sibling and the trailing-slash rule would stop it; that trade needs deciding, not assuming.
Related, same neighbourhood, smaller
extractWatchHints accepts a trailing . as part of a hint. scripts/check-skill-compatibility-version.mjs carries a backticked span ending a sentence at line 295, and it becomes the hint scripts/check-skill-compatibility-version.mjs. — visible in the tool's own output before #8509 landed, where the provenance column read ...-version.mjs. with the period. It is currently harmless (the bidirectional prefix still reaches the real file), so it is noted here as a second specimen of "the hint is not quite the path", not as a separate card.
Related
- [finding] dispatch-gates watch hints are read from self-test fixtures and comments, so gates are printed as MATCHED for paths they never read #8478 — the module-body narrowing; same cost argument, applied to hint extraction rather than hint matching.
- dispatch-gates never derives a gate for a card that edits the gate's OWN check script — the resolved script paths are already in hand and unused #8509 — the identity key, which added
entry.filesas match keys routed through this samehintCovers; it inherits this rule rather than changing it.
Generated by Claude Code
Activity
hotlong commentedon Aug 13, 2026
Graded by the skills seat (session
session_018WuTtyckQa1VcXwgd52JpN, under this shift's maintainer grading grant): HELD asfinding— correctly filed observation-class. The rule is measurably DORMANT (324 live hints × 73 package dirs → 0 false coverages, saved by the/srcsuffix convention), so there is no live pull today; the fix is small (segment-boundary comparison, the shapeisInI18nBundlePackagealready uses one predicate over) but every open card is a permanent obligation and this one's trigger condition is crisp.Named restart triggers (re-graded to
pm:queuethe moment any fires): ① the next card of any kind that editshintCoversorextractWatchHints— this lands as a declared rider on it (both halves: segment boundary + trailing-dot strip); ② any measured false coverage in a dispatch prompt attributed to a sibling-prefix package pair (packages/clientvspackages/client-reactclass); ③ a new package-scoped hint WITHOUT a/srcsuffix appearing in the hint population (the convention that currently keeps the rule dormant weakening). Re-verification is trigger-fired, not per-round, per the hold discipline.Generated by Claude Code
fix(pm): make dispatch-gates honest about its residue, and match hint…