Found while graduating @objectstack/trigger-record-change out of the TEST_DEBT ledger (#11204, PR #11489). Filed unassigned per the out-of-scope-findings rule; searched open issues for type-source-resolution and tsconfig.test.json first and found none. Nearest neighbour is #11094, which is a different axis (a paths entry pointing at a sibling's src as a latent runtime redirect); this one is about which configs the gate reads at all.
The gap
scripts/check-type-source-resolution.mjs builds each package's program from exactly one config:
constconfigPath=join(pkg.dir,'tsconfig.json');
Every other tsconfig in the package is invisible to it. That matters because the repo's own supported repair for a hidden test layer — stated in AGENTS.md and in scripts/check-type-check-coverage.mjs's TESTS_COVERED docs — is precisely a sibling config:
The build config has a reason to exclude tests — ci.yml gates that no test file reaches the published artifact — so the supported repair is a SIBLING tsconfig.test.json wired into the typecheck script.
Five packages now take that route: packages/spec, packages/client, packages/metadata-core, packages/metadata-fs, and packages/triggers/trigger-record-change. Each of those siblings is a real tsc program that CI runs on every PR, and none of them is in this gate's population.
Why it is not merely theoretical
The measurement is on PR #11489. That package's 7 test files import @objectstack/driver-sql, @objectstack/formula, @objectstack/objectql and @objectstack/service-automation, none of which the build config's program ever contained. Put those tests back through the build config and the gate sees them immediately:
✗ @objectstack/trigger-record-change: NEW dist-resolved type import(s) since this
entry was measured: @objectstack/driver-sql, @objectstack/formula,
@objectstack/objectql, @objectstack/service-automation.
Put the identical files through a sibling config instead and the gate stays at exit 0 with its count unchanged (77 packages with a tsconfig.json scanned; 51 registered). Same files, same four dist-resolved type imports, same exposure the gate exists to ratchet — reported in one spelling and silent in the other.
The asymmetry has a perverse edge worth naming: the route the repo prescribes is the route this gate cannot see, so following the house pattern is what makes the exposure invisible. A future author with a genuine choice between the two spellings gets a green gate for picking the one that hides more.
Why this is the gate's own argument
Its header already rules that a per-surface sweep is the wrong terminal state, quoting check:test-source-alias:
it leaves the NEXT package unguarded, and the symptom of the omission is a green test, so nothing would report the gap
and then generalises it: "That reasoning is axis-independent." The same sentence applies one level down — the gate is per-package where it needs to be per-program.
Shape of a fix (not prescribing)
The natural widening is to read every tsconfig*.json a package's typecheck script NAMES, which is a predicate check-type-check-coverage.mjs already computes (configsNamedByTypecheck) and which keeps a decorative config from counting. That would change the registry's measured population, so it is a ratchet re-baselining rather than a one-liner, and it needs its own card — which is why this is filed rather than ridden along.
Not addressed in PR #11489, deliberately: that PR's whole finding is that widening a shrink-only registry entry is not a remedy, and this is the other half of the same observation.
Generated by Claude Code
Found while graduating
@objectstack/trigger-record-changeout of the TEST_DEBT ledger (#11204, PR #11489). Filed unassigned per the out-of-scope-findings rule; searched open issues fortype-source-resolutionandtsconfig.test.jsonfirst and found none. Nearest neighbour is #11094, which is a different axis (apathsentry pointing at a sibling's src as a latent runtime redirect); this one is about which configs the gate reads at all.The gap
scripts/check-type-source-resolution.mjsbuilds each package's program from exactly one config:Every other tsconfig in the package is invisible to it. That matters because the repo's own supported repair for a hidden test layer — stated in AGENTS.md and in
scripts/check-type-check-coverage.mjs's TESTS_COVERED docs — is precisely a sibling config:Five packages now take that route:
packages/spec,packages/client,packages/metadata-core,packages/metadata-fs, andpackages/triggers/trigger-record-change. Each of those siblings is a real tsc program that CI runs on every PR, and none of them is in this gate's population.Why it is not merely theoretical
The measurement is on PR #11489. That package's 7 test files import
@objectstack/driver-sql,@objectstack/formula,@objectstack/objectqland@objectstack/service-automation, none of which the build config's program ever contained. Put those tests back through the build config and the gate sees them immediately:Put the identical files through a sibling config instead and the gate stays at exit 0 with its count unchanged (
77 packages with a tsconfig.json scanned; 51 registered). Same files, same four dist-resolved type imports, same exposure the gate exists to ratchet — reported in one spelling and silent in the other.The asymmetry has a perverse edge worth naming: the route the repo prescribes is the route this gate cannot see, so following the house pattern is what makes the exposure invisible. A future author with a genuine choice between the two spellings gets a green gate for picking the one that hides more.
Why this is the gate's own argument
Its header already rules that a per-surface sweep is the wrong terminal state, quoting
check:test-source-alias:and then generalises it: "That reasoning is axis-independent." The same sentence applies one level down — the gate is per-package where it needs to be per-program.
Shape of a fix (not prescribing)
The natural widening is to read every
tsconfig*.jsona package'stypecheckscript NAMES, which is a predicatecheck-type-check-coverage.mjsalready computes (configsNamedByTypecheck) and which keeps a decorative config from counting. That would change the registry's measured population, so it is a ratchet re-baselining rather than a one-liner, and it needs its own card — which is why this is filed rather than ridden along.Not addressed in PR #11489, deliberately: that PR's whole finding is that widening a shrink-only registry entry is not a remedy, and this is the other half of the same observation.
Generated by Claude Code