Skip to content

[finding] check:type-check-coverage cannot see PACKAGE-ROOT source (depth 0 is skipped by construction) — three more objectstack.config.ts manifest sites sit outside every tsc program with no ledger entry #14386

Description

@os-musk

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.jsoninclude: ["src/**/*", "objectstack.config.ts", "test/**/*", "vitest.config.ts"], noEmit: true
  • examples/app-showcase/tsconfig.jsoninclude: [..., "objectstack.config.ts", "playwright.config.ts", ...], noEmit: true
  • examples/app-todo/tsconfig.jsoninclude: ["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

  1. 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).
  2. 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.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions