From ffb0f544a31451a2b7d25b704aec61c22b4364bd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 19:25:03 +0000 Subject: [PATCH] fix(tooling): walk the references/ subtrees in check:skill-frame-sync The anti-dormancy scan skipped every directory named `references`, an idiom carried over from the prose gates over `content/docs`, where that directory is generated from spec. This gate never walks `content/docs`; under its two scan roots (`.claude`, `skills`) the same name holds hand-written operative prose, so the skip hid 32 of 72 markdown files - 44% of the population - from the half of the gate that exists to notice a new copy of the decision frame. Drop `references` from SCAN_SKIP_DIRS, with the reason it was there and why it does not hold recorded on the set. No narrowed carve-out for the generated pages under `skills/**`: a fingerprint hit there would mean the generator emits a copy, which is a real finding whose fix site is the generator. Pin the population in the self-test from the walk's own output rather than as a count, so restoring the skip goes red and names itself. Before: 2 copies isomorphic, 40 markdown files scanned. After: 2 copies isomorphic, 72 markdown files scanned, 0 undeclared copies. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox --- scripts/check-skill-frame-sync.mjs | 91 +++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/scripts/check-skill-frame-sync.mjs b/scripts/check-skill-frame-sync.mjs index 7025f97e80..53f2d98858 100644 --- a/scripts/check-skill-frame-sync.mjs +++ b/scripts/check-skill-frame-sync.mjs @@ -342,7 +342,57 @@ const SCAN_ROOTS = ['.claude', 'skills']; * than re-spelled, so re-scoping a root cannot leave this describing the old one. */ const ROOT_DIR_WATCH_HINTS = ['skills/**']; -const SCAN_SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'references']); +/** + * Directories the anti-dormancy walk refuses to descend into: trees that are + * INSTALLED or GENERATED, where nothing hand-written lives. + * + * ## Why `references` is NOT one of them (#15056) + * + * It was, until this edit, carried over from the idiom the repo's prose gates + * share — and that idiom states its reason outright, in + * `check-corpus-claim-drift.mjs`: + * + * > Generated `references/` is skipped: the spec source is the fix site + * > there, so a finding in a generated file names the wrong file. + * + * That sentence is about `content/docs/references/`, which IS generated from + * spec. This gate never walks `content/docs`. Under SCAN_ROOTS the same + * directory NAME holds hand-written operative prose: the PM protocol files + * under `.claude/skills/pm-dispatch/references/` — `decision-analysis.md` + * among them, whose subject matter IS escalation analysis and so is the single + * likeliest place for a third copy to be pasted — and the published reference + * companions that ship verbatim to third parties via `npx skills add`, the + * exact distribution path this gate's header gives as the reason the published + * copy is watched at all. So the reason did not hold here, and the skip hid 32 + * of 72 markdown files (44%) from the half of this gate whose entire job is to + * notice a copy of the frame appearing somewhere new. A scan that cannot see + * 44% of its population is the #4690 anti-pattern one directory level down: it + * runs, it passes, and it reports nothing about the files it never opened. + * + * Two sibling gates over this same corpus already carry the corrected verdict. + * `check-skill-identifier-liveness.mjs` keeps `references` OUT of its skip set + * and pins the difference in its self-test ("under `skills/**` those files are + * hand-authored published content, unlike content/docs/references which is + * generated"). `check-doc-authoring.mjs` gave its published-catalog rule a + * SECOND walk rather than reuse the skipping one, because reusing it "would + * have produced a gate that runs, passes, and cannot see a ninth of the + * population it exists to guard". + * + * Hence no narrowed carve-out for the generated pages under `skills/**` either + * (`references/_index.md`, `references/react-blocks.md`). That is the same + * refusal `check-doc-authoring.mjs` records beside them — "an exemption over a + * surface that no longer needs one is where the next regeneration would smuggle + * one back in. A red here is fixed AT THE SPEC SOURCE, never by hand-editing + * the artifact" — and it holds a fortiori here: a fingerprint hit in a + * generated page would mean the GENERATOR is emitting a copy of the frame, + * which is a real finding whose fix site is the generator, not a false one. + * + * The self-test pins this from the WALK's own output rather than as a file + * count: a count has to be re-typed whenever a page is added, and the failure + * it must catch — "the scan reaches no reference page at all" — is not a + * statement a count can make. + */ +const SCAN_SKIP_DIRS = new Set(['node_modules', '.git', 'dist']); const SCAN_EXTENSIONS = ['.md', '.mdx']; function matchAllOf(text, source) { @@ -935,11 +985,48 @@ function selfTest() { for (const f of declFailures) console.error(` ✗ dispatch-gates declaration: ${f}`); failed += declFailures.length; + // ── The scan population (#15056) ────────────────────────────────────────── + // + // What SCAN_SKIP_DIRS leaves out IS the anti-dormancy half's reach, and a + // skip is invisible in the pass line: "40 markdown files scanned" reads + // exactly like 72 to anyone not counting. `references` sat in that set for + // reasons belonging to a corpus this gate does not walk, and hid 44% of the + // population — see the SCAN_SKIP_DIRS docblock. + // + // Pinned from the WALK, on the real tree, by the same call `main()` makes — + // never as a hand-typed file count. A count has to be re-typed whenever a + // page is added, and it cannot state the thing that actually went wrong: + // "the scan reaches no reference page at all". + const INSTALLED_OR_GENERATED = new Set(['node_modules', '.git', 'dist']); + const popFailures = []; + let popCases = 0; + const pop = (label, ok) => { popCases += 1; if (!ok) popFailures.push(label); }; + const walked = SCAN_ROOTS.flatMap((root) => walkMarkdown(root, [])); + const inReferences = walked.filter((f) => /(^|[\\/])references[\\/]/.test(f.file)); + pop('the walk reaches the `references/` subtrees under SCAN_ROOTS — hand-written ' + + 'operative prose, not an installed or generated tree. Restoring the skip empties ' + + 'this and every gate stays green, which is the defect this case exists for', + inReferences.length > 0); + pop('the walk reaches more than those subtrees, so the case above is judging a real ' + + 'population rather than passing on a coincidence', + walked.length > inReferences.length); + pop('every SCAN_SKIP_DIRS entry is DECLARED installed-or-generated here — the ' + + 'criterion the three surviving entries meet and `references` never did, so a ' + + 'fourth entry has to be stated rather than appended', + [...SCAN_SKIP_DIRS].every((d) => INSTALLED_OR_GENERATED.has(d))); + for (const f of popFailures) console.error(` ✗ scan population: ${f}`); + failed += popFailures.length; + if (failed > 0) { console.error(`\n✗ check-skill-frame-sync self-test failed (${failed} case(s)).`); process.exit(1); } - console.log(`✓ check-skill-frame-sync self-test: ${cases.length} cases pass, plus 5 dispatch-gates declaration cases.`); + console.log( + `✓ check-skill-frame-sync self-test: ${cases.length} cases pass, plus 5 ` + + `dispatch-gates declaration cases and ${popCases} scan-population cases ` + + `(${walked.length} markdown files walked under the scan roots, ` + + `${inReferences.length} of them inside a references/ directory).`, + ); selfTestReachedVerdict = true; }