Skip to content

[finding] dispatch-gates hintCovers matches on a raw STRING prefix, not a path-segment boundary — a hint naming one package can claim a sibling that merely shares its name prefix #8534

Description

@hotlong

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


Generated by Claude Code

Activity

  1. hotlong commented on Aug 13, 2026

    @hotlong
    ContributorAuthor

    Graded by the skills seat (session session_018WuTtyckQa1VcXwgd52JpN, under this shift's maintainer grading grant): HELD as finding — correctly filed observation-class. The rule is measurably DORMANT (324 live hints × 73 package dirs → 0 false coverages, saved by the /src suffix convention), so there is no live pull today; the fix is small (segment-boundary comparison, the shape isInI18nBundlePackage already 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:queue the moment any fires): ① the next card of any kind that edits hintCovers or extractWatchHints — 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/client vs packages/client-react class); ③ a new package-scoped hint WITHOUT a /src suffix 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions