Found while implementing #13284 (which puts driver-memory / plugin-hono-server's objectstack.config.ts inside a tsc program). Filed rather than fixed: #13284's file surface is fenced to those two packages, and the repair here is not mechanical — it changes what the coverage ratchet reports for a population of 104 files across the whole workspace, which is a decision, not an edit. Measured on origin/main@d62f990a.
1. The mechanism — the ratchet skips depth 0
scripts/check-type-check-coverage.mjs builds SOURCES_COVERED's input from a package walk whose file predicate is:
function isUncheckedSourceCandidate(name, depth) {
if (depth === 0) return false;
...
}
depth === 0 is the package root. So a non-test .ts file sitting at a package root and read by no tsc program is invisible to the invariant whose whole job is "every workspace package's TypeScript is read by tsc somewhere, or its absence is a recorded, tracked decision". It is not reported, and it earns no UNCHECKED_SOURCE_DEBT entry, so it is not tracked either.
The aggregation is directory-granular in the same way — uncheckedByDir keys on rel.slice(0, rel.indexOf('/')), which has no key for a root-level file even if the predicate admitted one.
⇒ This is exactly why #13284 was invisible for as long as it was: pnpm --filter @objectstack/driver-memory typecheck exited 0 with a file that imported a symbol its entry does not export, and check:type-check-coverage reported the package COVERED at the same time. Both gates were green over an unread file.
2. The population — 104 package-root .ts files, and 3 of them are live manifest authoring sites
104 non-test, non-.d.ts .ts files sit at a package root across packages/, apps/ and examples/. Most are build/test tooling (vitest.config.ts, tsup.config.ts), and whether those belong in a program is the decision this card asks for. Three are not tooling — they are plugin manifest authoring sites of exactly the class #13284 is about, and all three are outside every tsc program their package's typecheck script runs:
| file |
shape |
program that reads it |
packages/plugins/plugin-auth/objectstack.config.ts |
defineStack({ ... }), composing ./src/manifest |
none — tsconfig.json is include: ["src/**/*"] |
packages/plugins/plugin-security/objectstack.config.ts |
defineStack({ ... }), composing ./src/manifest |
none — same |
packages/services/service-i18n/objectstack.config.ts |
defineStack({ manifest: { ... inline ... } }) |
none — include: ["src"] |
⚠️ Note for whoever triages: #13284's framing that its two files are "the only places in this repo where a plugin manifest is authored in TypeScript" is inaccurate — there are five package-level sites, not two. The other three differ in a way that softens but does not remove the defect: they call defineStack(...), so the manifest is checked against that function's parameter type if anything ever compiles the call, and today nothing does. service-i18n's is the sharpest of the three, because its manifest body is authored inline rather than imported from src/.
packages/services/service-i18n also runs a bare typecheck: "tsc --noEmit" with a single config, so it has no sibling program to extend.
3. The repo already has both shapes, which is why this reads as an oversight rather than a policy
The three example apps put their root config inside the program and have done so for a while:
examples/app-crm/tsconfig.json — include: ["src/**/*", "objectstack.config.ts", "test/**/*", "vitest.config.ts"], noEmit: true
examples/app-showcase/tsconfig.json — include: [..., "objectstack.config.ts", "playwright.config.ts", ...], noEmit: true
examples/app-todo/tsconfig.json — include: ["src/**/*", "objectstack.config.ts", "test/**/*"], rootDir: "."
None of them carries an emitting rootDir: "./src", which is the constraint that forces a sibling noEmit program in the packages that do (measured on #13284: adding the file to the emitting config raises TS6059 in both packages, under --noEmit too).
What a fix looks like
- Decide what a package-root
.ts file owes. The honest options are: admit depth 0 into isUncheckedSourceCandidate and give the aggregation a key for the root (then every un-programmed vitest.config.ts / tsup.config.ts needs either a program or a ledger entry — that is the 104-file bill), or admit depth 0 only for a declared set of root filenames that are real authored source rather than build tooling (objectstack.config.ts first among them).
- Whichever is chosen, add a
--self-test case for a root-level source file — the gate's existing source-layer cases are all subdirectory cases (packages/a/scripts), which is why the hole survived.
- Put the three sites above into a program, following whichever shape their
rootDir allows (sibling noEmit program where the build config emits from ./src; a widened include where it does not).
⛔ Not a duplicate of #4311 (framework-wide "tsup-built packages nobody typechecks"), #14062 (no plugin package compiles its tests) or #14342 (@objectstack/metadata has no typecheck script at all): every one of those is about a package or a layer the gate CAN see and reports on. This one is about source the gate's own observation half cannot reach, in packages it counts as fully COVERED.
Found while implementing #13284 (which puts
driver-memory/plugin-hono-server'sobjectstack.config.tsinside a tsc program). Filed rather than fixed: #13284's file surface is fenced to those two packages, and the repair here is not mechanical — it changes what the coverage ratchet reports for a population of 104 files across the whole workspace, which is a decision, not an edit. Measured onorigin/main@d62f990a.1. The mechanism — the ratchet skips depth 0
scripts/check-type-check-coverage.mjsbuilds SOURCES_COVERED's input from a package walk whose file predicate is:depth === 0is the package root. So a non-test.tsfile sitting at a package root and read by no tsc program is invisible to the invariant whose whole job is "every workspace package's TypeScript is read by tsc somewhere, or its absence is a recorded, tracked decision". It is not reported, and it earns noUNCHECKED_SOURCE_DEBTentry, so it is not tracked either.The aggregation is directory-granular in the same way —
uncheckedByDirkeys onrel.slice(0, rel.indexOf('/')), which has no key for a root-level file even if the predicate admitted one.⇒ This is exactly why #13284 was invisible for as long as it was:
pnpm --filter @objectstack/driver-memory typecheckexited 0 with a file that imported a symbol its entry does not export, andcheck:type-check-coveragereported the package COVERED at the same time. Both gates were green over an unread file.2. The population — 104 package-root
.tsfiles, and 3 of them are live manifest authoring sites104 non-test, non-
.d.ts.tsfiles sit at a package root acrosspackages/,apps/andexamples/. Most are build/test tooling (vitest.config.ts,tsup.config.ts), and whether those belong in a program is the decision this card asks for. Three are not tooling — they are plugin manifest authoring sites of exactly the class #13284 is about, and all three are outside every tsc program their package'stypecheckscript runs:packages/plugins/plugin-auth/objectstack.config.tsdefineStack({ ... }), composing./src/manifesttsconfig.jsonisinclude: ["src/**/*"]packages/plugins/plugin-security/objectstack.config.tsdefineStack({ ... }), composing./src/manifestpackages/services/service-i18n/objectstack.config.tsdefineStack({ manifest: { ... inline ... } })include: ["src"]defineStack(...), so the manifest is checked against that function's parameter type if anything ever compiles the call, and today nothing does.service-i18n's is the sharpest of the three, because its manifest body is authored inline rather than imported fromsrc/.packages/services/service-i18nalso runs a baretypecheck: "tsc --noEmit"with a single config, so it has no sibling program to extend.3. The repo already has both shapes, which is why this reads as an oversight rather than a policy
The three example apps put their root config inside the program and have done so for a while:
examples/app-crm/tsconfig.json—include: ["src/**/*", "objectstack.config.ts", "test/**/*", "vitest.config.ts"],noEmit: trueexamples/app-showcase/tsconfig.json—include: [..., "objectstack.config.ts", "playwright.config.ts", ...],noEmit: trueexamples/app-todo/tsconfig.json—include: ["src/**/*", "objectstack.config.ts", "test/**/*"],rootDir: "."None of them carries an emitting
rootDir: "./src", which is the constraint that forces a siblingnoEmitprogram in the packages that do (measured on #13284: adding the file to the emitting config raises TS6059 in both packages, under--noEmittoo).What a fix looks like
.tsfile owes. The honest options are: admit depth 0 intoisUncheckedSourceCandidateand give the aggregation a key for the root (then every un-programmedvitest.config.ts/tsup.config.tsneeds either a program or a ledger entry — that is the 104-file bill), or admit depth 0 only for a declared set of root filenames that are real authored source rather than build tooling (objectstack.config.tsfirst among them).--self-testcase for a root-level source file — the gate's existing source-layer cases are all subdirectory cases (packages/a/scripts), which is why the hole survived.rootDirallows (siblingnoEmitprogram where the build config emits from./src; a widenedincludewhere it does not).⛔ Not a duplicate of #4311 (framework-wide "tsup-built packages nobody typechecks"), #14062 (no plugin package compiles its tests) or #14342 (
@objectstack/metadatahas notypecheckscript at all): every one of those is about a package or a layer the gate CAN see and reports on. This one is about source the gate's own observation half cannot reach, in packages it counts as fully COVERED.