Skip to content

Commit c0d32cd

Browse files
claude[bot]os-steveclaude
authored
fix(gates): scan the published catalog's references/ in role-word and corpus-claim-drift (#15073)
* fix(gates): scan the published catalog's references/ in role-word and corpus-claim-drift Both gates walk ['content/docs', 'skills'] and carried `references` in a directory-NAME skip set. The reason for that entry — the spec source is the fix site for generated pages, so a finding in one names the wrong file — is true of `content/docs/references/` and of nothing under `skills/`, where the same directory name holds published catalog content: two hand-authored companions that ship verbatim to third-party projects via `npx skills add`, plus ten generator-owned index pages whose fix site is their generator. A name cannot say which tree it means, so the correct exclusion for one root was applied to the other and hid 12 of the 46 markdown files under `skills/` (26%). Both gates ran, passed, and reported nothing about the files they never opened. The exclusion moves from SKIP_DIRS to a new SKIP_SUBTREES set matched by path under the roots, so it names the generated tree it was always about. SKIP_DIRS keeps only the installed-or-generated names (node_modules, .git, dist). No carve-out follows for the generator-owned pages under `skills/` — that is the refusal check-doc-authoring.mjs already records beside those same files. Each self-test pins the population FROM THE WALK, in both directions: the walk reaches the published reference pages under `skills/`, reaches nothing under `content/docs/references/`, reaches more than those pages, every SKIP_DIRS entry is declared installed-or-generated, and every SKIP_SUBTREES entry lies under a configured root and exists. Never a typed file count — a count cannot state "the scan reaches no reference page at all". Populations, per each gate's own verdict line: check-role-word 224 -> 236 (content/docs 190 -> 190, skills 34 -> 46) check-corpus-claim-drift 224 -> 236 (content/docs 190 -> 190, skills 34 -> 46) check-corpus-claim-drift stays GREEN: none of the four VOCABULARY spellings occurs in the 12 newly visible files, so its baseline does not move. check-role-word goes RED on skills/objectstack-data/references/data-hooks.md (12 occurrences of the reserved word). That page is a hand-authored companion and is deliberately NOT edited here, and the baseline path is marked MAINTAINER-ONLY by the gate itself. The red is the dormant coverage becoming visible; the remedy is the maintainer's call. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox * chore(role-word): baseline the quoted-history residue in data-hooks.md The maintainer authorised the MAINTAINER-ONLY `--update` for this page's residue and for this PR only, after PR #15186 rewrote the page into ADR-0090 D3 vocabulary and left 3 occurrences that cannot be reworded without falsifying what they document. Both are quoted pre-rename history — D3's own named legitimate KIND: line 350 **No role list** - `session.roles` was retired in 17.0.0: it was declared but never produced, so every read was undefined line 837 Never gate this on a session claim: ctx.session?.roles?.… Naming the retired identifier is the sentence's entire content; renaming it makes the sentence false. `--update` rewrites the whole baseline from the current tree, so the result was inspected key by key rather than skimmed. Exactly one entry was added and nothing else moved: entries 43 -> 44, occurrences 120 -> 123 (delta 3) ADDED skills/objectstack-data/references/data-hooks.md: 3 REMOVED (none) CHANGED (none) check-role-word: green, 236 files across 2 roots, ledger 44 file(s) / 123 occurrence(s). check-corpus-claim-drift: green, ledger unchanged at 2 file(s). Both population pins hold: 236 walked, 12 published reference pages, 0 under the generated content/docs/references/ tree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox --------- Co-authored-by: os-steve <steve@objectstack.ai> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 5e724a2 commit c0d32cd

3 files changed

Lines changed: 267 additions & 13 deletions

File tree

scripts/check-corpus-claim-drift.mjs

Lines changed: 131 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,11 @@
115115
// ## Roots
116116
//
117117
// `content/docs` and `skills` — the corpus an AI author reads the contract from.
118-
// Generated `references/` is skipped: the spec source is the fix site there, so
119-
// a finding in a generated file names the wrong file. ⚠️ One of these roots is
118+
// The GENERATED `content/docs/references/` tree is skipped BY PATH: the spec
119+
// source is the fix site there, so a finding in one of those files names the
120+
// wrong file. `skills` is walked whole, its `references/` pages included — that
121+
// tree is published catalog content, not a generated one, and the distinction is
122+
// the SKIP_SUBTREES docblock below. ⚠️ One of these roots is
120123
// `skills/**`, a `domain:skills` surface; ownership of THIS gate stays
121124
// `domain:devx` per the #13582 triage ruling, because its subject is the factual
122125
// correctness of teaching text, not the governance of an agent instruction face.
@@ -131,7 +134,71 @@ import { join, relative, dirname } from 'node:path';
131134
import { fileURLToPath } from 'node:url';
132135

133136
const ROOTS = ['content/docs', 'skills'];
134-
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'references']);
137+
138+
// ── What the walk refuses to descend into, and in which of two shapes (#15061) ─
139+
//
140+
// SKIP_DIRS matches a directory NAME anywhere under ROOTS. That shape is only
141+
// sound for trees whose exclusion is a property of the name itself, true at
142+
// every depth and under every root: an INSTALLED or GENERATED tree where nothing
143+
// hand-written lives. `node_modules` is the same thing wherever it appears; so
144+
// are `.git` and `dist`.
145+
//
146+
// `references` was in this set and is not one of those. The sentence this gate
147+
// gave for it above — "the spec source is the fix site there, so a finding in a
148+
// generated file names the wrong file" — is true of exactly ONE of the two roots:
149+
//
150+
// content/docs/references/ 214 pages, generated from spec by
151+
// packages/spec/scripts/build-docs.ts, and
152+
// marked AUTO-GEN in AGENTS.md
153+
// skills/<name>/references/ 12 pages of published catalog: two
154+
// hand-authored companions plus ten
155+
// generator-owned index pages
156+
//
157+
// A name-keyed skip cannot tell those apart, so it took a correct exclusion for
158+
// the first root and applied it to the second, hiding 12 of the 46 markdown
159+
// files under `skills/` — 26% of the published catalog, both hand-authored
160+
// companions included (skills/objectstack-data/references/data-hooks.md and
161+
// skills/objectstack-platform/references/plugin-hooks.md). Those two ship
162+
// verbatim to third-party projects via `npx skills add`, so a stale claim there
163+
// is read by an AI author in a codebase this repo cannot see — the exact reason
164+
// `skills` is in ROOTS. A scan that cannot see a quarter of its population runs,
165+
// passes, and reports nothing about the files it never opened.
166+
//
167+
// Two sibling gates over this same catalog already carry the corrected verdict,
168+
// so this is not a fresh judgement being made here.
169+
// `check-skill-identifier-liveness.mjs` keeps `references` OUT of its skip set
170+
// and pins the difference in its self-test ("under `skills/**` those files are
171+
// hand-authored published content, unlike content/docs/references which is
172+
// generated"). `check-doc-authoring.mjs` gave its published-catalog rule a
173+
// SECOND walk rather than reuse its own skipping one, because reusing it "would
174+
// have produced a gate that runs, passes, and cannot see a ninth of the
175+
// population it exists to guard".
176+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist']);
177+
178+
// Generated subtrees, excluded by PATH under ROOTS rather than by name.
179+
//
180+
// A path states WHICH tree it excludes; a name states a property of every
181+
// directory that happens to be called that. This gate needs the first, because
182+
// its two roots disagree about `references/`.
183+
//
184+
// ⚠️ No carve-out follows for the ten generator-owned pages under `skills/`
185+
// (`references/_index.md`, `references/react-blocks.md`). That is the same
186+
// refusal `check-doc-authoring.mjs` records beside those very files — "an
187+
// exemption over a surface that no longer needs one is where the next
188+
// regeneration would smuggle one back in. A red here is fixed AT THE SPEC
189+
// SOURCE, never by hand-editing the artifact" — and the fix-site argument that
190+
// justifies the entry above is what decides it rather than weighing against it:
191+
// a contradicted claim emitted into a page by its generator is a REAL finding
192+
// whose remedy is that generator. Naming the generated tree is how the gate is
193+
// told where to send such a finding, and `content/docs/references/` is the only
194+
// tree under these roots whose fix site this gate cannot otherwise reach.
195+
//
196+
// The self-test pins both directions from the WALK's own output rather than as a
197+
// file count. A count has to be re-typed whenever a page is added, and neither
198+
// failure it must catch is a statement a count can make: "the scan reaches no
199+
// published reference page at all", and "the scan descended into the generated
200+
// tree".
201+
const SKIP_SUBTREES = new Set(['content/docs/references']);
135202
const EXTENSIONS = new Set(['.mdx', '.md']);
136203
const BASELINE_PATH = 'scripts/corpus-claim-drift-baseline.json';
137204

@@ -289,6 +356,9 @@ function walk(dir, out) {
289356
for (const e of readdirSync(dir)) {
290357
if (SKIP_DIRS.has(e)) continue;
291358
const p = join(dir, e);
359+
// Normalised before the lookup so the set is spelled once, in the POSIX
360+
// form the roots themselves are written in.
361+
if (SKIP_SUBTREES.has(p.replace(/\\/g, '/'))) continue;
292362
if (statSync(p).isDirectory()) walk(p, out);
293363
else if ([...EXTENSIONS].some((x) => e.endsWith(x))) out.push(p);
294364
}
@@ -570,6 +640,7 @@ let selfTestReachedVerdict = false;
570640
// must not red. A battery BELOW its floor means cases stopped running; the
571641
// remedy is to find what stopped registering.
572642
const SELF_TEST_BATTERIES = Object.freeze({
643+
'The scan population: which `references/` the skip means (#15061)': 5,
573644
'The table is a table, and it ships exactly one row (#13582 ruling)': 3,
574645
'Table hygiene: a claim that matches the empty string': 2,
575646
'THE genericity proof, without shipping a second word': 3,
@@ -590,7 +661,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
590661

591662
// DELETING an entry silences that battery's floor exactly as effectively as
592663
// zeroing it, so the roster's own size is pinned too.
593-
const SELF_TEST_BATTERY_FLOOR = 16;
664+
const SELF_TEST_BATTERY_FLOOR = 17;
594665

595666
// The key an assertion is filed under when no battery is open. It is not a
596667
// declared battery, so it reds by the same set difference rather than silently
@@ -622,6 +693,56 @@ function selfTest() {
622693
return Object.values(hits).reduce((n, c) => n + c, 0);
623694
};
624695

696+
// ── The scan population: which `references/` the skip means (#15061) ───────
697+
//
698+
// What the skip sets leave out IS this gate's reach, and an exclusion is
699+
// invisible in the pass line: "224 file(s) read across 2 root(s)" reads
700+
// exactly like 236 to anyone not counting. `references` sat in SKIP_DIRS for a
701+
// reason true of one ROOT and hid 26% of the other — see the SKIP_DIRS
702+
// docblock above.
703+
//
704+
// Pinned from the WALK, on the real tree, by the same function the scan below
705+
// calls — never as a hand-typed file count. A count has to be re-typed
706+
// whenever a page is added, and it cannot state either thing that actually
707+
// goes wrong. Both directions are asserted, because this gate walks a
708+
// generated root AND a published one: a walk that reaches neither `references`
709+
// tree is the defect being repaired here, and a walk that reaches both is the
710+
// opposite mistake — findings in `content/docs/references/` name the wrong
711+
// file.
712+
battery('The scan population: which `references/` the skip means (#15061)');
713+
const INSTALLED_OR_GENERATED = new Set(['node_modules', '.git', 'dist']);
714+
const walked = [];
715+
for (const r of ROOTS) walk(r, walked);
716+
const walkedRel = walked.map((f) => relative('.', f).replace(/\\/g, '/'));
717+
const publishedRefs = walkedRel.filter(
718+
(f) => f.startsWith('skills/') && f.includes('/references/'),
719+
);
720+
const generatedRefs = walkedRel.filter((f) => f.startsWith('content/docs/references/'));
721+
expect('#15061 — the walk REACHES the published catalog\'s reference pages under skills/: '
722+
+ 'hand-authored companions that ship verbatim to third parties via `npx skills add`, plus '
723+
+ 'generator-owned index pages whose fix site is their generator. Restoring the name-keyed '
724+
+ 'skip empties this while the gate itself stays green, which is the dormancy this case '
725+
+ 'exists for',
726+
publishedRefs.length > 0);
727+
expect('#15061 — and DOES NOT reach content/docs/references/, the generated tree the exclusion '
728+
+ 'was always about: a claim site there names the wrong file, because the fix site is the '
729+
+ 'spec source. This half is why the exclusion is a PATH — dropping it entirely would pass '
730+
+ 'the case above',
731+
generatedRefs.length === 0);
732+
expect('#15061 — the walk reaches more than those reference pages, so the two cases above are '
733+
+ 'judging a real population rather than passing on a coincidence',
734+
walkedRel.length > publishedRefs.length);
735+
expect('#15061 — every SKIP_DIRS entry is DECLARED installed-or-generated here: the criterion '
736+
+ 'the three surviving entries meet at every depth and under every root, and `references` '
737+
+ 'never did. A fourth entry has to be stated rather than appended',
738+
[...SKIP_DIRS].every((d) => INSTALLED_OR_GENERATED.has(d)));
739+
expect('#15061 — every SKIP_SUBTREES entry lies under a configured ROOT and EXISTS: a path '
740+
+ 'excluding a tree no root reaches, or one that has since moved, is dead configuration that '
741+
+ 'reads as coverage',
742+
[...SKIP_SUBTREES].every(
743+
(p) => ROOTS.some((r) => p === r || p.startsWith(`${r}/`)) && existsSync(p),
744+
));
745+
625746
// ── The table is a table, and it ships exactly one row (#13582 ruling) ─────
626747
//
627748
// Both halves are load-bearing and they pull in opposite directions, which is
@@ -1303,7 +1424,12 @@ function selfTest() {
13031424
process.exit(1);
13041425
}
13051426
console.log(
1306-
'OK self-test: the vocabulary is a TABLE — a synthetic row is driven through the real '
1427+
`OK self-test: the walk reaches ${walkedRel.length} markdown file(s) across the roots, `
1428+
+ `${publishedRefs.length} of them published reference pages under skills/, and `
1429+
+ `${generatedRefs.length} under the generated content/docs/references/ tree — both `
1430+
+ 'directions pinned from the WALK, never from a typed count, so a skip that empties the '
1431+
+ 'published half and one that swallows the generated half each name themselves. '
1432+
+ 'The vocabulary is a TABLE — a synthetic row is driven through the real '
13071433
+ 'engine, keyed separately in the ledger and honouring its own window — while the shipped '
13081434
+ 'table is pinned as an EXACT ENUMERATION of its four row ids in order, so the next word is '
13091435
+ 'a card rather than a drive-by. #13745\'s three rows each carry their own survey: the '

0 commit comments

Comments
 (0)