Skip to content

Commit 4e4d79d

Browse files
claude[bot]claude
andauthored
fix(devx): the two corpus walkers skip .cache/, so a console pin bump no longer reds on objectui's files (#15557) (#15596)
`scripts/build-console.sh` materialises objectui at the pinned SHA into `.cache/objectui-<sha>/` -- a whole foreign checkout, gitignored, that every console pin bump must create. Both repo-wide corpus walkers descended into it: `check-comment-mask-corpus` judged objectui's sources against this repo's masker and told the operator to pin a shape in `js-comment-mask.mjs`, and `check-agent-test-spelling` red on objectui's own AGENTS.md while its `deriveVitestScripts` walk let a foreign manifest widen the script names it judges here. CI never saw either, because the lint job does not build the console. Both walkers now carry `.cache` in the directory skip set they already keep, next to `node_modules` -- the same "not our source" class. Each self-test gains a control that plants the SAME BYTES inside and outside `.cache/` on a real temp tree: the copy inside never enters the corpus, the copy outside still reds, so the exclusion cannot become a mute button. Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk Co-authored-by: Claude <noreply@anthropic.com>
1 parent 080834e commit 4e4d79d

2 files changed

Lines changed: 113 additions & 9 deletions

File tree

scripts/check-agent-test-spelling.mjs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,22 @@ const SCANNED_EXTENSIONS = new Set([
351351
'.json',
352352
]);
353353

354-
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.turbo']);
354+
/**
355+
* Directories the three walks below never descend into: dependencies, build
356+
* output -- and `.cache`, which holds ANOTHER REPOSITORY.
357+
*
358+
* `scripts/build-console.sh` materialises objectui at the pinned SHA into
359+
* `.cache/objectui-<sha>/`, a whole foreign checkout that every console pin
360+
* bump MUST create because the console cannot be built without it. All three
361+
* walks reached into it: `scannedFiles`'s loose walk read objectui's own
362+
* `AGENTS.md` -- instructions objectui writes for objectui's agents -- and red
363+
* this gate on it, and `deriveVitestScripts` read its manifests, so a foreign
364+
* package's script names silently widened the set of names this gate judges in
365+
* OUR corpus. CI never saw either, because the lint job does not build the
366+
* console; the red landed on whoever bumped the pin, over a file their diff
367+
* could not have touched.
368+
*/
369+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.turbo', '.cache']);
355370

356371
/* ────────────────────────────── the classifier ────────────────────────────── */
357372

@@ -847,11 +862,12 @@ const SELF_TEST_BATTERIES = Object.freeze({
847862
'the declared lists cannot quietly become mute buttons': 4,
848863
'the dispatch-gates declaration — both directions, derived from the scan roots': 8,
849864
'the derivation reads THIS workspace, and reads it non-empty': 4,
865+
'a vendored checkout under .cache/ is not our corpus — both directions': 5,
850866
});
851867

852868
// DELETING an entry silences that battery's floor exactly as effectively as
853869
// zeroing it, so the roster's own size is pinned too.
854-
const SELF_TEST_BATTERY_FLOOR = 12;
870+
const SELF_TEST_BATTERY_FLOOR = 13;
855871

856872
// The key an assertion is filed under when no battery is open. It is not a
857873
// declared battery, so it reds by the same set difference rather than silently
@@ -940,6 +956,39 @@ function selfTest() {
940956
rmSync(redTree, { recursive: true, force: true });
941957
}
942958

959+
console.log('a vendored checkout under .cache/ is not our corpus — both directions');
960+
battery('a vendored checkout under .cache/ is not our corpus — both directions');
961+
// The plant is objectui's OWN instruction, in objectui's own AGENTS.md, in the
962+
// place `scripts/build-console.sh` puts it. Both directions are asserted from
963+
// the SAME BYTES: under `.cache/` the sweep is clean and the walk never
964+
// reaches the file, and one directory higher the identical text still reds —
965+
// an exclusion that also silenced the loose walk would be a mute button, not
966+
// a skip. The manifest case covers the second walk: a foreign package's
967+
// script names must not widen the set of names judged in our corpus.
968+
const foreignInstruction = 'Run one package with `pnpm --filter <pkg> test -- --run`.\n';
969+
const cachedTree = makeFixtureTree(
970+
baseFixtureFiles({
971+
'.cache/objectui-pin/AGENTS.md': foreignInstruction,
972+
'.cache/objectui-pin/package.json': JSON.stringify({ name: 'foreign', scripts: { 'test:foreign-only': 'vitest run' } }),
973+
}),
974+
);
975+
try {
976+
t('a planted .cache/ checkout leaves the sweep CLEAN', run(cachedTree, () => {}), EXIT_CLEAN);
977+
t('the walk never reaches it', scannedFiles(cachedTree).filter((f) => f.startsWith('.cache')), []);
978+
t('and its manifests do not widen the vitest-script derivation', deriveVitestScripts(cachedTree).names.has('test:foreign-only'), false);
979+
} finally {
980+
rmSync(cachedTree, { recursive: true, force: true });
981+
}
982+
983+
const vendoredTree = makeFixtureTree(baseFixtureFiles({ 'vendor/objectui-pin/AGENTS.md': foreignInstruction }));
984+
try {
985+
const vendoredLines = [];
986+
t('the SAME bytes one directory outside .cache/ still RED', run(vendoredTree, (s) => vendoredLines.push(s)), EXIT_VIOLATIONS);
987+
t('...and the finding names that file', vendoredLines.join('\n').includes('vendor/objectui-pin/AGENTS.md'), true);
988+
} finally {
989+
rmSync(vendoredTree, { recursive: true, force: true });
990+
}
991+
943992
console.log('the same tree WITHOUT the plant is green — the red above is the plant, not the fixture');
944993
battery('the same tree WITHOUT the plant is green — the red above is the plant, not the fixture');
945994
const greenTree = makeFixtureTree(baseFixtureFiles());

scripts/check-comment-mask-corpus.mjs

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,9 @@
125125

126126
// dispatch-gates: whole-tree-population -- `collectSources` walks every authored JS/TS file from the repo root, so the corpus is the whole tree; the one literal below names the masker this gate exercises, not the files it reads.
127127

128-
import { readdirSync, readFileSync } from 'node:fs';
129-
import { dirname, extname, join, relative, resolve } from 'node:path';
128+
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
129+
import { tmpdir } from 'node:os';
130+
import { dirname, extname, join, relative, resolve, sep } from 'node:path';
130131
import { fileURLToPath, pathToFileURL } from 'node:url';
131132
import { isEntrypoint } from './invoked-as.mjs';
132133

@@ -137,11 +138,24 @@ const REPO_ROOT = resolve(HERE, '..');
137138
export const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs', '.cjs', '.jsx']);
138139

139140
/**
140-
* Directories that hold dependencies or build output rather than source. Same
141-
* list `js-comment-mask.mjs`'s header states, so the prose and the instrument
142-
* cannot drift apart. Every package in this tree builds to `dist`.
141+
* Directories that hold dependencies or build output rather than source -- plus
142+
* one that holds ANOTHER REPOSITORY. Every package in this tree builds to
143+
* `dist`. `js-comment-mask.mjs`'s header quotes the six this walk started from
144+
* as part of the 2026-08-21 measurement it records; the set below is the
145+
* instrument and has grown past that sentence since (`.git`, and now `.cache`),
146+
* so this declaration is the list, and the header is the history.
147+
*
148+
* `.cache` is where `scripts/build-console.sh` materialises objectui at the
149+
* pinned SHA: a whole foreign checkout, gitignored, that every console pin bump
150+
* MUST create because the console cannot be built without it. Walked, it put
151+
* ~4,300 files this repo does not author into the corpus and reported one of
152+
* them as a disagreement -- against a masker objectui's pages have no stake in.
153+
* The failure text below is correct for OUR sources and wrong for those: it
154+
* sends the reader to pin a shape in `js-comment-mask.mjs`, which is the last
155+
* thing a pin bump should be editing. CI never saw it, because the lint job
156+
* does not build the console; every instance landed on a person instead.
143157
*/
144-
export const SKIPPED_DIRECTORIES = new Set(['node_modules', 'dist', '.next', 'build', '.turbo', 'coverage', '.git']);
158+
export const SKIPPED_DIRECTORIES = new Set(['node_modules', 'dist', '.next', 'build', '.turbo', 'coverage', '.git', '.cache']);
145159

146160
/** Below this, the corpus is not a corpus -- see the header. */
147161
export const CORPUS_FLOOR = 1000;
@@ -449,7 +463,7 @@ async function main(argv) {
449463
// not red. A battery BELOW its floor means cases stopped running; the remedy is
450464
// to find what stopped registering.
451465
const SELF_TEST_BATTERIES = Object.freeze({
452-
'check-comment-mask-corpus self-test': 12,
466+
'check-comment-mask-corpus self-test': 17,
453467
});
454468

455469
// DELETING an entry silences that battery's floor exactly as effectively as
@@ -611,6 +625,47 @@ async function runSelfTestCases(parse) {
611625
ok(`the corpus walk finds at least ${CORPUS_FLOOR} files in this tree`, collectSources().length >= CORPUS_FLOOR);
612626
ok('...and every path it returns carries a known source extension', collectSources().every((file) => SOURCE_EXTENSIONS.has(extname(file))));
613627

628+
// ── The walk's exclusions, on a REAL tree, in both directions ─────────────
629+
//
630+
// `SKIPPED_DIRECTORIES` is the kind of declaration that reads as obviously
631+
// correct and is measured by nothing: for `.cache` it was wrong for as long
632+
// as `scripts/build-console.sh` has existed, and the only reader who ever
633+
// found out was an operator staring at a red gate over someone else's file.
634+
// So the exclusion is proven the way the corpus is judged -- by walking a
635+
// directory on disk. The SAME BYTES are planted twice, inside `.cache` and
636+
// outside it, against a masker that disagrees with the parser on them: the
637+
// copy outside reds, the copy inside never enters the corpus at all, and the
638+
// only variable between the two is location.
639+
//
640+
// ⚠️ These cases run on the production sweep path too (`main()` calls this
641+
// body on every sweep), which is deliberate: what they hold is a property of
642+
// the corpus that sweep is about to report on. The fixture is two files in a
643+
// temp dir, removed in `finally`.
644+
const plantedSource = 'export const Probe = () => null;\n';
645+
const fixtureRoot = mkdtempSync(join(tmpdir(), 'comment-mask-corpus-'));
646+
try {
647+
const outsidePath = join('src', 'probe.tsx');
648+
const insidePath = join('.cache', 'objectui-pin', 'src', 'probe.tsx');
649+
for (const relPath of [outsidePath, insidePath]) {
650+
mkdirSync(dirname(join(fixtureRoot, relPath)), { recursive: true });
651+
writeFileSync(join(fixtureRoot, relPath), plantedSource, 'utf8');
652+
}
653+
654+
const collected = collectSources(fixtureRoot).map((file) => relative(fixtureRoot, file));
655+
ok('the walk collects a planted source that sits outside .cache', collected.includes(outsidePath));
656+
ok('...and collects NOTHING under .cache', collected.every((file) => !file.split(sep).includes('.cache')));
657+
658+
const swept = sweep({ root: fixtureRoot, parse, scan: flagEverything });
659+
ok('the copy outside .cache DISAGREES -- the plant is genuinely red', swept.disagreements.length === 1 && swept.disagreements[0].file === outsidePath);
660+
ok('...and the sweep judged exactly the one file it walked', swept.files.length === 1);
661+
// Excluded by LOCATION, not because those bytes happen to agree: compared
662+
// directly, the identical copy under `.cache` disagrees just as loudly.
663+
const wouldDisagree = compareFile(join(fixtureRoot, insidePath), plantedSource, { scan: flagEverything, parse });
664+
ok('...while the identical bytes under .cache would have disagreed if walked', wouldDisagree.overMasks > 0);
665+
} finally {
666+
rmSync(fixtureRoot, { recursive: true, force: true });
667+
}
668+
614669
SELF_TEST_CASE_COUNT = cases.length;
615670
for (const testCase of cases) if (!testCase.condition) failures.push(testCase.label);
616671
return { failures, cases };

0 commit comments

Comments
 (0)