Skip to content

Commit c97edee

Browse files
os-steveclaude
andauthored
fix(tooling): make check:type-source-resolution per-program, not per-package (#11922)
The gate built each package's program from `join(pkg.dir, 'tsconfig.json')` and nothing else, so every other tsc program in the package was outside its declared population. That is not an exotic case here: the repair this repo PRESCRIBES for a hidden test layer -- AGENTS.md and check-type-check-coverage's TESTS_COVERED docs -- is a SIBLING `tsconfig.test.json` named in the `typecheck` script, and CI runs it on every PR. Following the house pattern was therefore what made an exposure invisible. Measured on packages/triggers/trigger-record-change before the change, the same 7 test files with the same four dist-resolved type imports: through the BUILD config exit 1, "NEW dist-resolved type import(s): driver-sql, formula, objectql, service-automation" through the SIBLING config exit 0, count unchanged The population is now every `tsconfig*.json` a package's `typecheck` script NAMES -- not every one on disk. `packages/cli/tsconfig.build.json` is present and named by nothing, and stays out: a gate that measures a program nobody runs looks stronger while reading as covered. The predicate is `configsNamedByTypecheck`, extracted from check-type-check-coverage.mjs into scripts/typecheck-configs.mjs so both gates share one answer; its cases moved with it and both gates fold them in. Registry re-baseline, both figures from `--list` on the same checkout: before 77 programs / 77 packages, 51 entries, 221 package-dep pairs after 93 programs / 77 packages, 54 entries, 233 package-dep pairs Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx Co-authored-by: Claude <noreply@anthropic.com>
1 parent 18dea75 commit c97edee

3 files changed

Lines changed: 625 additions & 182 deletions

File tree

scripts/check-type-check-coverage.mjs

Lines changed: 18 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,16 @@ import {
417417
selfTest as workspaceEnumeratorSelfTest,
418418
workspacePackageDirs,
419419
} from './workspace-enumerator.mjs';
420+
// `typecheck`-script -> tsconfig program set. Shared with
421+
// `check-type-source-resolution.mjs` since #11490, which needs the identical
422+
// answer to decide its POPULATION: two copies of this predicate drift, and the
423+
// symptom of drift is a green gate on either side.
424+
import {
425+
configsNamedByTypecheck,
426+
typecheckScriptChain,
427+
SELF_TEST_CASE_COUNT as TYPECHECK_CONFIGS_CASES,
428+
selfTest as typecheckConfigsSelfTest,
429+
} from './typecheck-configs.mjs';
420430

421431
// Anchored to the script, not to cwd: the verdict must not depend on where the
422432
// guard was invoked from.
@@ -1354,36 +1364,6 @@ function configCovers(config, rel) {
13541364
return config.roots.some(under);
13551365
}
13561366

1357-
/**
1358-
* The `typecheck` script, plus every same-package script it delegates to, as a
1359-
* list of the script bodies in visit order.
1360-
*
1361-
* A LIST rather than the joined blob it used to build, because the two readers
1362-
* want different things from it. `configsNamedByTypecheck` only ever asked
1363-
* "does this text mention X" and a blob answers that; GENERATED_COVERED also
1364-
* asks "does X run BEFORE tsc", and that is only decidable INSIDE one script
1365-
* body, where text order is shell order. Across bodies the concatenation order
1366-
* is visit order, which has nothing to do with execution order --
1367-
* `typecheck: 'pnpm gen && tsc'` + `gen: 'next typegen'` joins to `... tsc ...
1368-
* next typegen` while running the generator first, so a blob would red a
1369-
* correct config. Keeping the bodies apart is what lets that case ABSTAIN
1370-
* instead (#10880).
1371-
*/
1372-
function typecheckScriptChain(scripts) {
1373-
const visited = new Set();
1374-
const chain = [];
1375-
const visit = (name, depth) => {
1376-
if (depth > 4 || visited.has(name) || typeof scripts[name] !== 'string') return;
1377-
visited.add(name);
1378-
chain.push(scripts[name]);
1379-
for (const m of scripts[name].matchAll(/\b(?:pnpm(?:\s+run)?|npm\s+run|yarn(?:\s+run)?)\s+([\w:.-]+)/g)) {
1380-
visit(m[1], depth + 1);
1381-
}
1382-
};
1383-
visit('typecheck', 0);
1384-
return chain;
1385-
}
1386-
13871367
/**
13881368
* A declared generator command as a pattern that matches it in a script body:
13891369
* tokens in order, any run of whitespace between them, and a word boundary at
@@ -1495,23 +1475,6 @@ function gitIgnoredPaths(rels) {
14951475
return new Set(res.stdout.split('\0').filter(Boolean));
14961476
}
14971477

1498-
/**
1499-
* Which tsconfig files does the `typecheck` script actually put in front of
1500-
* tsc? Expanded through same-package `pnpm <script>` / `npm run <script>`
1501-
* indirection, because a package that splits the work across two scripts is
1502-
* still running both. A bare `tsc` reads `tsconfig.json`, so any mention of tsc
1503-
* credits the default config; every other config must be NAMED (`-p
1504-
* tsconfig.test.json`), which is what keeps a decorative sibling config from
1505-
* reading as coverage (#5286).
1506-
*/
1507-
function configsNamedByTypecheck(scripts) {
1508-
const text = typecheckScriptChain(scripts).map((s) => ` ${s}`).join('');
1509-
const named = new Set();
1510-
for (const m of text.matchAll(/tsconfig[\w.-]*\.json/g)) named.add(m[0]);
1511-
if (/\btsc\b/.test(text)) named.add('tsconfig.json');
1512-
return named;
1513-
}
1514-
15151478
/**
15161479
* Which tsc programs ACCOUNT for a package's test files -- the ones whose error
15171480
* count somebody actually reads.
@@ -3971,32 +3934,13 @@ function selfTest() {
39713934

39723935
// The observation half is where the :267 blind spot lived: `excludesTests`
39733936
// read only `tsconfig.json`, so a sibling test config was invisible however
3974-
// it was wired. These two helpers now decide it, so they are pinned too.
3975-
const namedCases = [
3976-
{ label: 'a bare tsc credits the default config only', scripts: { typecheck: 'tsc --noEmit' }, expect: ['tsconfig.json'] },
3977-
{
3978-
label: 'an explicitly named sibling config counts',
3979-
scripts: { typecheck: 'tsc --noEmit && tsc --noEmit -p tsconfig.test.json' },
3980-
expect: ['tsconfig.json', 'tsconfig.test.json'],
3981-
},
3982-
{
3983-
label: 'one level of `pnpm <script>` indirection is followed',
3984-
scripts: { typecheck: 'tsc --noEmit && pnpm check:tests', 'check:tests': 'tsx x.mts --project tsconfig.test.json' },
3985-
expect: ['tsconfig.json', 'tsconfig.test.json'],
3986-
},
3987-
{
3988-
label: 'a config no script names is not coverage, however present the file is',
3989-
scripts: { typecheck: 'tsc --noEmit', 'some:other': 'tsc -p tsconfig.test.json' },
3990-
expect: ['tsconfig.json'],
3991-
},
3992-
{ label: 'no typecheck script names nothing', scripts: {}, expect: [] },
3993-
];
3994-
for (const c of namedCases) {
3995-
const got = [...configsNamedByTypecheck(c.scripts)].sort();
3996-
if (JSON.stringify(got) !== JSON.stringify([...c.expect].sort())) {
3997-
failures.push(`configsNamedByTypecheck — ${c.label}: expected ${JSON.stringify(c.expect)}, got ${JSON.stringify(got)}`);
3998-
}
3999-
}
3937+
// it was wired. `configsNamedByTypecheck` and `typecheckScriptChain` now
3938+
// decide it, and since #11490 they live in `scripts/typecheck-configs.mjs`
3939+
// because `check-type-source-resolution.mjs` needs the same answer for its
3940+
// population. Their cases moved WITH them -- one rule, one home, one battery
3941+
// -- and are folded in here so this gate still fails when the predicate it
3942+
// depends on breaks.
3943+
for (const failure of typecheckConfigsSelfTest()) failures.push(failure);
40003944

40013945
const src = { file: 'tsconfig.json', roots: ['src'], excludesTests: false };
40023946
const srcNoTests = { file: 'tsconfig.json', roots: ['src'], excludesTests: true };
@@ -5108,7 +5052,7 @@ function selfTest() {
51085052
}
51095053
console.log(
51105054
`✓ check:type-check-coverage --self-test — ${cases.length} semantic case(s) + ` +
5111-
`${namedCases.length + coverCases.length + unreadCases.length + accountedCases.length
5055+
`${TYPECHECK_CONFIGS_CASES + coverCases.length + unreadCases.length + accountedCases.length
51125056
+ derivedCases.length + sourceCandidateCases.length + includeRootCases.length
51135057
+ chainCases.length + generatorCases.length + layerCases.length} observation case(s) + ` +
51145058
`${driftCases.length + countCases.length + projectCases.length + setupErrorCases.length} re-measure case(s) + ` +

0 commit comments

Comments
 (0)