Skip to content

[finding] check:type-check-coverage has no invariant for a tsconfig include that names a GENERATED directory the package's typecheck script never generates #10880

Description

@os-zhuang

What

scripts/check-type-check-coverage.mjs holds three invariants — COVERED, REAL, TESTS_COVERED. REAL is the closest one to this gap, and its own words are:

a declared typecheck script actually invokes tsc. A script that echoes, lints, or runs tests is not type coverage.

There is no invariant one notch further along: the script invokes tsc, but over a program whose completeness depends on generated files nothing in the script produces.

The measured instance

apps/docs/tsconfig.json includes .next/types/**/*.ts and .next/dev/types/**/*.ts. Those directories are produced by next typegen, which the wired typecheck script did not run. Measured on origin/main @ 7d483e1e5f, .next deleted:

pnpm --filter @objectstack/docs typecheck -> exit 0

Green, having compiled none of the generated route types. Running typegen first put 6 more files in the program (tsc --listFiles: 1225 -> 1231), including a 160-line validator.ts that checks 13 route entry points. An ablation confirmed the difference is load-bearing: a layout given a params shape its route cannot supply left the bare script at exit 0 / 0 errors and made the typegen'd one fail with TS2344.

That specific instance is repaired in #10879 — the script now generates what it reads. Nothing mechanical stops it, or a future Next/app package, from regressing.

Why it may be worth a card

The gate already parses every tsconfig in every package and already reads each typecheck script (readTsconfig, configsNamedByTypecheck). The missing question is cheap to ask with what it has:

  • for each include entry rooted in a generated / gitignored directory, does the package's typecheck script contain the command that produces it?
  • and the stricter half: an include entry naming a generated root that no declared generator covers is a config promising coverage nothing produces — which is what .next/dev/types/**/*.ts is for a plain tsc run.

A small declared table (generated root -> the command that must appear in the script) keeps the Next-specific knowledge out of the logic. Today it would hold one row.

Note the second bullet cannot be answered by deleting the offending glob: Next owns that array. writeConfigurationDefaults re-adds .next/dev/types/**/*.ts on the next next dev / next build, measured directly by running that routine against a narrowed copy. So a guard here has to be able to say "declared, and deliberately not generated" as well as "generated by the script".

Coordination

This would edit scripts/check-type-check-coverage.mjs, the same file #10756 is dispatched against. Different invariant and different failure direction — #10756 is a source directory falling outside tsc, this is a generated directory whose absence makes a check read green — but whichever lands second has to rebase onto the first. Worth sequencing rather than running in parallel.

Found while implementing #10871; deliberately kept out of that PR's diff, which is two files in apps/docs. Unassigned.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions