Skip to content

fix(oracles): install the gap CLI's TypeScript under an alias, so it never changes a project's own - #18

Merged
KyleAMathews merged 3 commits into
mainfrom
overnight/b-f19
Nov 11, 2026
Merged

KyleAMathews merged 3 commits into
mainfrom
overnight/b-f19

Conversation

@KyleAMathews

Copy link
Copy Markdown
Contributor

Summary

The gap CLI's TypeScript dependency is now the npm alias "typescript-for-tanstack-oracles": "npm:typescript@^5.9.2". The CLI resolves it by that name with createRequire, and no code path in the package resolves typescript by its plain name.

The problem

Since d8adfee, @tanstack/oracles depends on typescript for the gap CLI. Under npm, that dependency is hoisted into an adopter's node_modules. I reproduced it offline from the packed tarball and the npm cache (npm 11.17.0):

  • In a project without TypeScript, npm installed node_modules/typescript at 5.9.3, plus .bin/tsc and .bin/tsserver.
  • In a project that ran npm i -D typescript@5.4.5 and then installed the package, npm saved ^5.4.5 and deduped the project's root TypeScript up to 5.9.3. Both require('typescript') and tsc changed. This is the mitt case.

Why an unscoped alias name

The brief suggested @tanstack/oracles-typescript. I tested it, and it makes tsc worse. npm links the bins of every package hoisted to the root. It resolves a bin conflict by the first package in sorted path order (@npmcli/arborist sortNodes, bin-links linkGently). node_modules/@tanstack/... sorts before node_modules/typescript. So with the scoped alias, even a clean install of a project with its own typescript@5.4.5 linked .bin/tsc to the alias's 5.9.3. The name typescript-for-tanstack-oracles sorts after typescript, so on a clean install the project's own typescript keeps tsc. Its scope cannot collide with a real package in the adopter's tree.

Package managers: npm and pnpm both install an npm: alias that a published package declares. pnpm puts nothing at the adopter's root and links only direct dependencies' bins. I checked this with pnpm add --offline <tarball>: there is no typescript and no tsc at the root, and the CLI's createRequire resolves the alias (5.9.3). pnpm pack and npm pack both keep the alias as written in dependencies, and the tarball bundles no node_modules. pkg.pr.new (publish --pnpm) publishes that packed tarball, so previews keep working. publint --strict and sherif pass.

Changes

  • package.json: the alias replaces the plain dependency. The lockfile changes only that importer entry.
  • oracle-gaps/check.mjs: the CLI loads typescript-for-tanstack-oracles. The API-less refusal is unchanged.
  • The gap oracles' TypeScript (gap-report-model-oracle.mjs, and the static, probe-form, and mutant-validity oracles) and tests/oracle-gaps-owns-through-calls.test.ts resolve the same alias. They use the CLI's TypeScript, not the workspace's.
  • tests/package.test.ts: a new test builds and packs the package. It installs the tarball with npm (--prefer-offline) into two scratch projects. It works fully offline from this machine's npm cache.
    • In a project with no TypeScript, node_modules/typescript is absent. The test writes a small oracle and source, and runs the CLI's analysis (oracle-gaps --source src/order.ts --contexts 1). The CLI exits 0 and finds mutants. The test also checks the known residue as documented behavior: .bin/tsc links to the alias.
    • A second project first installs typescript@5.4.5 and then the tarball. This is the incremental case where a plain dependency deduped the root. The test checks that the root typescript is still 5.4.5 and that .bin/tsc links to the alias. After npm ci, the root is still 5.4.5 and .bin/tsc links to ../typescript/bin/tsc.

Evidence

This change is to packaging. No oracle owns how npm lays out an adopter's tree, and npm is the authority. So the focused install test checks the law, as the brief asked. The gap oracles' apiLessTypescript fault still covers the CLI side: the CLI never reads the package's TypeScript.

  • Law: installing @tanstack/oracles with npm puts no typescript package where the adopter resolves it. A project with its own TypeScript keeps its version for require, and on a clean install for tsc too. The CLI's analysis works in a project with no TypeScript. Authority: maintainer brief F19 (2026-11-10), and the d8adfee decision that the CLI analyzes with its own TypeScript 5.
  • Reach: the test builds both scratch projects on every run. One has no TypeScript. The other has its own TypeScript 5.4.5, outside the alias's range, installed incrementally and then cleanly with npm ci. A fresh install of both together does not tell old from new, because npm nests a plain dependency there.
  • RED: with the parent's production (plain typescript dependency, plain resolution), pnpm exec vitest --run --maxWorkers=2 tests/package.test.ts -t "installs with npm" fails with expected true to be false: node_modules/typescript is present. With the same old-style tarball, the incremental half alone fails with expected '5.9.3' to be '5.4.5': the root typescript was deduped up to 5.9.3.
  • GREEN: the same command passes, and the whole tests/package.test.ts gives 6 passed. The default suite gives 56 passed. The gap oracles that now load the alias pass: static, classify, probe-form, and mutant-validity give 31249 passed. The end-to-end gap oracle's fixed seed, run under the e2e lock, gives 1 passed (216 s). tsc, eslint, publint --strict, and sherif are clean.
  • Mutant: M1, a scoped alias name (@tanstack/oracles-typescript), fails with expected '../@tanstack/oracles-typescript/bin/t…' to be '../typescript/bin/tsc'. M2, the alias dependency but plain typescript resolution in the CLI, fails because the CLI exits 3 in the project without TypeScript (expected 3 to be +0).
  • Limits:
    • npm still links the alias's tsc and tsserver into .bin in two cases. The first is a project without TypeScript. The second is an incremental npm install @tanstack/oracles into a project that has TypeScript. There .bin/tsc points at the alias until a clean install (npm ci, or removing node_modules). A later plain npm install leaves it there.
    • This is still strictly better than before. The incremental install used to raise the root typescript itself from 5.4.5 to 5.9.3.
    • npm has no per-dependency way to stop the residue. Only bundling the dependency would, and the brief rules that out for pkg.pr.new.
    • Yarn PnP probably applies its TypeScript compatibility patch only to the name typescript. So under PnP the alias may run unpatched. This is untested, and so are Yarn and Bun in general.
    • Whether the name typescript-for-tanstack-oracles exists on the npm registry is unverified, because offline npm view returned ENOTCACHED. An alias does not fetch by its own name. So this matters only for a project that also depends on a real package of that name.
    • The test needs npm on the PATH, and a warm npm cache or registry access.

Decisions for the maintainer

  • The alias name: I chose typescript-for-tanstack-oracles over the brief's scoped example, for the bin-precedence reason above. Choose another name only if it sorts after typescript.
  • Decision taken: keep the unscoped alias. An optional peer dependency would bring back the TypeScript 7 crash that d8adfee fixed. The .bin/tsc cases above remain, and bundling TypeScript, the only npm-side fix, is ruled out.

This pull request and its description were written by Isaac.

KyleAMathews and others added 3 commits November 10, 2026 23:27
npm hoisted the plain typescript dependency to adopters' roots, which gave
a project without TypeScript a typescript package and deduped a project's
own range up to 5.9. The CLI now resolves typescript-for-tanstack-oracles
(npm:typescript@^5.9.2), an unscoped name that sorts after typescript, so
on a clean install the project's own typescript keeps its tsc bin. A test
packs the package, installs it with npm, and runs the CLI where the project
has no TypeScript.

Co-authored-by: Isaac <no-reply@databricks.com>
…ct's TypeScript

The owned project now installs typescript@5.4.5 first and the tarball
second, the case where a plain dependency raised the root to 5.9.3, and
checks the clean install with npm ci. The bin residue is asserted as
documented behavior, and the changeset states that it lasts until a
clean install.

Co-authored-by: Isaac <no-reply@databricks.com>
@changeset-bot

changeset-bot Bot commented Nov 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 361d268

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@tanstack/oracles Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Nov 11, 2026

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2f8dd28e-6568-4980-99d4-80ce865fc85e
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@pkg-pr-new

pkg-pr-new Bot commented Nov 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/TanStack/oracles/@tanstack/eslint-plugin-oracles@18
npm i https://pkg.pr.new/TanStack/oracles/@tanstack/oracles@18

commit: 361d268

@KyleAMathews
KyleAMathews merged commit 819bd3a into main Nov 11, 2026
8 checks passed
@KyleAMathews
KyleAMathews deleted the overnight/b-f19 branch November 11, 2026 07:57
KyleAMathews added a commit that referenced this pull request Nov 11, 2026
Replace interim rules with what landed: the take-back check now fails at
load (#15); lint reports empty tests, and the presets ignore tool configs, so
a custom ignores must merge the preset's (#16); the gap check's TypeScript is
an npm alias, with only a .bin/tsc residue (#18); enumerated sources declare
states, and the reach test enforces them (#19).

Co-authored-by: Isaac <no-reply@databricks.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant