Skip to content

Commit c7d5e1d

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-15145-verify-test-typecheck-wiring
2 parents c3ca054 + 4444885 commit c7d5e1d

5 files changed

Lines changed: 300 additions & 28 deletions

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 };

scripts/check-registry-log-declared.mjs

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync,
127127
import { dirname, join, resolve, sep } from 'node:path';
128128
import { tmpdir } from 'node:os';
129129
import { fileURLToPath } from 'node:url';
130-
import { blank, scanSource } from './js-comment-mask.mjs';
130+
import { blank, maskComments, scanSource } from './js-comment-mask.mjs';
131131
import { isEntrypoint } from './invoked-as.mjs';
132132
import { workspacePackageDirs } from './check-console-intercept-disarm.mjs';
133133

@@ -178,19 +178,21 @@ const REMEDY = ` // #13517: quiet the registry's per-item registration chatte
178178
// default. Enforced by scripts/check-registry-log-declared.mjs.
179179
env: { OS_REGISTRY_LOG: 'warn' },`;
180180

181-
/** Comments blanked, offsets kept. Import specifiers survive — S2/S3 need them. */
182-
function maskComments(source) {
183-
return blank(source, scanSource(source).comment);
184-
}
185-
186181
/**
187182
* Comments AND string/template/regex content blanked, offsets kept. Used where
188183
* the signal is a bare CODE position (`new SchemaRegistry(`, a property key), so
189184
* a spelling inside prose or a template literal can never satisfy it.
190185
*
191-
* Both masks preserve offsets, so a range brace-matched on the code mask indexes
192-
* the comment mask identically — which is how the level VALUE (a string, blanked
193-
* by this mask) is read out of a block located with it.
186+
* It COMPOSES the shared scanner — `scanSource`'s `comment` and `literal` flags
187+
* OR-ed through `blank` — and carries no scanning logic of its own; it stays
188+
* local only because `js-comment-mask.mjs` publishes no comments+literals
189+
* projection yet, and hoisting one waits on a follow-up card.
190+
*
191+
* The imported `maskComments` (comments blanked, string/template/regex content
192+
* INTACT — S2/S3 read import specifiers out of it) and this mask both preserve
193+
* offsets, so a range brace-matched on the code mask indexes the comment mask
194+
* identically — which is how the level VALUE (a string, blanked by this mask)
195+
* is read out of a block located with it.
194196
*/
195197
function maskCode(source) {
196198
const { comment, literal } = scanSource(source);
@@ -563,6 +565,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
563565
'a package that never runs vitest is ignored entirely': 1,
564566
'prose naming the key does not satisfy the check': 1,
565567
'prose naming bootStack does not SELECT the package (the packages/cli shape)': 1,
568+
'a code signal spelled only in a string or a comment does not SELECT': 1,
566569
'a level the engine does not recognise is RED': 1,
567570
'the key outside any env block does not count': 1,
568571
'S2: importing bootStack from @objectstack/verify selects': 1,
@@ -578,7 +581,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
578581
// zeroing it, so the roster's own size is pinned too. This pin is also half of
579582
// the duplicate-label refusal: two rows sharing a label collapse to ONE key in
580583
// the literal above, so the roster falls below this number.
581-
const SELF_TEST_BATTERY_FLOOR = 15;
584+
const SELF_TEST_BATTERY_FLOOR = 16;
582585

583586
// The key an assertion is filed under when a row carries no label. It is not a
584587
// declared battery, so it reds by the same set difference rather than silently
@@ -657,6 +660,33 @@ function selfTest() {
657660
expectFindings: 0,
658661
expectSelected: 0,
659662
},
663+
{
664+
// The other direction of the row above, for the CODE mask: a signal
665+
// spelled inside a string literal or a comment must not select, and the
666+
// same spelling in real code still must. Package `a` carries both
667+
// non-code spellings, package `b` the code one, so a mask that stopped
668+
// blanking either span shows up as a SECOND selected package rather than
669+
// as a silent widening of the population.
670+
name: 'a code signal spelled only in a string or a comment does not SELECT',
671+
packages: {
672+
a: {
673+
'package.json': TEST_MANIFEST,
674+
'test/x.test.ts':
675+
`// new SchemaRegistry() is what this file describes; bootStack too.\n`
676+
+ `/* const described = new SchemaRegistry(); */\n`
677+
+ `export const doc = 'new SchemaRegistry() quoted, and bootStack, never called';\n`,
678+
'vitest.config.ts': `export default { test: { globals: true } };\n`,
679+
},
680+
b: {
681+
'package.json': TEST_MANIFEST,
682+
'test/x.test.ts': BOOTS,
683+
'vitest.config.ts': `export default { test: { globals: true } };\n`,
684+
},
685+
},
686+
expectFindings: 1,
687+
expectSelected: 1,
688+
expectText: 'S1 constructs a SchemaRegistry',
689+
},
660690
{
661691
name: 'a level the engine does not recognise is RED',
662692
packages: {

0 commit comments

Comments
 (0)