Skip to content

chore: derive verify:dep-lockstep candidates from what actually enters each tsc program #1965

Description

@cliffhall

Follow-up to #1896 / PR #1962, from a review finding on that PR.

The gap

verify:dep-lockstep derives its candidate set from the packages the shared first-party sources (core/, test-servers/src, vitest.shared.mts) name directly. A package whose declarations reach a client's tsc program only through another package's .d.ts is invisible to it — so it can be skewed across installs and recreate the exact two-copy blow-up #1896 was about, while the guard stays green.

This is not hypothetical. @modelcontextprotocol/sdk is skewed today1.29.0 at the root, 1.30.0 in clients/web — and it does enter web's program from both install roots. It is never written in first-party code, because the shared sources import the split @modelcontextprotocol/client / core / server packages instead. The guard cannot see it.

Two derivations, measured

Lockfile dependency closure — unusable. Expanding the direct candidates over the lockfiles' dependencies yields 155 packages, 25 of them skewed, nearly all irrelevant transitive tooling:

chai, convert-source-map, qs, iconv-lite, hasown, side-channel, range-parser,
media-typer, type-is, estree-walker, tinyexec, es-object-atoms, …

That would force a 25-entry allowlist of meaningless entries that grows with every lockfile refresh — and it still misses the SDK, which isn't reachable through those dependencies maps.

What actually enters the program — correct and small. Running each client's tsconfig projects with tsc --listFilesOnly, mapping every emitted .d.ts to its owning install root and package, and keeping packages present under two different install roots gives, for clients/web, 15 packages — about 10 once nested within-one-install duplicates are dropped:

@modelcontextprotocol/{client,core,ext-apps,sdk}, @standard-schema/spec,
eventsource, jose, json-schema-typed, zod, …

That is the true signal: it's exactly the set that can produce two structurally-distinct copies of one type in a single program.

What to do

  • Swap the candidate derivation to the tsc --listFilesOnly approach, keeping the rest of the guard (top-level lockfile comparison, deny-by-default, TOLERATED_SKEW scoped to a major).
  • Note the cost: verify:typecheck-coverage already runs these same tsc invocations, so either accept paying them twice (~30–60s) or factor the file-listing into a shared helper both guards use — the latter is probably right, and scripts/lib/ already exists for exactly this kind of extraction.
  • Expect it to surface skews needing individual decisions on first run — at minimum @modelcontextprotocol/sdk (1.29.0 vs 1.30.0), plus jose and @modelcontextprotocol/ext-apps, which are currently allowlisted on the direct-import path. Each needs either alignment or a TOLERATED_SKEW entry with a real rationale.
  • Remove the KNOWN BOUNDARY note at the top of scripts/verify-dep-lockstep.mjs when this lands.

Metadata

Metadata

Assignees

Labels

choreMaintenance: deps, build tooling, CI, cleanup — no user-facing behavior changev2Issues and PRs for v2

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions