diff --git a/scripts/coverage-crap/patches.test.ts b/scripts/coverage-crap/patches.test.ts index ddffffc121..89031fc79b 100644 --- a/scripts/coverage-crap/patches.test.ts +++ b/scripts/coverage-crap/patches.test.ts @@ -78,16 +78,22 @@ async function convertRecord(convert: Converter, capture: Capture): Promise record.branchMap[key].type === 'if' && record.branchMap[key].line === line, - ); + const id = Object.keys(record.b).find((key) => { + const branch = record.branchMap[key]; + assert.ok(branch, `the converter recorded no branch map entry for ${key}`); + return branch.type === 'if' && branch.line === line; + }); assert.ok(id, `the converter recorded no if branch on line ${line}`); - return record.b[id].map(Number); + const counters = record.b[id]; + assert.ok(counters, `the converter recorded no counters for if branch on line ${line}`); + return counters.map(Number); } // `async-fallback-operand` is the reported shape: `params.release ?? await probe(...)` feeding an @@ -99,7 +105,9 @@ function ifCounters(record: IstanbulRecord, line: number): number[] { test('an async branch with an awaited operand drops the path it cannot count', async () => { const convert = await loadConverter(); const capture = captures['async-fallback-operand']; + assert.ok(capture, 'missing async-fallback-operand capture'); const branch = capture.branches.i1; + assert.ok(branch, 'missing i1 branch in async-fallback-operand capture'); assert.deepEqual([branch.entered, branch.taken], [8, 6]); const record = await convertRecord(convert, capture); // The taken path is provable and stays. The entry count is not recoverable from a record whose @@ -112,20 +120,27 @@ test('an async branch with an awaited operand drops the path it cannot count', a test('a branch whose awaited continuation out-counts it publishes no impossible count', async () => { const convert = await loadConverter(); const capture = captures['early-return-then-fallback']; + assert.ok(capture, 'missing early-return-then-fallback capture'); + const firstBranch = capture.branches.i1; + const secondBranch = capture.branches.i2; + assert.ok(firstBranch, 'missing i1 branch in early-return-then-fallback capture'); + assert.ok(secondBranch, 'missing i2 branch in early-return-then-fallback capture'); const record = await convertRecord(convert, capture); - assert.deepEqual([capture.branches.i1.entered, capture.branches.i1.taken], [8, 3]); - assert.deepEqual([capture.branches.i2.entered, capture.branches.i2.taken], [5, 2]); + assert.deepEqual([firstBranch.entered, firstBranch.taken], [8, 3]); + assert.deepEqual([secondBranch.entered, secondBranch.taken], [5, 2]); // The branch ahead of the await is counted correctly and must not move; the one behind it was // `[2,-2]` before the patch. - assert.deepEqual(ifCounters(record, capture.branches.i1.line), [3, 5]); - assert.deepEqual(ifCounters(record, capture.branches.i2.line), [2]); + assert.deepEqual(ifCounters(record, firstBranch.line), [3, 5]); + assert.deepEqual(ifCounters(record, secondBranch.line), [2]); }); test('sync and non-awaited branches keep counting their implicit else', async () => { const convert = await loadConverter(); for (const name of ['sync-fallback-operand', 'plain-async']) { const capture = captures[name]; + assert.ok(capture, `missing ${name} capture`); const branch = capture.branches.i1; + assert.ok(branch, `missing i1 branch in ${name} capture`); assert.deepEqual( ifCounters(await convertRecord(convert, capture), branch.line), [6, 2], diff --git a/scripts/layering/package-boundaries.test.ts b/scripts/layering/package-boundaries.test.ts index b5cb6f5a1c..b633411faf 100644 --- a/scripts/layering/package-boundaries.test.ts +++ b/scripts/layering/package-boundaries.test.ts @@ -110,10 +110,8 @@ test('specifier sites carry 1-based lines for static and dynamic imports', () => }); test('the compiled set classifies every file region the R11 walk visits', () => { - // The compiler-owned/compiler-exempt split above is keyed on this predicate (#3279), so the - // mutation it must fail on is a file region joining or leaving `tsc -b`'s programs without - // the predicate following: package `src/`, root `src/` and root `test/` compile; `scripts/` - // and the package harness dirs are the fail-closed side. + // Package src, root src and root test are compiler-owned. Project-reference pins record + // compiler-root changes; scripts and package harness directories retain full R11 checks. assert.equal(insideCompiledSources('packages/kernel/src/errors.ts'), true); assert.equal(insideCompiledSources('packages/maestro/src/index.ts'), true); assert.equal(insideCompiledSources('src/cli.ts'), true); @@ -122,11 +120,11 @@ test('the compiled set classifies every file region the R11 walk visits', () => assert.equal(insideCompiledSources('packages/maestro/test/conformance/harness.ts'), false); assert.equal(insideCompiledSources('packages/proxy/tsdown.config.ts'), false); - // The one scripts file the root program DOES compile is classified as uncompiled: R11 - // keeps its full branch set there (redundant with tsc, never narrower). The exact root - // include list and the reference sets are pinned against the graph itself in - // project-references.test.ts, where the divergence is recorded rather than assumed absent. assert.equal(insideCompiledSources('scripts/help-conformance-command-validator.ts'), false); + assert.equal(insideCompiledSources('scripts/coverage-crap/stamp.ts'), false); + assert.equal(insideCompiledSources('scripts/coverage-changed/run.ts'), false); + assert.equal(insideCompiledSources('scripts/vitest-coverage-run-stamp-reporter.ts'), false); + assert.equal(insideCompiledSources('scripts/vitest-coverage-run-stamp-reporter.test.ts'), false); }); test('readWorkspacePackages reads tracked manifests only', () => { diff --git a/scripts/layering/package-boundaries.ts b/scripts/layering/package-boundaries.ts index 22f59b864d..620a7d9296 100644 --- a/scripts/layering/package-boundaries.ts +++ b/scripts/layering/package-boundaries.ts @@ -1,7 +1,8 @@ // Catches: a package importing a workspace sibling its manifest never declared, and a root // file tunnelling into a package's src with a relative path — both for every file, compiled // or not — plus the whole specifier sweep (unknown package, un-exported subpath) for the -// scripts/ files the type graph omits. `tsc -b` owns the rest of the old R11 surface for +// scripts/ files, including tooling roots the type graph also checks. `tsc -b` owns the +// rest of the old R11 surface for // compiled sources: NodeNext resolution rejects a non-exported subpath and an unknown // package (TS2307), and composite `rootDir` rejects a package→root relative escape // (TS6059 + TS6307) — planted proofs recorded on #3279. @@ -18,13 +19,13 @@ // // R11 package-boundaries: the workspace rules of #1490, as data the gate walks. // -// What remains is exactly what the type graph cannot see: +// R11 retains the claims the type graph cannot see, plus conservative checks for scripts: // a package import of a sibling the manifest never declared (pnpm's root links make it resolve // anyway), a root file tunnelling into `packages/*/src` with a relative path (tsc typechecks // it fine; Node's ESM loader does not realpath specifiers, so a module loaded BOTH relatively // and via its package specifier instantiates twice in one process — duplicate AppError, broken -// instanceof), and, for `scripts/` files which sit outside the type graph by design, the -// root-side unknown-package and exports-map checks that compiled files get from TS2307. +// instanceof), and, for all `scripts/` files, the root-side unknown-package and exports-map +// checks. Explicit tooling roots also receive those resolution checks from TS2307. // On top, `TEST_ONLY_FIXTURE_SUBPATHS` mirrors R13's Android fixture gate for the non-platform // packages: a production file importing one of those fixture subpaths is an R11 violation. @@ -221,20 +222,18 @@ function specifierPackageName(specifier: string): string | undefined { /** * The file set R11 treats as parsed by the `tsc -b` graph: each package project includes its - * own `src/`, and the root project includes `src/` and `test/`. Everything else the R11 walk - * visits — the rest of `scripts/` (gate and tooling modules run through - * `--experimental-strip-types`, not a tsconfig program), the `packages/maestro/test/` - * conformance harness, and package build configs — reaches packages through import sites the + * own `src/`, and the root project includes `src/` and `test/`. Other regions the R11 walk + * visits include `scripts/`, the `packages/maestro/test/` conformance harness, and package + * build configs. Files outside the compiler graph reach packages through import sites the * compiler never parses, so R11 stays the only owner of every boundary claim about those * routes (#3279). Only the RESOLUTION branches (unknown package, un-exported subpath) are * split this way; the relative-escape branch fires regardless — a planted sibling tunnel * `tsc -b` accepted, see `checkPackageInternalSites`. * - * One documented divergence: the root program also includes - * `scripts/help-conformance-command-validator.ts`, which this predicate classifies as - * uncompiled. That is fail-closed — R11 keeps its full branch set (unknown package, - * exports-map) for the file on top of what tsc enforces — never fail-open. The divergence - * and the exact root include list are pinned in `project-references.test.ts`, and the + * The root program also includes explicit help-conformance and coverage-tooling roots. + * This predicate classifies all scripts as uncompiled. That is fail-closed — R11 keeps its + * full branch set (unknown package, exports-map) on top of what tsc enforces. The divergence + * and the root include regions are pinned in `project-references.test.ts`, and the * per-package `include: ["src"]` / no-`exclude` shape this predicate's regexes assume is * pinned there too, so widening any project's file set must be recorded there rather than * silently diverging from this predicate. @@ -364,8 +363,8 @@ export function checkPackageInternalSites( * fine (#3279), but Node's ESM loader does not realpath specifiers, so dual relative/specifier * loads instantiate the module twice — and a workspace specifier the root manifest never * declares, which the root's own link farm resolves anyway. For compiled root files the - * compiler owns unknown packages and un-exported subpaths with TS2307; for scripts/ files, - * which sit outside the type graph by design, R11 keeps every branch. + * compiler owns unknown packages and un-exported subpaths with TS2307. For scripts/ files, + * R11 keeps every branch, including where explicit tooling roots also receive compiler checks. */ export function checkRootSites( sites: readonly SpecifierSite[], diff --git a/scripts/layering/project-references.test.ts b/scripts/layering/project-references.test.ts index 7579689073..9f18256ec0 100644 --- a/scripts/layering/project-references.test.ts +++ b/scripts/layering/project-references.test.ts @@ -58,20 +58,19 @@ test('root and examples/sdk reference exactly the workspace package set', () => }); test('the compiled-set predicate is licensed by the tsconfig include/exclude shape it mirrors', () => { - // `insideCompiledSources` (package-boundaries.ts) hard-codes the three include shapes the - // tsc -b graph compiles: `packages/*/src`, root `src`, root `test`. That regex is honest - // only while no project widens its file set behind it, so the shape is pinned here (the - // file that already reads the tsconfigs), and the R11 side keeps its pin in - // package-boundaries.test.ts (the #3279 walk-region test). A package tsconfig that gains - // an `exclude`, or an include beyond `src` (plus replay-port's shared `global.d.ts` - // sibling), or a root file added to the root include beyond src/test, must update the - // predicate or fail here — the same "views cannot drift" rule as the reference lists. - assert.deepEqual(readTsconfig('tsconfig.json').include, [ - 'src', - 'test', - 'scripts/help-conformance-command-validator.ts', - 'packages/command-registry/src/global.d.ts', - ]); + // Scripts retain full R11 checks even when the compiler also checks them. + const rootConfig = readTsconfig('tsconfig.json'); + assert.equal( + rootConfig.exclude, + undefined, + 'root tsconfig must not exclude compiler-owned paths', + ); + assert.deepEqual( + (rootConfig.include ?? []).filter( + (entry) => !path.posix.normalize(entry).startsWith('scripts/'), + ), + ['src', 'test', 'packages/command-registry/src/global.d.ts'], + ); // `src` everywhere, plus the one shared ambient-declaration entry replay-port and // session-journal include. Exact include ARRAYS, not a `global.d.ts` suffix class: a // new include of any name must extend this allow-list deliberately, or the drift fails @@ -94,17 +93,6 @@ test('the compiled-set predicate is licensed by the tsconfig include/exclude sha } }); -test('the one scripts file in the root program is the divergence insideCompiledSources records', () => { - // The predicate classifies scripts/help-conformance-command-validator.ts as UNCOMPILED - // (fail-closed: R11 keeps its full branch set there) even though the root include names - // it. This is the assertion the predicate's doc block points at: the divergence is - // recorded here, and a second scripts file joining the root program must be recorded - // next to this one rather than silently widening the exception. - const rootInclude = readTsconfig('tsconfig.json').include ?? []; - const scriptsEntries = rootInclude.filter((entry) => entry.startsWith('scripts/')); - assert.deepEqual(scriptsEntries, ['scripts/help-conformance-command-validator.ts']); -}); - test('the root manifest declares every workspace package as a workspace dependency', () => { const manifest = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) as { dependencies?: Record; diff --git a/scripts/lib/cli-args.ts b/scripts/lib/cli-args.ts index acd555ee5d..beeaaa0ee1 100644 --- a/scripts/lib/cli-args.ts +++ b/scripts/lib/cli-args.ts @@ -11,12 +11,16 @@ export function parseScriptArgs( usage: string, options: T, ): ReturnType>['values'] { - const { values } = parseArgs({ + const { values } = parseArgs<{ + args: readonly string[]; + options: T & { help: { type: 'boolean'; short: 'h'; default: false } }; + allowPositionals: false; + }>({ args: [...argv], options: { ...options, help: { type: 'boolean', short: 'h', default: false } }, allowPositionals: false, }); - if (values.help) { + if ((values as { help?: boolean }).help) { process.stdout.write(usage); process.exit(0); } diff --git a/tsconfig.json b/tsconfig.json index e0a9e4aefe..bf020fa523 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,10 @@ "src", "test", "scripts/help-conformance-command-validator.ts", + "scripts/coverage-crap/**/*.ts", + "scripts/coverage-changed/**/*.ts", + "scripts/vitest-coverage-run-stamp-reporter.ts", + "scripts/vitest-coverage-run-stamp-reporter.test.ts", "packages/command-registry/src/global.d.ts" ], "references": [