From 39227a876e8c23486b76e3e5e247fd88197e2697 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 22:44:15 +0000 Subject: [PATCH] fix(devx): the two corpus walkers skip .cache/, so a console pin bump no longer reds on objectui's files (#15557) `scripts/build-console.sh` materialises objectui at the pinned SHA into `.cache/objectui-/` -- 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. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --- scripts/check-agent-test-spelling.mjs | 53 +++++++++++++++++++- scripts/check-comment-mask-corpus.mjs | 69 ++++++++++++++++++++++++--- 2 files changed, 113 insertions(+), 9 deletions(-) diff --git a/scripts/check-agent-test-spelling.mjs b/scripts/check-agent-test-spelling.mjs index b75408ebd2..b028549f9f 100644 --- a/scripts/check-agent-test-spelling.mjs +++ b/scripts/check-agent-test-spelling.mjs @@ -351,7 +351,22 @@ const SCANNED_EXTENSIONS = new Set([ '.json', ]); -const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.turbo']); +/** + * Directories the three walks below never descend into: dependencies, build + * output -- and `.cache`, which holds ANOTHER REPOSITORY. + * + * `scripts/build-console.sh` materialises objectui at the pinned SHA into + * `.cache/objectui-/`, a whole foreign checkout that every console pin + * bump MUST create because the console cannot be built without it. All three + * walks reached into it: `scannedFiles`'s loose walk read objectui's own + * `AGENTS.md` -- instructions objectui writes for objectui's agents -- and red + * this gate on it, and `deriveVitestScripts` read its manifests, so a foreign + * package's script names silently widened the set of names this gate judges in + * OUR corpus. CI never saw either, because the lint job does not build the + * console; the red landed on whoever bumped the pin, over a file their diff + * could not have touched. + */ +const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.turbo', '.cache']); /* ────────────────────────────── the classifier ────────────────────────────── */ @@ -847,11 +862,12 @@ const SELF_TEST_BATTERIES = Object.freeze({ 'the declared lists cannot quietly become mute buttons': 4, 'the dispatch-gates declaration — both directions, derived from the scan roots': 8, 'the derivation reads THIS workspace, and reads it non-empty': 4, + 'a vendored checkout under .cache/ is not our corpus — both directions': 5, }); // DELETING an entry silences that battery's floor exactly as effectively as // zeroing it, so the roster's own size is pinned too. -const SELF_TEST_BATTERY_FLOOR = 12; +const SELF_TEST_BATTERY_FLOOR = 13; // The key an assertion is filed under when no battery is open. It is not a // declared battery, so it reds by the same set difference rather than silently @@ -940,6 +956,39 @@ function selfTest() { rmSync(redTree, { recursive: true, force: true }); } + console.log('a vendored checkout under .cache/ is not our corpus — both directions'); + battery('a vendored checkout under .cache/ is not our corpus — both directions'); + // The plant is objectui's OWN instruction, in objectui's own AGENTS.md, in the + // place `scripts/build-console.sh` puts it. Both directions are asserted from + // the SAME BYTES: under `.cache/` the sweep is clean and the walk never + // reaches the file, and one directory higher the identical text still reds — + // an exclusion that also silenced the loose walk would be a mute button, not + // a skip. The manifest case covers the second walk: a foreign package's + // script names must not widen the set of names judged in our corpus. + const foreignInstruction = 'Run one package with `pnpm --filter test -- --run`.\n'; + const cachedTree = makeFixtureTree( + baseFixtureFiles({ + '.cache/objectui-pin/AGENTS.md': foreignInstruction, + '.cache/objectui-pin/package.json': JSON.stringify({ name: 'foreign', scripts: { 'test:foreign-only': 'vitest run' } }), + }), + ); + try { + t('a planted .cache/ checkout leaves the sweep CLEAN', run(cachedTree, () => {}), EXIT_CLEAN); + t('the walk never reaches it', scannedFiles(cachedTree).filter((f) => f.startsWith('.cache')), []); + t('and its manifests do not widen the vitest-script derivation', deriveVitestScripts(cachedTree).names.has('test:foreign-only'), false); + } finally { + rmSync(cachedTree, { recursive: true, force: true }); + } + + const vendoredTree = makeFixtureTree(baseFixtureFiles({ 'vendor/objectui-pin/AGENTS.md': foreignInstruction })); + try { + const vendoredLines = []; + t('the SAME bytes one directory outside .cache/ still RED', run(vendoredTree, (s) => vendoredLines.push(s)), EXIT_VIOLATIONS); + t('...and the finding names that file', vendoredLines.join('\n').includes('vendor/objectui-pin/AGENTS.md'), true); + } finally { + rmSync(vendoredTree, { recursive: true, force: true }); + } + console.log('the same tree WITHOUT the plant is green — the red above is the plant, not the fixture'); battery('the same tree WITHOUT the plant is green — the red above is the plant, not the fixture'); const greenTree = makeFixtureTree(baseFixtureFiles()); diff --git a/scripts/check-comment-mask-corpus.mjs b/scripts/check-comment-mask-corpus.mjs index 433e0c8419..a2e698b94c 100644 --- a/scripts/check-comment-mask-corpus.mjs +++ b/scripts/check-comment-mask-corpus.mjs @@ -125,8 +125,9 @@ // 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. -import { readdirSync, readFileSync } from 'node:fs'; -import { dirname, extname, join, relative, resolve } from 'node:path'; +import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, extname, join, relative, resolve, sep } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { isEntrypoint } from './invoked-as.mjs'; @@ -137,11 +138,24 @@ const REPO_ROOT = resolve(HERE, '..'); export const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs', '.cjs', '.jsx']); /** - * Directories that hold dependencies or build output rather than source. Same - * list `js-comment-mask.mjs`'s header states, so the prose and the instrument - * cannot drift apart. Every package in this tree builds to `dist`. + * Directories that hold dependencies or build output rather than source -- plus + * one that holds ANOTHER REPOSITORY. Every package in this tree builds to + * `dist`. `js-comment-mask.mjs`'s header quotes the six this walk started from + * as part of the 2026-08-21 measurement it records; the set below is the + * instrument and has grown past that sentence since (`.git`, and now `.cache`), + * so this declaration is the list, and the header is the history. + * + * `.cache` is where `scripts/build-console.sh` materialises objectui at the + * pinned SHA: a whole foreign checkout, gitignored, that every console pin bump + * MUST create because the console cannot be built without it. Walked, it put + * ~4,300 files this repo does not author into the corpus and reported one of + * them as a disagreement -- against a masker objectui's pages have no stake in. + * The failure text below is correct for OUR sources and wrong for those: it + * sends the reader to pin a shape in `js-comment-mask.mjs`, which is the last + * thing a pin bump should be editing. CI never saw it, because the lint job + * does not build the console; every instance landed on a person instead. */ -export const SKIPPED_DIRECTORIES = new Set(['node_modules', 'dist', '.next', 'build', '.turbo', 'coverage', '.git']); +export const SKIPPED_DIRECTORIES = new Set(['node_modules', 'dist', '.next', 'build', '.turbo', 'coverage', '.git', '.cache']); /** Below this, the corpus is not a corpus -- see the header. */ export const CORPUS_FLOOR = 1000; @@ -449,7 +463,7 @@ async function main(argv) { // not red. A battery BELOW its floor means cases stopped running; the remedy is // to find what stopped registering. const SELF_TEST_BATTERIES = Object.freeze({ - 'check-comment-mask-corpus self-test': 12, + 'check-comment-mask-corpus self-test': 17, }); // DELETING an entry silences that battery's floor exactly as effectively as @@ -611,6 +625,47 @@ async function runSelfTestCases(parse) { ok(`the corpus walk finds at least ${CORPUS_FLOOR} files in this tree`, collectSources().length >= CORPUS_FLOOR); ok('...and every path it returns carries a known source extension', collectSources().every((file) => SOURCE_EXTENSIONS.has(extname(file)))); + // ── The walk's exclusions, on a REAL tree, in both directions ───────────── + // + // `SKIPPED_DIRECTORIES` is the kind of declaration that reads as obviously + // correct and is measured by nothing: for `.cache` it was wrong for as long + // as `scripts/build-console.sh` has existed, and the only reader who ever + // found out was an operator staring at a red gate over someone else's file. + // So the exclusion is proven the way the corpus is judged -- by walking a + // directory on disk. The SAME BYTES are planted twice, inside `.cache` and + // outside it, against a masker that disagrees with the parser on them: the + // copy outside reds, the copy inside never enters the corpus at all, and the + // only variable between the two is location. + // + // ⚠️ These cases run on the production sweep path too (`main()` calls this + // body on every sweep), which is deliberate: what they hold is a property of + // the corpus that sweep is about to report on. The fixture is two files in a + // temp dir, removed in `finally`. + const plantedSource = 'export const Probe = () => null;\n'; + const fixtureRoot = mkdtempSync(join(tmpdir(), 'comment-mask-corpus-')); + try { + const outsidePath = join('src', 'probe.tsx'); + const insidePath = join('.cache', 'objectui-pin', 'src', 'probe.tsx'); + for (const relPath of [outsidePath, insidePath]) { + mkdirSync(dirname(join(fixtureRoot, relPath)), { recursive: true }); + writeFileSync(join(fixtureRoot, relPath), plantedSource, 'utf8'); + } + + const collected = collectSources(fixtureRoot).map((file) => relative(fixtureRoot, file)); + ok('the walk collects a planted source that sits outside .cache', collected.includes(outsidePath)); + ok('...and collects NOTHING under .cache', collected.every((file) => !file.split(sep).includes('.cache'))); + + const swept = sweep({ root: fixtureRoot, parse, scan: flagEverything }); + ok('the copy outside .cache DISAGREES -- the plant is genuinely red', swept.disagreements.length === 1 && swept.disagreements[0].file === outsidePath); + ok('...and the sweep judged exactly the one file it walked', swept.files.length === 1); + // Excluded by LOCATION, not because those bytes happen to agree: compared + // directly, the identical copy under `.cache` disagrees just as loudly. + const wouldDisagree = compareFile(join(fixtureRoot, insidePath), plantedSource, { scan: flagEverything, parse }); + ok('...while the identical bytes under .cache would have disagreed if walked', wouldDisagree.overMasks > 0); + } finally { + rmSync(fixtureRoot, { recursive: true, force: true }); + } + SELF_TEST_CASE_COUNT = cases.length; for (const testCase of cases) if (!testCase.condition) failures.push(testCase.label); return { failures, cases };