Skip to content

Commit 987fe37

Browse files
claude[bot]claude
andauthored
Declare the populations four under-matching gates really read, and refuse a dead declaration (#14188)
* fix(devx): declare the populations four under-matching gates really read, and refuse a dead declaration Fixes #13519 * fix(devx): build the manifest matcher without a placeholder byte * fix(devx): declare check:skill-docs at 12-of-12 precision, and re-point its worklist row --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9dd022a commit 987fe37

9 files changed

Lines changed: 648 additions & 24 deletions

‎.github/workflows/lint.yml‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,24 @@ jobs:
688688
- name: ROOT_DIR_WATCH_HINTS declarations are literals
689689
run: pnpm check:watch-hint-literal
690690

691+
# The other half of the same failure (#13519). The step above holds that a
692+
# declaration is still READABLE BY A TEXT SCANNER; this one holds that
693+
# what it reads still names something. A family whose declared literals
694+
# all reach zero tracked files prints as an ORDINARY SILENCE — byte for
695+
# byte the output of a gate that declared nothing — so the derivation and
696+
# the gate disagree about whether a population was declared and neither
697+
# says so. Measured on the tree this landed against: two families, one of
698+
# them a gate that sweeps every workflow file and appeared on no card that
699+
# edited one. The rule is per-FAMILY, not per-literal: a gate may spell a
700+
# slug or a sentinel path beside a live declaration, and only a whole
701+
# declaration that reaches nothing is refused. No allowlist and no gate
702+
# names — the stronger rule ("a gate that enumerates a directory must
703+
# declare one") was implemented, measured at 86 findings over 114
704+
# enumerating gate files, and refused as an allowlist with a verdict
705+
# attached. Reads the derivation once over the tracked corpus; ~5s.
706+
- name: A declared gate population reaches the tree
707+
run: pnpm check:declared-population-live
708+
691709
# PM bare-root worklist self-test (#10840). The step above proves the
692710
# dispatch derivation still WORKS; this one proves the recorded triage of
693711
# the gates that derivation structurally cannot see is still true of the

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"check:pm-label-desc-cap": "node scripts/pm/check-label-desc-cap.mjs --self-test && node scripts/pm/check-label-desc-cap.mjs",
7171
"check:pm-dispatch-gates": "node scripts/pm/check-dispatch-gates.mjs",
7272
"check:watch-hint-literal": "node scripts/check-watch-hint-literal.mjs --self-test && node scripts/check-watch-hint-literal.mjs",
73+
"check:declared-population-live": "node scripts/check-declared-population-live.mjs --self-test && node scripts/check-declared-population-live.mjs",
7374
"check:pm-half-states": "node scripts/pm/check-half-states.mjs --self-test",
7475
"check:pm-clause2-carriers": "node scripts/pm/check-clause2-carriers.mjs --self-test",
7576
"check:pm-governed-merges": "node scripts/pm/check-governed-merges.mjs --self-test",

‎packages/spec/scripts/build-skill-docs.ts‎

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,73 @@ import path from 'path';
2626

2727
const REPO_ROOT = path.resolve(__dirname, '../../..');
2828
const SKILLS_DIR = path.resolve(REPO_ROOT, 'skills');
29+
/** The file a child of the skills root must CARRY to be a skill (`:main` admits on it). */
30+
const ENTRYPOINT_FILE = 'SKILL.md';
2931
const README = path.resolve(SKILLS_DIR, 'README.md');
3032
const GUIDE = path.resolve(REPO_ROOT, 'content/docs/ai/skills-reference.mdx');
3133

34+
/**
35+
* The population this script READS, declared for `scripts/pm/dispatch-gates.mjs`
36+
* — spelled as a LITERAL array because the hint extractor reads source TEXT (a
37+
* declaration computed from `SKILLS_DIR` would build no hint at all;
38+
* `scripts/check-watch-hint-literal.mjs` holds that spelling for every declarer
39+
* in the tree).
40+
*
41+
* ## What was declared before, and why it under-matched
42+
*
43+
* The only path literal this module body spelled was the GUIDE path,
44+
* `content/docs/ai/skills-reference.mdx` — an OUTPUT. So `check:skill-docs`
45+
* declared its generated artifact and not one of its inputs: a card editing a
46+
* skill entrypoint — the file whose frontmatter is the entire catalog — was
47+
* never told that this gate reads it, and `--check` reds in CI on drift the
48+
* derivation could have predicted.
49+
*
50+
* The inputs were not forgotten, they were unspellable AS ONE HINT: `SKILLS_DIR`
51+
* is built with `path.resolve(REPO_ROOT, 'skills')`, and a single-segment
52+
* literal is refused by the extractor as too generic to be a path population.
53+
*
54+
* ## Why NOT the subtree spelling, which is the idiom's usual escape
55+
*
56+
* `scripts/pm/bare-root-worklist.mjs` carries the measured triage for exactly
57+
* this root and refuses the wholesale hint: this script's population is 12 of
58+
* the 50 files tracked under it (24%), so `skills/**` would name this gate for
59+
* 38 files it never opens. That is the REFUSE-WIDE trade, and the worklist
60+
* prices a false wholesale hint as the costlier error.
61+
*
62+
* What the recorded refusal turned on is that no SINGLE spelling reaches both
63+
* halves: `:221` readdirs the root and admits a child only if it carries a
64+
* SKILL.md, while the twelfth file is the root README this script WRITES, which
65+
* sits outside every skill directory. Two literals reach both, and reach
66+
* nothing else — 12 of 12, precise AND complete — which is the declaration
67+
* below and why the row's verdict is withdrawn rather than the declaration.
68+
*/
69+
const DECLARED_WATCH_HINTS = ['skills/*/SKILL.md', 'skills/README.md'];
70+
71+
/**
72+
* The declaration above, held against the constants this script really reads
73+
* from. A hand-written path that agreed with the read only on the day it was
74+
* typed is the drift this idiom replaces, so the check derives what it compares
75+
* against from `SKILLS_DIR` and `README` — move either and this throws here, in
76+
* this file, instead of going quiet in a dispatch brief.
77+
*/
78+
function assertWatchHintsDeclareTheReadSurface(): void {
79+
const rel = (abs: string) => path.relative(REPO_ROOT, abs).split(path.sep).join('/');
80+
const expected = [`${rel(SKILLS_DIR)}/*/${ENTRYPOINT_FILE}`, rel(README)];
81+
if (DECLARED_WATCH_HINTS.join('|') !== expected.join('|')) {
82+
throw new Error(
83+
`build-skill-docs: the declared watch-hint population ${JSON.stringify(DECLARED_WATCH_HINTS)} no longer ` +
84+
`names what this script reads (${JSON.stringify(expected)}) — update the declaration, as a LITERAL array.`,
85+
);
86+
}
87+
// …and it stays NARROW. A hint that reached the bare root would be the
88+
// wholesale claim the worklist refused, arriving through a reword.
89+
if (DECLARED_WATCH_HINTS.some((h) => h.endsWith('/**'))) {
90+
throw new Error(
91+
`build-skill-docs: the declaration must stay narrower than the root: ${JSON.stringify(DECLARED_WATCH_HINTS)}`,
92+
);
93+
}
94+
}
95+
3296
// Marker comments delimit the generated region. MDX does not support HTML
3397
// comments (`<!-- -->`) — it needs `{/* */}` — so the syntax is per file type.
3498
type CommentStyle = 'html' | 'mdx';
@@ -78,7 +142,7 @@ interface Skill {
78142
}
79143

80144
function parseFrontmatter(name: string, label: string): Skill {
81-
const file = path.resolve(SKILLS_DIR, name, 'SKILL.md');
145+
const file = path.resolve(SKILLS_DIR, name, ENTRYPOINT_FILE);
82146
const raw = fs.readFileSync(file, 'utf-8');
83147
const parts = raw.split(/^---\s*$/m);
84148
if (parts.length < 3) throw new Error(`${name}: no YAML frontmatter`);
@@ -216,10 +280,12 @@ function spliceBlock(file: string, block: string, style: CommentStyle): string {
216280
function main() {
217281
const check = process.argv.includes('--check');
218282

283+
assertWatchHintsDeclareTheReadSurface();
284+
219285
// Catalog ⇄ DISPLAY must be in lockstep.
220286
const onDisk = fs
221287
.readdirSync(SKILLS_DIR)
222-
.filter((d) => d.startsWith('objectstack-') && fs.existsSync(path.resolve(SKILLS_DIR, d, 'SKILL.md')));
288+
.filter((d) => d.startsWith('objectstack-') && fs.existsSync(path.resolve(SKILLS_DIR, d, ENTRYPOINT_FILE)));
223289
const configured = new Set(DISPLAY.map((d) => d.name));
224290
const missing = onDisk.filter((d) => !configured.has(d));
225291
const extra = DISPLAY.filter((d) => !onDisk.includes(d.name)).map((d) => d.name);

0 commit comments

Comments
 (0)