Skip to content

Commit ca4e2d1

Browse files
os-steveclaude
andcommitted
ci(gates): declare this gate's bare roots so the dispatch derivation can see it
`INSTRUCTION_ROOTS` and `EXECUTED_ROOTS` spelled two of their four roots as bare single-segment words. `extractWatchHints` builds no hint from a literal without a separator, so a card touching `scripts/**` or `skills/**` derived this gate NOWHERE — including the cards most likely to add the spelling it exists to judge. `scripts/pm/bare-root-worklist.mjs --self-test` caught it on the commit that introduced it. Declared rather than refused, and the choice is measured from this gate's own walker rather than taken from the shape: numerator is what `scannedFiles()` admits, denominator the tracked files under the subtree the declaration names — `skills/**` 50 of 50 (100%), `scripts/**` 239 of 242 (98.8%, the 3 misses are `.txt`). This is the `subtree` case, not the `filtered` one: the walk descends the whole of each root and the only losses are non-code files the extension filter drops. Both precedents are already in this tree at the same or lower coverage — `check-pnpm-filter-targets.mjs` declares `scripts/**` at 97.0%, and `check-role-word.mjs` declares `skills/**`. `AGENTS.md/**` and `CLAUDE.md/**` take the escapable repo-root-FILE case the derivation's own residue names: a bare filename is refused as too generic and reaches its file through the subtree spelling. Measured — `AGENTS.md/**` covers `AGENTS.md` and does NOT cover `examples/AGENTS.md`, so it claims the one file this gate reads at that root and does not overclaim the nested ones. The workspace-manifest read stays UNDECLARED: the only spellable claim is `packages/**` + `apps/**` + `examples/**`, which would name this gate for ~5200 files to reach ~80 manifests. Refused, and pinned in `--self-test`. ⚠️ The declaration is pinned STRUCTURALLY, derived from the scan roots, not by importing `hintCovers`. An earlier draft did import it, and `discoverFamilies` then treated dispatch-gates.mjs as this gate's own source and hoisted ITS path literals — `packages/spec/src/**`, `packages/plugins`, `packages/drivers`, `packages/services` — into this gate's hints. That is the fabricated lead the declaration exists to avoid, arriving through the assertion meant to prevent it. Measured before and after: 19 hints with the import, 11 without, 0 fabricated. `scripts/pm/bare-root-worklist.mjs` itself is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
1 parent af75880 commit ca4e2d1

1 file changed

Lines changed: 136 additions & 0 deletions

File tree

scripts/check-agent-test-spelling.mjs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,85 @@ export const EXECUTED_ROOTS = ['.github/workflows', 'scripts'];
257257
*/
258258
export const LOOSE_FILE_NAMES = new Set(['AGENTS.md', 'CLAUDE.md']);
259259

260+
/**
261+
* The half of this gate's population that `scripts/pm/dispatch-gates.mjs` could
262+
* not see, written in the syntax that derivation CAN read (#10840).
263+
*
264+
* ## The defect this repairs, and how it was caught
265+
*
266+
* `extractWatchHints` builds a hint only from a literal that "looks pathy" —
267+
* carries a separator, or starts with a dotted root. `INSTRUCTION_ROOTS` and
268+
* `EXECUTED_ROOTS` spell two of their four roots as BARE single-segment words,
269+
* so a card touching `scripts/**` or `skills/**` derived this gate NOWHERE. It
270+
* was invisible to the very derivation that is supposed to route work to it,
271+
* including the cards most likely to add the spelling it exists to judge.
272+
*
273+
* Measured on this tree before the declaration landed — `.claude` cleared
274+
* `looksPathy` on its dotted branch and `.github/workflows` on its separator,
275+
* so exactly the two bare words were blind:
276+
*
277+
* scripts/foo.mjs misses
278+
* skills/objectstack-data/rules/indexing.md misses
279+
* AGENTS.md misses
280+
* CLAUDE.md misses
281+
* .claude/agents/os-dev.md DERIVES
282+
* .github/workflows/ci.yml DERIVES
283+
*
284+
* `scripts/pm/bare-root-worklist.mjs --self-test` is the ratchet that caught it,
285+
* and it caught it on the commit that introduced it.
286+
*
287+
* ## Why declaring is honest here — the measurement, from this gate's own walker
288+
*
289+
* ⛔ The costlier error is declaring a root the gate does not read WHOLESALE: a
290+
* root hint covers a whole SUBTREE, so a declaration over a kind-filtered walk
291+
* fabricates a lead in every dispatch prompt that brushes it. That is why most
292+
* rows in the worklist are refusals rather than declarations. So the numbers
293+
* below are numerator = files `scannedFiles()` admits, denominator = tracked
294+
* files under the subtree the declaration names:
295+
*
296+
* skills/** 50 of 50 100.0% nothing under it is skipped at all
297+
* scripts/** 239 of 242 98.8% the 3 misses are `.txt`
298+
* AGENTS.md/** 1 of 1 100.0% the repo-root file, named exactly
299+
* CLAUDE.md/** 1 of 1 100.0% the repo-root file, named exactly
300+
*
301+
* This is the `subtree` case, not the `filtered` one: the walk descends the
302+
* whole of each root and every file carrying a scanned extension is read. The
303+
* three files it skips are the non-code files the extension filter drops, not a
304+
* subtree it never opens — the same shape `check-pnpm-filter-targets.mjs`
305+
* declares `scripts/**` on at 97.0%, and `check-role-word.mjs` declares
306+
* `skills/**` on. Both precedents are in this tree.
307+
*
308+
* The two `.md/**` entries are the escapable case the derivation's own residue
309+
* names: a population that is a repo-ROOT FILE spelled as a bare filename is
310+
* refused as too generic, and reaches its file by declaring the subtree
311+
* spelling. `hintCovers('AGENTS.md/**', 'AGENTS.md')` is true and
312+
* `hintCovers('AGENTS.md/**', 'examples/AGENTS.md')` is FALSE — both measured —
313+
* so this claims the one file it reads and does not overclaim the nested ones.
314+
*
315+
* ## What stays UNDECLARED, and why that is not a gap
316+
*
317+
* `deriveVitestScripts()` reads every tracked `package.json` in the workspace —
318+
* a real read, deliberately not reached here. The instrument cannot express it:
319+
* the only spellable claim is `packages/**` + `apps/**` + `examples/**`, which
320+
* would name this gate for every one of the ~5200 tracked files under those
321+
* roots in order to reach ~80 manifests. That is the +139084 fabrication
322+
* `hintCovers`' docblock prices, and it is refused.
323+
*
324+
* The nested `AGENTS.md` files (`examples/`, `packages/create-objectstack/`) are
325+
* unreachable for the same reason and stay so. Both refusals are pinned in
326+
* `--self-test`, so a later author who adds those globs meets an assertion
327+
* instead of this paragraph.
328+
*
329+
* ## Provenance, never a lookup key
330+
*
331+
* ⛔ Nothing in this gate reads this array. The glob form appearing in
332+
* `INSTRUCTION_ROOTS` / `EXECUTED_ROOTS` would point the walk at a directory
333+
* that does not exist, and `run()` REFUSES a missing declared root — so the
334+
* mistake would turn this gate red rather than quietly shrink its population.
335+
* `--self-test` pins the two apart in both directions.
336+
*/
337+
export const ROOT_DIR_WATCH_HINTS = ['skills/**', 'scripts/**', 'AGENTS.md/**', 'CLAUDE.md/**'];
338+
260339
const SCANNED_EXTENSIONS = new Set([
261340
'.md',
262341
'.mdx',
@@ -865,6 +944,63 @@ function selfTest() {
865944
t('every counter-example carries a reason', COUNTER_EXAMPLE_FILES.every((e) => typeof e.reason === 'string' && e.reason.trim().length > 0), true);
866945
t('the counter-example list stays tiny', COUNTER_EXAMPLE_FILES.length <= 3, true);
867946

947+
// ── The dispatch-gates declaration (#10840) ───────────────────────────────
948+
//
949+
// ⛔ Enforcement cannot hold any of this: the declaration is read by ANOTHER
950+
// tool entirely, so a wrong or stale one runs green here forever and pays
951+
// itself out as a dev dispatched on a scripts/ or skills/ card with this gate
952+
// missing from the brief.
953+
//
954+
// ⛔ And it is pinned STRUCTURALLY — derived from the scan roots on both
955+
// sides — rather than by importing `hintCovers` from the derivation. That is
956+
// the precedents' choice (`check-role-word.mjs`, `check-driver-conformance.mjs`,
957+
// `check-examples-live-imports.mjs`: none of them imports it) and the reason
958+
// is measured, not stylistic. An earlier draft of this block DID import it,
959+
// and `discoverFamilies` then treated `scripts/pm/dispatch-gates.mjs` as this
960+
// gate's own source and hoisted ITS path literals into this gate's hints:
961+
// `packages/spec/src/**`, `packages/plugins`, `packages/drivers`,
962+
// `packages/services`. A card touching any of those would have derived this
963+
// gate, which reads nothing there — the fabricated lead this declaration
964+
// exists to avoid, arriving through the assertion meant to prevent it.
965+
// The live `hintCovers` results are recorded in the docblock as a
966+
// MEASUREMENT, taken at the command line where it costs nothing.
967+
console.log('the dispatch-gates declaration — both directions, derived from the scan roots');
968+
{
969+
const scanRoots = [...INSTRUCTION_ROOTS, ...EXECUTED_ROOTS];
970+
// A root with no separator is refused by the extractor as too generic, so
971+
// it is exactly the set that NEEDS the subtree spelling. Derived, so that
972+
// renaming or adding a root cannot leave the declaration describing the old
973+
// population.
974+
const separatorless = scanRoots.filter((r) => !r.includes('/') && !r.startsWith('.'));
975+
t('every scan root the extractor cannot see has the subtree spelling declared',
976+
separatorless.every((r) => ROOT_DIR_WATCH_HINTS.includes(`${r}/**`)), true);
977+
t('…and the set it had to cover is the two the worklist flagged',
978+
[...separatorless].sort(), ['scripts', 'skills']);
979+
// The repo-ROOT files are the other escapable case: a bare filename is
980+
// refused as too generic and reaches its file through the same spelling.
981+
t('every loose root file is declared the same way',
982+
[...LOOSE_FILE_NAMES].every((n) => ROOT_DIR_WATCH_HINTS.includes(`${n}/**`)), true);
983+
// The opposite direction — a declaration that can drift from the scan is
984+
// worse than none, because it replaces a silent gate with a lying one.
985+
t('and it declares nothing this gate does not read',
986+
ROOT_DIR_WATCH_HINTS.every((h) => {
987+
const bare = h.replace(/\/\*+$/, '');
988+
return scanRoots.includes(bare) || LOOSE_FILE_NAMES.has(bare);
989+
}), true);
990+
// The refusals from the docblock, pinned so a later author who adds them
991+
// meets an assertion rather than a paragraph: declaring these would name
992+
// this gate for ~5200 files to reach the ~80 manifests it actually opens.
993+
for (const refused of ['packages/**', 'apps/**', 'examples/**']) {
994+
t(`⛔ ${refused} stays UNDECLARED — the manifest read is unspellable at honest precision`,
995+
ROOT_DIR_WATCH_HINTS.includes(refused), false);
996+
}
997+
// Provenance, never a lookup key: the glob form must never leak into the
998+
// scan roots, where it would point the walk at a directory that does not
999+
// exist — and `run()` refuses a missing declared root, so it would go red.
1000+
t('no declared hint is itself a scan root',
1001+
ROOT_DIR_WATCH_HINTS.some((h) => scanRoots.includes(h)), false);
1002+
}
1003+
8681004
console.log('the derivation reads THIS workspace, and reads it non-empty');
8691005
const derived = deriveVitestScripts(REPO_ROOT);
8701006
t('derives a non-empty script set', derived.names.size > 0, true);

0 commit comments

Comments
 (0)