Skip to content

Commit 2fc1195

Browse files
claude[bot]claude
andauthored
fix(devx): give check:llms-txt the population it re-derives, not the artifact it guards (#13302)
check:llms-txt verifies packages/spec/llms.txt against trees elsewhere - the *.zod.ts counts under packages/spec/src, the api-surface/ shards, the manifest exports keys, and the non-private @objectstack/* workspace set - but reached every one of them through join(PKG, ...), so the only repo-relative literal it spelled was llms.txt itself. scripts/pm/dispatch-gates.mjs derives a card's gate family by scanning each gate's source for path literals, so it could name this gate only AFTER the artifact had been edited, while every edit that falsifies it lands somewhere else. Measured on PR #13186 across two rounds of one branch: deleting a src/ schema module moved src/kernel/ 32 -> 31 and the summed total 208 -> 207, the derived family did not contain the gate, and the red reached CI instead of the local sweep. The direction is the bad one - over-matching costs a wasted run, this under-matched silently and the tool's output gave no signal. Each input is now spelled repo-relative and joined onto ROOT, so the literals stay load-bearing: the file opens exactly the paths it declares. The repo-root workspace file and the per-package manifests, which cannot be spelled as literals this file opens, are declared in the established <file>/** form that check-doc-anchors and the pm line ratchet already use. Case 21 of the gate's own self-test binds the declaration to the reads, and earned that immediately: the first draft declared only packages/, and the case caught apps/* and examples/* being workspace roots this gate also opens. It derives the roots from the real pnpm-workspace.yaml rather than repeating a list, so the next new root fails at the gate that reads it. Thirteen pinned cases in the register's self-test hold both halves - the four trees are reached, and nothing else is. Priced over the 232 commits this checkout holds: the whole declared population matches 47 of them (20.3%), where a blanket packages/** would have bought the "22 leads is the same as none" failure the derivation's own header refuses. Claude-Session: https://claude.ai/code/session_01KX8wnyjStaZcuMyAMNsy3N Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2ebfe7e commit 2fc1195

2 files changed

Lines changed: 184 additions & 5 deletions

File tree

packages/spec/scripts/check-llms-txt.ts

Lines changed: 133 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,85 @@ import { join, resolve } from 'node:path';
151151
const HERE = import.meta.dirname;
152152
const PKG = resolve(HERE, '..');
153153
const ROOT = resolve(PKG, '..', '..');
154+
155+
/**
156+
* ## The population this gate reads, SPELLED — because the derivation scans it
157+
*
158+
* `scripts/pm/dispatch-gates.mjs` derives the "local gates for this card" line
159+
* of every dispatch prompt by scanning each gate's own source for PATH
160+
* LITERALS. Every input below used to be reached with `join(PKG, ...)`, so the
161+
* only repo-relative literal this file spelled was `TARGET` — its own guarded
162+
* artifact. The derivation could therefore name this gate only once `llms.txt`
163+
* had ALREADY been edited, while every edit that actually FALSIFIES it lands
164+
* somewhere else entirely.
165+
*
166+
* #13207 measured that on PR #13186, two consecutive rounds of the same branch:
167+
* deleting a `src/` schema module moved `src/kernel/` 32 -> 31 and the
168+
* domain-summed total 208 -> 207, and the derived family did not contain this
169+
* gate. The red reached CI instead of the local sweep. The direction is the bad
170+
* one — a gate that over-matches costs a wasted run, this one under-matched
171+
* SILENTLY, and the tool's output gave no signal that a gate was omitted.
172+
*
173+
* So each input is spelled repo-relative and joined onto `ROOT`. The literals
174+
* are LOAD-BEARING: this file opens exactly these paths, which is what keeps
175+
* the declaration from drifting away from what the gate really reads. Case 21
176+
* of the self-test pins that binding in both directions.
177+
*/
154178
const TARGET = 'packages/spec/llms.txt';
155179
const SELF = 'packages/spec/scripts/check-llms-txt.ts';
180+
/** The `*.zod.ts` tree the schema-inventory counts are re-derived against. */
181+
const SRC_DIR = 'packages/spec/src';
182+
/** The checked-in shards every NAMED claim resolves against. */
183+
const API_SURFACE_DIR = 'packages/spec/api-surface';
184+
/** The manifest whose `exports` keys every SUBPATH claim resolves against. */
185+
const PKG_MANIFEST = 'packages/spec/package.json';
156186
const WORKSPACE_FILE = 'pnpm-workspace.yaml';
157187

188+
/**
189+
* The two population entries this file cannot spell as a literal it opens, and
190+
* so must DECLARE — same shape as `check-doc-anchors`' and the pm line
191+
* ratchet's root-file declarations, for the same reason.
192+
*
193+
* `pnpm-workspace.yaml/**` The gate opens the repo-ROOT workspace file
194+
* (`WORKSPACE_FILE`, right above), but a bare
195+
* top-level filename carries no separator, so
196+
* the extractor builds no hint from it — the
197+
* class it refuses wholesale, because
198+
* `package.json` / `turbo.json` basenames are
199+
* joined with a package directory in dozens of
200+
* gates. `<file>/**` is the sanctioned escape:
201+
* `collapseHint` reduces it back to that one
202+
* path, so it claims the root file and no
203+
* same-named file inside a directory.
204+
*
205+
* `packages/**\/package.json` The package-ecosystem heading is re-derived
206+
* against the real non-private `@objectstack/*`
207+
* workspace set, which this file enumerates by
208+
* READDIR over the `packages:` globs — there is
209+
* no literal per manifest to scan. Adding or
210+
* removing a workspace package moves that
211+
* denominator and falsifies the heading, in
212+
* exactly the way a `src/` deletion falsifies
213+
* the schema counts; leaving it unspelled would
214+
* keep one hole of this card's own species open.
215+
*
216+
* Priced rather than assumed, over the 232 commits this checkout holds:
217+
* `packages/**\/package.json` matches 8 of them (3.4%), and the whole declared
218+
* population matches 47 (20.3%). That is a population, not the "22 leads is the
219+
* same as none" failure a blanket `packages/**` would buy.
220+
*
221+
* ⚠️ Provenance, NOT a lookup key — nothing here is joined with `ROOT` and
222+
* stat'd. The glob spellings exist to be SCANNED; using one as a path would
223+
* make the read vanish silently, which is the disease this gate's own header
224+
* opens with.
225+
*/
226+
export const DECLARED_WATCH_HINTS = [
227+
'pnpm-workspace.yaml/**',
228+
'packages/**/package.json',
229+
'apps/*/package.json',
230+
'examples/*/package.json',
231+
];
232+
158233
/** A domain directory with no `*.zod.ts` under it is not a schema domain. */
159234
const ZOD_SUFFIX = '.zod.ts';
160235

@@ -571,7 +646,7 @@ function countZodFiles(dir: string): number {
571646
}
572647

573648
function readDomainZodCounts(): Record<string, number> {
574-
const src = join(PKG, 'src');
649+
const src = join(ROOT, SRC_DIR);
575650
if (!existsSync(src)) {
576651
console.error(`\n✗ ${SELF}: packages/spec/src/ not found.\n`);
577652
console.error(
@@ -590,7 +665,7 @@ function readDomainZodCounts(): Record<string, number> {
590665
}
591666

592667
function readApiSurface(): { entryExports: Record<string, Set<string>> } {
593-
const dir = join(PKG, 'api-surface');
668+
const dir = join(ROOT, API_SURFACE_DIR);
594669
if (!existsSync(dir)) {
595670
console.error(`\n✗ ${SELF}: packages/spec/api-surface/ not found.\n`);
596671
console.error(
@@ -623,7 +698,7 @@ function readApiSurface(): { entryExports: Record<string, Set<string>> } {
623698
* packages would shrink the denominator of the package count and make a stale
624699
* heading look correct.
625700
*/
626-
function readWorkspacePackages(): Set<string> {
701+
function readWorkspaceGlobs(): string[] {
627702
const file = join(ROOT, WORKSPACE_FILE);
628703
if (!existsSync(file)) {
629704
console.error(`\n✗ ${SELF}: ${WORKSPACE_FILE} not found at the repo root.\n`);
@@ -646,6 +721,11 @@ function readWorkspacePackages(): Set<string> {
646721
console.error(`\n✗ ${SELF}: ${WORKSPACE_FILE} \`packages:\` block is empty.\n`);
647722
process.exit(1);
648723
}
724+
return globs;
725+
}
726+
727+
function readWorkspacePackages(): Set<string> {
728+
const globs = readWorkspaceGlobs();
649729
const dirs: string[] = [];
650730
for (const glob of globs) {
651731
if (glob.endsWith('/*')) {
@@ -677,7 +757,7 @@ function readWorkspacePackages(): Set<string> {
677757
}
678758

679759
function readSubpaths(): Set<string> {
680-
const j = JSON.parse(readFileSync(join(PKG, 'package.json'), 'utf8')) as {
760+
const j = JSON.parse(readFileSync(join(ROOT, PKG_MANIFEST), 'utf8')) as {
681761
exports?: Record<string, unknown>;
682762
};
683763
return new Set(Object.keys(j.exports ?? {}));
@@ -925,12 +1005,60 @@ function selfTest(): void {
9251005
expect('empty contract table is reported', has(findings, /has a table with no rows|has 0 tables/), true);
9261006
}
9271007

1008+
// 21. The POPULATION DECLARATION is bound to what this gate really reads
1009+
// (#13207). Everything above pins the gate's verdict; this pins the
1010+
// other half — that `scripts/pm/dispatch-gates.mjs` can NAME this gate
1011+
// for the diffs that falsify it. A declaration nothing binds is how the
1012+
// two drift apart silently, which is the defect this card is about one
1013+
// level up.
1014+
{
1015+
// (a) Load-bearing: the three spelled literals are the paths this file
1016+
// opens, not decoration beside a `join(PKG, ...)` that still runs.
1017+
expect('SRC_DIR is the tree actually read', existsSync(join(ROOT, SRC_DIR)), true);
1018+
expect('API_SURFACE_DIR is the tree actually read', existsSync(join(ROOT, API_SURFACE_DIR)), true);
1019+
expect('PKG_MANIFEST is the manifest actually read', existsSync(join(ROOT, PKG_MANIFEST)), true);
1020+
// …and each still resolves where the package-relative join used to point,
1021+
// so the respelling moved no read.
1022+
expect('SRC_DIR still resolves to the package tree', join(ROOT, SRC_DIR), join(PKG, 'src'));
1023+
expect('API_SURFACE_DIR still resolves to the package tree', join(ROOT, API_SURFACE_DIR), join(PKG, 'api-surface'));
1024+
expect('PKG_MANIFEST still resolves to the package manifest', join(ROOT, PKG_MANIFEST), join(PKG, 'package.json'));
1025+
1026+
// (b) The root-file declaration names the workspace file this gate opens
1027+
// — not a second, drifting copy of that filename.
1028+
expect('the root-file declaration names WORKSPACE_FILE', DECLARED_WATCH_HINTS.includes(`${WORKSPACE_FILE}/**`), true);
1029+
1030+
// (c) The manifest declarations really cover the workspace this gate
1031+
// enumerates. A manifest declaration reaches only under its own root,
1032+
// so a workspace glob rooted anywhere undeclared would leave that part
1033+
// of the package-ecosystem denominator unspelled — the same hole as
1034+
// this card's, one root over.
1035+
//
1036+
// Derived from the REAL pnpm-workspace.yaml rather than from a list
1037+
// repeated here, so a new workspace root fails HERE, at the gate that
1038+
// reads it, instead of silently narrowing the derivation months later.
1039+
// This case has already earned that: `apps/*` and `examples/*` are
1040+
// workspace roots, and the first draft of this declaration named only
1041+
// `packages/`. Neither holds a non-private `@objectstack/*` package
1042+
// today — but this gate OPENS every manifest under them, and one
1043+
// landing there moves the denominator exactly as a `packages/` one
1044+
// does.
1045+
const globs = readWorkspaceGlobs();
1046+
const declaredRoots = new Set(DECLARED_WATCH_HINTS.map((h) => h.split('/')[0]));
1047+
const undeclared = globs.map((g) => g.split('/')[0]!).filter((r) => !declaredRoots.has(r));
1048+
expect(`every workspace root is declared (undeclared: ${undeclared.join(', ') || 'none'})`, undeclared.length, 0);
1049+
1050+
// (d) Provenance, not a lookup key: no declared spelling is a real path.
1051+
// Using one as a path would make the read vanish behind `existsSync`.
1052+
for (const h of DECLARED_WATCH_HINTS)
1053+
expect(`declaration \`${h}\` is not used as a path`, existsSync(join(ROOT, h)), false);
1054+
}
1055+
9281056
if (failures.length) {
9291057
console.error('\n✗ check-llms-txt self-test failed:\n');
9301058
for (const f of failures) console.error(f);
9311059
process.exit(1);
9321060
}
933-
console.log('✓ check-llms-txt self-test: 20 cases pass.');
1061+
console.log('✓ check-llms-txt self-test: 21 cases pass.');
9341062
}
9351063

9361064
// ---------------------------------------------------------------------------

scripts/pm/dispatch-gates.mjs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7869,6 +7869,57 @@ function selfTest() {
78697869
const anchorRootHints = extractWatchHints(readFileSync(join(ROOT, 'scripts/check-doc-anchors.mjs'), 'utf8'), 'scripts/check-doc-anchors.mjs');
78707870
t('and the doc-anchors pair claims neither instruction file', !anchorRootHints.some((h) => hintCovers(h, 'AGENTS.md') || hintCovers(h, 'CLAUDE.md')));
78717871

7872+
// A THIRD shape of the same class, and the one with the worst failure
7873+
// direction (#13207): a gate whose declared population was its OWN GUARDED
7874+
// ARTIFACT. `check:llms-txt` re-derives the claims of `packages/spec/llms.txt`
7875+
// against trees elsewhere — the `*.zod.ts` counts under `packages/spec/src`,
7876+
// the `api-surface/` shards, the manifest `exports` keys, and the non-private
7877+
// `@objectstack/*` workspace set — but reached every one of them through
7878+
// `join(PKG, ...)`, so the only literal it spelled was `llms.txt` itself.
7879+
//
7880+
// The derivation could therefore name the gate only AFTER the artifact had
7881+
// been edited, while the edits that FALSIFY it land in those other trees.
7882+
// Measured on PR #13186 across two rounds of one branch: deleting a `src/`
7883+
// schema module moved `src/kernel/` 32 -> 31 and the summed total 208 -> 207,
7884+
// the derived family did not contain the gate, and the red reached CI. That
7885+
// is UNDER-matching — silent, and invisible in the tool's own output, where an
7886+
// omitted gate looks exactly like a gate that does not apply.
7887+
//
7888+
// Read from the real gate, not a fixture: what is pinned is that the tree
7889+
// still HAS the declaration. If this gate stops reading one of these trees,
7890+
// delete the case together with the literal — never keep it green by
7891+
// re-pointing it at a tree the gate never reads.
7892+
const llmsHints = extractWatchHints(
7893+
readFileSync(join(ROOT, 'packages/spec/scripts/check-llms-txt.ts'), 'utf8'),
7894+
'packages/spec/scripts/check-llms-txt.ts',
7895+
);
7896+
const llmsReaches = (f) => llmsHints.some((h) => hintCovers(h, f));
7897+
// The reproduction, as a case: the falsifying edit alone names the gate.
7898+
t('check:llms-txt reaches the schema tree its counts are derived from', llmsReaches('packages/spec/src/kernel/cluster.zod.ts'));
7899+
// …and by a route that is NOT the artifact hint. This is the reproduction
7900+
// itself: before this declaration the only hint covering anything was
7901+
// `packages/spec/llms.txt`, so a src-only diff derived nothing.
7902+
t('and by a route that is not the guarded artifact — the #13207 reproduction', llmsHints.some((h) => h !== 'packages/spec/llms.txt' && hintCovers(h, 'packages/spec/src/kernel/cluster.zod.ts')));
7903+
t('it still reaches the artifact it guards', llmsReaches('packages/spec/llms.txt'));
7904+
t('it reaches the api-surface shards every NAMED claim resolves against', llmsReaches('packages/spec/api-surface/data.json'));
7905+
t('it reaches the manifest whose exports keys the SUBPATH claims resolve against', llmsReaches('packages/spec/package.json'));
7906+
t('it reaches the repo-root workspace file it opens', llmsReaches('pnpm-workspace.yaml'));
7907+
t('and the workspace manifests whose set is the package-ecosystem denominator', llmsReaches('packages/drivers/driver-mongodb/package.json'));
7908+
// The negative half, and the load-bearing one. A population this broad is
7909+
// one respelling away from the "22 leads is the same as none" failure the
7910+
// header prices: `packages/spec` or `packages/**` would have bought the flip
7911+
// too, and named this gate on nearly every card in the repo. These pin that
7912+
// it bought the four trees it reads and NOTHING else — including the sibling
7913+
// directories inside its own package.
7914+
t('but claims no other file in its own package', !llmsReaches('packages/spec/docs/anything.md'));
7915+
t('nor a sibling package source', !llmsReaches('packages/rest/src/analytics-dataset-dimension-gate.test.ts'));
7916+
t('nor a content page', !llmsReaches('content/docs/deployment/cli.mdx'));
7917+
t('nor an app source', !llmsReaches('apps/docs/components/ui/card.tsx'));
7918+
t('nor an example', !llmsReaches('examples/app-crm/src/objects/lead.object.ts'));
7919+
// A workspace manifest is reached; a workspace SOURCE file is not. This is
7920+
// the pair that separates `packages/**\/package.json` from `packages/**`.
7921+
t('and a package manifest is reached where its source is not', llmsReaches('packages/qa/dogfood/package.json') && !llmsReaches('packages/qa/dogfood/test/two-factor-lockout.dogfood.test.ts'));
7922+
78727923
// The DIRECTORY half of the same class (#10107). A gate whose population is a
78737924
// top-level DIRECTORY spelled as a bare word is invisible for the same reason
78747925
// a root file is — `looksPathy` finds no separator, so the extractor builds no

0 commit comments

Comments
 (0)