Filed unassigned and ungraded by the dev of #14918 (domain:devx), as an out-of-scope finding of that card. ⛔ domain:*, priority and type are triage's. Latent, not measured-live — see the measurement below before grading it as anything but low.
The mechanism
scripts/check-type-check-coverage.mjs builds its subject list two different ways and only one of them is observed:
workspacePackages() walks the 79 dirs workspacePackageDirs(ROOT) returns, and for each one calls testCoverage(dir, scripts) — the walk that produces hiddenTests, pinFiles, uncheckedSources and generatedIncludes.
observed() builds the root as { name: rootManifest.name, scripts: rootManifest.scripts ?? {} } — a manifest read, no walk, none of those four fields.
evaluate(packages, root, state) then asks the per-package invariants inside for (const pkg of packages), and the root is not in that array (measured: workspacePackageDirs() returns 79 entries, '.' and '' among none of them). The root reaches evaluate only through byName and the COVERED / REAL / RUNNABLE / RECONCILED checks written for it by name.
⇒ TESTS_COVERED, SOURCES_COVERED, PINS_CHECKED and GENERATED_COVERED are asked of 79 packages and never of the root. The file's own header says the opposite, in the sentence that introduces the invariant list:
Invariants, per workspace package (the root workspace package included -- #4311's audit counted its top-level TypeScript like any other package's)
Why this is a fourth axis, not a re-filing of the other three
| axis |
card |
population |
| needs a pre-existing ledger row to notice a package at all |
#14630 |
packages with no row |
| cannot see PACKAGE-ROOT source (depth 0) |
#14386 |
depth-0 files inside a package |
SOURCES_COVERED asked only of packages declaring typecheck |
#14918 |
ledgered packages |
| four clauses never asked of the ROOT package |
this card |
the workspace root itself |
Same pattern each time, and it is the pattern #14918's triage named: a clause skipped for a population, with the run saying so nowhere. #15414 records the identical shape on a different gate (a scan that walks only the root scripts/, so one package-local gate is in no population).
The measurement, which is why this is LATENT
On origin/main today the root's own config makes three of the four vacuous by accident rather than by decision:
- root
tsconfig.json declares no include (exclude: ["node_modules","packages","apps","examples"]), so readTsconfig gives it roots: [''] — the whole package. Every root-level file is inside the program, so an unread source directory or a hidden test file could not exist there right now.
git ls-files '*.test.ts' '*.test.mts' '*.spec.ts' outside packages/, apps/ and examples/: 0 files. 0 of them carry @ts-expect-error.
⇒ Nothing is hidden today. What is missing is the guard: the day the root config grows an include (say ["scripts"]), or a root tsconfig.build.json that excludes a test glob, or the repo gains its first root-level test file with a compile-time pin, all four clauses stay silent about it — and the header will still say the root is covered like any other package. That is the same "a green line reads as everything checked" failure the three closed axes each cost a dev a round to find.
Repair sketch (not decided here)
The observation half already generalises: testCoverage('.', rootManifest.scripts) would produce the four fields for the root, and its walk already skips any subdirectory carrying a package.json at depth > 0, so it would not re-walk the 79. The parts that need a decision rather than a patch:
PINS_CHECKED anchors on the invoked programs (configsNamedByTypecheck), and the root's coverage runs through typecheck:root, not typecheck — asking the clause without teaching it that spelling would manufacture findings rather than find them.
- the root's
exclude list is how it stays out of the packages' way; a clause that reads it as "hiding source" would be wrong.
#14918's PR adds a SCOPED_CLAUSES table plus a clause scope: line that prints which per-package clauses were not asked of which packages. That line is computed over the 79 and states so; this card is the population it cannot currently express, and closing it should either bring the root into the population or declare it in that table with the argument for why it stays out.
Refs: #14918 (the card whose dev found this) · #14630 · #14386 · #15414 (same shape, different gate) · #4311 (the tracking issue the gate cites).
Filed unassigned and ungraded by the dev of #14918 (
domain:devx), as an out-of-scope finding of that card. ⛔domain:*, priority and type are triage's. Latent, not measured-live — see the measurement below before grading it as anything but low.The mechanism
scripts/check-type-check-coverage.mjsbuilds its subject list two different ways and only one of them is observed:workspacePackages()walks the 79 dirsworkspacePackageDirs(ROOT)returns, and for each one callstestCoverage(dir, scripts)— the walk that produceshiddenTests,pinFiles,uncheckedSourcesandgeneratedIncludes.observed()builds the root as{ name: rootManifest.name, scripts: rootManifest.scripts ?? {} }— a manifest read, no walk, none of those four fields.evaluate(packages, root, state)then asks the per-package invariants insidefor (const pkg of packages), and the root is not in that array (measured:workspacePackageDirs()returns 79 entries,'.'and''among none of them). The root reachesevaluateonly throughbyNameand the COVERED / REAL / RUNNABLE / RECONCILED checks written for it by name.⇒ TESTS_COVERED, SOURCES_COVERED, PINS_CHECKED and GENERATED_COVERED are asked of 79 packages and never of the root. The file's own header says the opposite, in the sentence that introduces the invariant list:
Why this is a fourth axis, not a re-filing of the other three
typecheckSame pattern each time, and it is the pattern #14918's triage named: a clause skipped for a population, with the run saying so nowhere. #15414 records the identical shape on a different gate (a scan that walks only the root
scripts/, so one package-local gate is in no population).The measurement, which is why this is LATENT
On
origin/maintoday the root's own config makes three of the four vacuous by accident rather than by decision:tsconfig.jsondeclares noinclude(exclude: ["node_modules","packages","apps","examples"]), soreadTsconfiggives itroots: ['']— the whole package. Every root-level file is inside the program, so an unread source directory or a hidden test file could not exist there right now.git ls-files '*.test.ts' '*.test.mts' '*.spec.ts'outsidepackages/,apps/andexamples/: 0 files. 0 of them carry@ts-expect-error.⇒ Nothing is hidden today. What is missing is the guard: the day the root config grows an
include(say["scripts"]), or a roottsconfig.build.jsonthat excludes a test glob, or the repo gains its first root-level test file with a compile-time pin, all four clauses stay silent about it — and the header will still say the root is covered like any other package. That is the same "a green line reads as everything checked" failure the three closed axes each cost a dev a round to find.Repair sketch (not decided here)
The observation half already generalises:
testCoverage('.', rootManifest.scripts)would produce the four fields for the root, and itswalkalready skips any subdirectory carrying apackage.jsonat depth > 0, so it would not re-walk the 79. The parts that need a decision rather than a patch:PINS_CHECKEDanchors on the invoked programs (configsNamedByTypecheck), and the root's coverage runs throughtypecheck:root, nottypecheck— asking the clause without teaching it that spelling would manufacture findings rather than find them.excludelist is how it stays out of the packages' way; a clause that reads it as "hiding source" would be wrong.#14918's PR adds a
SCOPED_CLAUSEStable plus aclause scope:line that prints which per-package clauses were not asked of which packages. That line is computed over the 79 and states so; this card is the population it cannot currently express, and closing it should either bring the root into the population or declare it in that table with the argument for why it stays out.Refs: #14918 (the card whose dev found this) · #14630 · #14386 · #15414 (same shape, different gate) · #4311 (the tracking issue the gate cites).