You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
No gate asserts the TYPE axis of the source-resolution invariant: check:test-source-alias reads vitest.config.* only, so every package's tsconfig can resolve workspace deps through dist with nothing saying so #8180
Filing unassigned; found while implementing #8021 (PR #8176), and deliberately not fixed there — that card's scope is one package's tsconfig, and a repo-wide gate is its own blast radius.
The asymmetry
#7849 / PR #7966 landed check:test-source-alias, which makes the source-resolution invariant mechanical rather than per-package goodwill: for every workspace package with tests, walk the imports reachable from those tests, keep the deps that resolve under dist/, simulate Vite's resolution over vitest.config.*, and demand that anything left is registered. Its own header states why a sweep is the wrong terminal state — "it leaves the NEXT package unguarded, and the symptom of the omission is a green test, so nothing would report the gap".
That reasoning is axis-independent, but the gate is not. It reads vitest.config.* and nothing else. No gate reads tsconfig.json, so the identical exposure on the type axis is unguarded repo-wide, and its symptom is likewise a green typecheck.
Measured, on the one package that has now been fixed
#8021 measured it end to end on packages/qa/downstream-contract. Identical checkout, identical stale dist, a narrowing injected into packages/spec/src/integration/connector.zod.ts in source only with no rebuild:
without a paths block: tsc --noEmit exit 0, clean
with a paths block: src/additional-domains.fixtures.ts(35,3): error TS2322: Type 'string' is not assignable to type 'number'.
That package is the repo's backward-compatibility gate, whose README promises "a removed or NARROWED export fails here" — so it was the loudest instance, which is why it got a card. It is not the only instance. The mechanism is generic and is in fact documented as the expected state in .github/workflows/lint.yml, beside the type-check-debt re-measure step:
tsc resolves workspace imports through each dependency's built dist/*.d.ts — so it needs the build steps above
Ordering does not reach it, same as both prior cards: turbo.json orders typecheck after ^build, so turbo run typecheck and CI are not the failing paths. What breaks is every path turbo does not mediate — pnpm --filter PKG typecheck inside a package, an editor's TS server, an agent in a tree built at an older commit. Those are exactly the paths a type contract is re-read on while someone is changing the dependency.
What a gate would have to get right (from #8021's measurements)
Not a copy of the vitest one — the resolution algorithm and the trap are both different:
tsc's paths algorithm, not Vite's. An exact (star-free) key wins outright; otherwise the pattern key with the longest matching prefix wins and the captured text substitutes into the target's star. Vite's is first-match-wins with prefix matching for string finds.
The trap has a different spelling and is worse. A tsconfig key without a * is an exact match, so the bare @objectstack/spec entry cannot swallow @objectstack/spec/ui the way a Vite object-form alias does (ENOTDIR). The tsconfig-shaped mistake is @objectstack/spec* — star not preceded by a slash — which matches every namespace and folds it onto whatever the target names. It does not crash: spec/src/index.ts re-exports most of the namespace surface, so it type-checks against the wrong module and stays green.
tsconfig.json is JSONC. Whole-line comment stripping before JSON.parse, as scripts/check-type-check-coverage.mjs already does.
extends chains. Unlike vitest.config.*, tsconfigs inherit; a gate must resolve extends before reading paths.
The switch is not free, and the cost is not the gate's to hide. Putting a dependency's source into a consumer's program makes tsc check that source under the consumer's compilerOptions. On downstream-contract's OTHER half — its typecheck — still resolves @objectstack/spec types through dist, so a narrowed export type reads green (measured) #8021 that surfaced 2 TS2591 (spec source needs types: ["node"]) and 247 TS6059 from a rootDir that emits nothing under noEmit but is still enforced. So remediation is per-package and lands as its own card — the gate's job is to make the list of cards finite and unable to grow behind anyone's back, exactly as the vitest gate's header argues. A registry of the measured state, shrink-only and audited for set equality in both directions, is the shape that worked there.
Filing unassigned; found while implementing #8021 (PR #8176), and deliberately not fixed there — that card's scope is one package's tsconfig, and a repo-wide gate is its own blast radius.
The asymmetry
#7849 / PR #7966 landed
check:test-source-alias, which makes the source-resolution invariant mechanical rather than per-package goodwill: for every workspace package with tests, walk the imports reachable from those tests, keep the deps that resolve underdist/, simulate Vite's resolution overvitest.config.*, and demand that anything left is registered. Its own header states why a sweep is the wrong terminal state — "it leaves the NEXT package unguarded, and the symptom of the omission is a green test, so nothing would report the gap".That reasoning is axis-independent, but the gate is not. It reads
vitest.config.*and nothing else. No gate readstsconfig.json, so the identical exposure on the type axis is unguarded repo-wide, and its symptom is likewise a greentypecheck.Measured, on the one package that has now been fixed
#8021 measured it end to end on
packages/qa/downstream-contract. Identical checkout, identical staledist, a narrowing injected intopackages/spec/src/integration/connector.zod.tsin source only with no rebuild:pathsblock:tsc --noEmitexit 0, cleanpathsblock:src/additional-domains.fixtures.ts(35,3): error TS2322: Type 'string' is not assignable to type 'number'.That package is the repo's backward-compatibility gate, whose README promises "a removed or NARROWED export fails here" — so it was the loudest instance, which is why it got a card. It is not the only instance. The mechanism is generic and is in fact documented as the expected state in
.github/workflows/lint.yml, beside the type-check-debt re-measure step:Ordering does not reach it, same as both prior cards:
turbo.jsonorderstypecheckafter^build, soturbo run typecheckand CI are not the failing paths. What breaks is every path turbo does not mediate —pnpm --filter PKG typecheckinside a package, an editor's TS server, an agent in a tree built at an older commit. Those are exactly the paths a type contract is re-read on while someone is changing the dependency.What a gate would have to get right (from #8021's measurements)
Not a copy of the vitest one — the resolution algorithm and the trap are both different:
pathsalgorithm, not Vite's. An exact (star-free) key wins outright; otherwise the pattern key with the longest matching prefix wins and the captured text substitutes into the target's star. Vite's is first-match-wins with prefix matching for string finds.*is an exact match, so the bare@objectstack/specentry cannot swallow@objectstack/spec/uithe way a Vite object-form alias does (ENOTDIR). The tsconfig-shaped mistake is@objectstack/spec*— star not preceded by a slash — which matches every namespace and folds it onto whatever the target names. It does not crash:spec/src/index.tsre-exports most of the namespace surface, so it type-checks against the wrong module and stays green.src/stack.ts'sdefineStacktypes came fromdist.tsconfig.jsonis JSONC. Whole-line comment stripping beforeJSON.parse, asscripts/check-type-check-coverage.mjsalready does.extendschains. Unlikevitest.config.*, tsconfigs inherit; a gate must resolveextendsbefore readingpaths.compilerOptions. On downstream-contract's OTHER half — its typecheck — still resolves @objectstack/spec types through dist, so a narrowed export type reads green (measured) #8021 that surfaced 2TS2591(spec source needstypes: ["node"]) and 247TS6059from arootDirthat emits nothing undernoEmitbut is still enforced. So remediation is per-package and lands as its own card — the gate's job is to make the list of cards finite and unable to grow behind anyone's back, exactly as the vitest gate's header argues. A registry of the measured state, shrink-only and audited for set equality in both directions, is the shape that worked there.Prior art (searched before filing)
vitest.config.tslets a stale@objectstack/coredist decide its verdicts — #7668 fixed one, nothing stops the next #7849 / PR ci(test-alias): gate that a unit test judges source, not a sibling's dist #7966 — the runtime-axis gate and registry. Type resolution is explicitly outside their scope.packages/qa/downstream-contract— the repo's backward-compatibility gate for@objectstack/specreports 14/14 green against a spec change that breaks its own fixture, because it readsspec/dist#7991 / PR test(qa): make the spec backward-compatibility gate read spec source, not spec dist #8129 — the runtime half ofdownstream-contract.+-concatenated alias replacement — the #8020 defect one spelling over #8108 — defects in how the vitest gate reads alias replacements (template literal,+concatenation). Same family, and a warning about how easily a resolution-simulating gate reads a correct config as wrong.packages/spec/scripts/**在任何 tsconfig 的 include 之外:9 个 vitest 测试文件 + 全部构建/gate 脚本无类型检查覆盖 #5475 (closed), [P2] framework: 66 个包用 tsup 构建、无人做类型检查 —— 实测 18 个包共 380 处 code-tier 错误(#4118 的 framework 侧对应) #4311 — type-check coverage holes, a different question: those ask whether tsc reads a file at all, this asks what a file's imports resolve to once it does.Nothing open covers the type axis.
Generated by Claude Code