Uh oh!
There was an error while loading. Please reload this page.
test(qa): make the backward-compatibility gate's TYPE half read spec source, not spec dist - #8176
Conversation
…source, not spec dist (#8021) `packages/qa/downstream-contract` is the repo's backward-compatibility gate for `@objectstack/spec` (#2035), and its README gives `typecheck` a job the runtime suite cannot do: "a removed or NARROWED export fails here". It declared no `paths`, so under NodeNext tsc resolved `@objectstack/spec` through the workspace link and the `exports` map -- `dist/index.d.mts`, a build artifact. The type half was rendering a verdict about the last `pnpm build`, exactly as the runtime half was before #7991/#8129 aliased vitest to source. Measured, one variable moved, identical checkout and identical stale `dist`, with `label: z.string()` narrowed to `z.number()` in `spec/src/integration/connector.zod.ts` in SOURCE ONLY and no rebuild: without `paths` tsc --noEmit -> exit 0, CLEAN with `paths` src/additional-domains.fixtures.ts(35,3): error TS2322: Type 'string' is not assignable to type 'number'. `lib`/`types` are consequences of the switch, not preferences: spec source is written against spec's own environment, and without them the first run reported two TS2591 `Cannot find name 'process'`. `rootDir: "."` was dropped for the same reason -- it emits nothing under `noEmit` but still produced 247 TS6059, drowning the one error the gate exists to print. The demonstration is made permanent rather than historical. The type axis is pinned directly by a literal `import type` from `@objectstack/spec/conversions`, a namespace the published `exports` map does not carry, so it compiles only through the subpath rule. The bare-entry rule has no type-level discriminator -- measured, with the subpath rule kept and only the bare rule deleted tsc stayed CLEAN while `src/stack.ts`'s `defineStack` types came from `dist` -- so it is pinned the way `check:test-source-alias` pins the Vite side: simulate the resolution and assert where it lands. Reverse-verified in four directions; each goes red naming the exact defect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B3Kurx8qufrDzNjk4rag7V
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckNo hand-written docs reference the 1 changed package(s). ✅ |
hotlong
commented
Aug 12, 2026
PM review — |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#8021
packages/qa/downstream-contractis the repo's backward-compatibility gate for@objectstack/spec(#2035), and its README givestypechecka job the runtime suite cannot do: "a removed or NARROWED export fails here". It declared nopaths, so under NodeNext tsc resolved@objectstack/specthrough the workspace link and theexportsmap —dist/index.d.mts, a build artifact. The type half was rendering a verdict about the lastpnpm build, exactly as the runtime half was before #7991 / PR #8129 aliased vitest to source.The flip, measured — one variable moved
Identical checkout, identical stale
dist(.build-input-hash cf8635aa…, unchanged and unrebuilt across both runs), a narrowing injected into source only inpackages/spec/src/integration/connector.zod.ts:The frozen
DcConnectorfixture is declaredconst DcConnector: Connectorand assignslabel: 'DC HubSpot', so with source types this is a type error.Before —
tsconfig.jsonexactly as onmain:After — same tree, same probe, same
dist; only thepathsblock added:Line 35 is the fixture's
label: 'DC HubSpot'— the injected field, named. Probe reverted;packages/spec/src/integration/connector.zod.tsverified byte-identical afterwards (sha256 61e58c5e72fc403d…, matching its pre-probe value), andgit diff origin/main -- packages/spec/is empty.Turbo orders
typecheckafter^build, soturbo run typecheckwas never the failing path and needs no change. What breaks is every path turbo does not mediate:pnpm --filter … typecheckin the package, an editor's TS server, or an agent in a tree built at an older commit — i.e. exactly the paths this gate is re-read on while someone is changing the spec.No genuine narrowing surfaced
With the fix applied and no probe,
typecheckis clean (exit 0) and the suite is green — so this is not the "the repo has been shipping a break" case. The only errors the switch surfaced were mechanical consequences of putting spec source into this program, and both are fixed here rather than absorbed:TS2591: Cannot find name 'process'inspec/src/shared/lazy-schema.ts— spec source is written against spec's own environment, solibandtypesnow mirrorpackages/spec/tsconfig.json. This keeps every red the gate produces attributable to the spec contract, which is the only thing it may be read as.TS6059: … is not under 'rootDir'—rootDir: "."emits nothing undernoEmitbut is still enforced, and it drowned the one error the gate exists to print. Removed.The demonstration is permanent, not historical
Both halves of the pin live in
test/source-resolution.pin.test.ts, the file that already owns this invariant.Type axis, direct. A literal
import type { ConversionNotice } from '@objectstack/spec/conversions'. That namespace exists in the source tree and is deliberately absent from spec'sexportsmap, so tsc can reach it only through the subpath rule. It also corrects a comment in that file which said a literal specifier could not compile — true before this card, false after it.Bare-entry rule, simulated. It has no type-level discriminator: measured, with the subpath rule kept and only the bare rule deleted, tsc stayed clean while
src/stack.ts'sdefineStacktypes came fromdist. The obvious candidate — an identity check betweentypeof defineConnectorreached through both entries, the type-level twin of the existingtoBecase — was tried and rejected on measurement: with the two entries on different trees, tsc had not finished comparing spec's zod-derived types after nine minutes (the same 18-self-contained-entry-bundles duplication #8133 records, on the type axis). So the bare rule is pinned the waycheck:test-source-aliaspins the Vite side: simulate tsc'spathsresolution and assert where it lands, over the specifier set scanned out of this package's own files plus every namespace spec publishes.Reverse verification — four directions, each red, each naming the defect
pathsblock deleted entirely@objectstack/spec falls through 'paths' to node resolution, i.e. to disttsc:test/source-resolution.pin.test.ts(16,39): error TS2307: Cannot find module '@objectstack/spec/conversions'@objectstack/spec*paths key '@objectstack/spec*' matches namespaces by prefixAnchoring discipline (the PR #7778 constraint, in its tsconfig spelling)
One subpath rule for every namespace, never an enumeration — a hand-maintained list goes stale silently the first time a fixture reaches a new namespace, silently because the failure mode is a green typecheck. A tsconfig
pathskey without a*is an exact match, so the bare entry cannot swallow@objectstack/spec/uithe way a Vite object-form alias does. The tsconfig-shaped version of that mistake is spelling the key@objectstack/spec*— star not preceded by a slash — which is worse than the Vite trap: it does not crash withENOTDIR, it type-checks the fixtures against the wrong module and stays green. The fourth pin case refuses that spelling.Registry coupling
None to delete.
check:test-source-aliasreadsvitest.config.*only, and #8129 already removed this package's runtime registry entry; no typecheck-side registry exists (searchedscripts/— the only match for this package anywhere is a prose mention in a comment). That absence is filed as a finding, see below.Verification
pnpm --filter @objectstack/downstream-contract typecheck— clean, exit 0pnpm --filter @objectstack/downstream-contract test—Test Files 2 passed (2) / Tests 20 passed (20)(16 before, +4 pin cases)pnpm check:test-source-alias— OK, self-test OKnode scripts/check-type-check-coverage.mjs --self-test— OK; structural run OK, headline unmoved (the ledger is audited for set equality in both directions, so an unchanged green is the "did not rise" proof; this package carries no DEBT/TEST_DEBT entry, and itstypecheckalready includestest/**/*, so the new pin cases are read by the very check they guard)node scripts/check-nul-bytes.mjs— OK, plus a wider self-scan of both changed files for control byteseslinton the changed test file — cleanChangeset
None, deliberately.
@objectstack/downstream-contractisprivate: trueand never published, and no published package's code changes — so per the gate that reddened #8129, this PR releases nothing: no empty-frontmatter changeset (a real input tochangesets/action, and an all-empty set stalls a release silently and greenly, #4898), and theskip-changesetlabel instead.Generated by Claude Code