Skip to content

[finding] A package can pass check:type-check-coverage with a whole source directory outside tsc — packages/objectql/scripts/** is invisible to its own typecheck #10756

Description

@claude

Measured while implementing #10269 (PR #10755). Not repaired there: that card's surface
is four entry guards, and this is a property of the tsconfig include globs.

The measurement

@objectstack/objectql declares a typecheck script, passes it, and is counted as
covered by check:type-check-coverage (64/77 workspace packages type-checked ... 13 in the DEBT ledger, 1 exempt — objectql is in neither ledger). But its tsconfig is

{ "include": ["src/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts"] }

so packages/objectql/scripts/** is outside the program the package's own
typecheck compiles (see the CORRECTION below: that is narrower than "no tsc program
at all", which is what this body first claimed). Ablation on
origin/main + the #10269 branch, at ca022387f2:

inject const OS_ABLATION_TYPE_ERROR: number = "definitely not a number";
into packages/objectql/scripts/dry-run-hash-compat.ts
confirm on disk grep -c OS_ABLATION_TYPE_ERROR -> 1
pnpm --filter @objectstack/objectql typecheck -> EXIT 0, zero `error TS` lines
pnpm exec tsc --noEmit --listFiles | grep -c scripts/dry-run-hash-compat.ts -> 0
restore, confirm on disk grep -c OS_ABLATION_TYPE_ERROR -> 0

The file appears zero times in tsc's own file list, and a number initialised with a
string sits in it without moving the gate.

CORRECTION (2026-08-21) — "in no tsc program at all" was too broad

This body originally read "so packages/objectql/scripts/** is in no tsc program at
all". That is wrong as written, and PR #10755 going red is what measured it. The
accurate statement is the one already in the title: that directory is invisible to
the package's owntsc -p, and it is not invisible to every tsc program.

The TEST_DEBT re-measure builds a different project. measureTestDebt calls
remeasureProject (scripts/check-type-check-coverage.mjs), which copies the package
tsconfig and drops every test glob from exclude — so
packages/objectql/src/dry-run-hash-compat.test.ts becomes a root file, and its
line 5 is:

import{runDryRun,typeLegacyMetadataRow}from'../scripts/dry-run-hash-compat';

tsc pulls every transitively imported file into the program regardless of include,
so the script is compiled there. It is reached through the test, not through
include.

Measured: the import.meta.url that PR #10755 added to that script (the package
declares no "type", so under the repo-wide module: NodeNext it compiles as
CommonJS) produced exactly one TS1470 in the re-measure and drove
@objectstack/objectql from its frozen TEST_DEBT of 355 to 356 — a red
Type Check · debt ledger — while pnpm --filter @objectstack/objectql typecheck
stayed at exit 0 the whole time.

Why the ablation above could not see it: it ran the package'stypecheck and a
--listFiles over the package's own config, and that is precisely the program the
file is missing from. It never built the test-inclusive project.

The finding stands and this card stays open. The blind spot is real, and the
accidental coverage described here argues for the gate rather than against it: it is
not a property anyone declared. It holds only for the files some test happens to
import, only while that import exists, and it reports through a shrink-only debt
ledger — as a drifted count on a package the ratchet calls covered — rather than
through the package's own typecheck.

Why this is worth a card rather than a shrug

packages/objectql/scripts/dry-run-hash-compat.ts is not scratch: it is a
compatibility checker with a documented CLI, and packages/objectql/src/dry-run-hash-compat.test.ts
imports runDryRun from it. So a package test depends on a module whose types nothing
verifies.

The same shape holds for packages/core/examples/** (include: ["src/**/*"],
rootDir: "./src"), which docs/qa/platform-checklist/FOLLOW-UPS.md already notes in
passing for phase2-integration.ts — but @objectstack/core has no typecheck script at
all and is a ledgered DEBT entry (#4311), so that half is already tracked. The objectql
half is not: it is a package the ratchet reports as covered.

The gap in the ratchet, stated precisely

check:type-check-coverage is package-granular: it asks whether a package declares a
typecheck script or carries a ledger entry. It cannot see a package that declares one
and whose tsconfig include omits a real source directory. That is the same class AGENTS.md
already warns about for test files —

Never exclude*.test.ts / *.spec.ts from a package's tsconfig.jsontsc --noEmit reads that config, so the exclusion hides the tests from the very check the
typecheck script advertises (a green gate over source nothing read).

— one level up: a whole directory rather than a file glob, and reached through include
rather than exclude, so the existing prose does not cover it.

Not proposing the remedy here

Several shapes are possible (a sibling tsconfig.scripts.json named in the typecheck
script, the packages/spec pattern; widening include; or a ratchet limb that compares a
package's tracked .ts files against tsc --listFiles), and which one is right depends on
how many packages are in this state — which nobody has counted. Filed so the count gets
made rather than guessed.

Dedupe

Grepped the titles and bodies of all 299 open issues for packages/objectql/scripts,
packages/core/examples, include.{0,20}src/\*\*, outside (every|any) tsconfig and
--listFiles. Only #10269 matches, and only because it names the two paths.
(search_issues was unavailable to this seat.)


Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions