feat(devx): gate the TYPE axis of the source-resolution invariant - #8248
Merged
Merged
Conversation
) `check:test-source-alias` makes the source-resolution invariant mechanical on the runtime axis, but it reads `vitest.config.*` and nothing else — so the identical exposure on the type axis was unguarded repo-wide, and its symptom is likewise a green `typecheck`. `check:type-source-resolution` closes it: for every workspace package with a `tsconfig.json`, determine the files tsc puts in the program, collect the workspace deps they import (type-only imports included — the inversion of the runtime gate's rule), keep the ones whose declarations resolve under `dist/`, resolve each specifier through the effective `paths`, and demand that anything still landing on an artifact be registered. Deliberately not a port of the runtime gate. tsc's `paths` algorithm is exact-key-wins then longest-matching-prefix; Vite's is first-match-wins, and a resolver that used it reads correct configs as wrong. `tsconfig.json` is JSONC (55 of 76 carry comments) and inherits through `extends` (66 of 76), with `compilerOptions` merging shallowly so a child's `paths` replaces the parent's. The trap is different and worse: `@objectstack/spec*`, star not after a separator, folds the bare name, every namespace and every prefix-sharing sibling onto one target — and does not crash, it type-checks green against the wrong module. Also refused: a `paths` target that does not exist, which is not a rule at all — tsc falls back to node resolution silently, so it reads as coverage while providing none. The registry is the measured state: 51 of 76 packages, 222 package-dep pairs, 25 clean. It finds no new offender today. Its value is the ratchet — the remediation list is now finite, audited for set equality in both directions, and unable to grow behind anyone's back. Remediation is per-package and stays out of this change: putting a dep's source into a consumer's program makes tsc check that source under the consumer's `compilerOptions`, which on one package surfaced 2 TS2591 and 247 TS6059. No package `tsconfig.json` is touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018JWibNopHc8HYM18SSVqVA
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
hotlong
marked this pull request as ready for review
August 12, 2026 23:24
This was referenced Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #8180
check:test-source-aliasmade the source-resolution invariant mechanical on the runtime axis. It readsvitest.config.*and nothing else, so the identical exposure on the type axis was unguarded repo-wide — and its symptom is likewise a greentypecheck.This adds
check:type-source-resolution, wired into thelintjob beside its runtime sibling.What it checks
For every workspace package with a
tsconfig.json:files/include/exclude, with TS's defaults).dist/.pathsusing tsc's algorithm, and demand that anything still landing on an artifact be registered inKNOWN_DIST_RESOLVED_TYPE_IMPORTS.Specifiers are judged individually, never "does a
pathsblock exist" — a block covering eight of nine specifiers is eight-ninths of a verdict, and the ninth is silent.Deliberately not a port of the runtime gate
Four things differ, and each is a way a copied gate would be wrong:
extends(66 of 76),compilerOptionsmerging shallowly so a child'spathsreplaces the parent'sENOTDIR, loud, at run time@objectstack/spec*— star not after a separator — folds the bare name, every namespace and every prefix-sharing sibling onto one target, does not crash, and type-checks green against the wrong moduleAlso refused: a
pathstarget that does not exist on disk. That is not a rule — tsc falls back to node resolution, i.e. todist, silently — so it reads as coverage while providing none.The registry is large, and that is the point
51 of 76 packages, 222 package-dependency pairs, 25 clean. Exactly one package declares a
pathsrule for a workspace dep today, so this gate finds no new offender. Its entire value is the ratchet: the remediation list is now finite, audited for set equality in both directions, and unable to grow behind anyone's back. Stating the number plainly so a large registry is not read as a large finding.Remediation stays out of this PR by the card's own scope. It is not free: putting a dependency's source into a consumer's program makes tsc check that source under the consumer's
compilerOptions, which on one package surfaced 2TS2591and 247TS6059. No packagetsconfig.jsonis touched here.Verification
--self-testcovers 16 fixtures. Because every reader of a resolution config this repo has written has been wrong at least once — always by validating against the spelling its author had in mind rather than the one the repo uses — each case pins the correct form as well as the wrong one.The self-test passed on its first run, so it was mutation-tested to show it is not vacuous. Six mutations, each caught by its named assertion:
the `@pkg*` trap (star not preceded by a separator) was NOT flaggedan EXACT key declared after a matching pattern key did not win,the LONGEST matching prefix did not win— plus false reds on four compliant fixturesa TYPE-ONLY import of a dist-resolving dep was not counteda CORRECT config was reported because its comments were not strippedextendsresolution removeda correct `paths` block inherited through `extends` was not seenthe missing-target diagnostic did not say the target is missingReverse verification on real packages, direction predicted before running, every mutation restored (
git statusclean afterwards):downstream-contract: bare-entry rule deleted, subpath rule kept@objectstack/spec— the configuration tsc itself reports clean ondownstream-contract: subpath rule deleted, bare rule keptdownstream-contract: both rules replaced by@objectstack/spec*src/— a gate asking only "did it reach source" certifies this configpackages/core: a rule added for@objectstack/speconlyNarrow the entry to exactly: ["@objectstack/metadata-core"]The one package remediated on the type axis before this PR —
packages/qa/downstream-contract— is the one package the gate reads as clean. That is the false-positive check on real data.Cost and placement: full run is 1.2s and needs neither
node_modulesnor a build (verified: it was the only gate that passed in the fresh worktree beforepnpm install). It is placed in thelintjob, which has no build step — the same job as its runtime sibling.Gates run locally
check:nul-bytes,check:changeset-gate-self-tests,check:filter-alias-parity,check:node-version,check:required-contexts,check:shard-attestation,check:workflow-status-functions,check:type-check-coverage,check:type-source-resolution,scripts/check-changeset-no-major.mjs— all green. The list is the re-derivation fromscripts/pm/dispatch-gates.mjsagainst the actual changed paths.check:type-check-debtwas derived but not run locally: it refuses to measure without a built workspace closure (its own guard). No package source ortsconfig.jsonis touched here, so no ledger number can move; CI builds the closure before that step.Releases nothing
Root tooling only —
scripts/, a rootpackage.jsonscript, andlint.yml. No published package changes, so this takes theskip-changesetlabel rather than a changeset.Generated by Claude Code