Skip to content

ci(release): type-check the packed tarball from the consumer side (closes #77) - #78

Open
yakimoto wants to merge 3 commits into
mainfrom
ci/consumer-type-resolution
Open

ci(release): type-check the packed tarball from the consumer side (closes #77)#78
yakimoto wants to merge 3 commits into
mainfrom
ci/consumer-type-resolution

Conversation

@yakimoto

@yakimoto yakimoto commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Closes #77.

The gap

The release e2e-smoke asserts every declared types target exists in the tarball. It never asserts they resolve — and only the second is something a consumer experiences.

dist/sdk-server.d.ts references a type from @anthropic-ai/claude-agent-sdk, an optional peer dependency. So the existence check stays green on a package that fails to type-check for anyone who did not install that peer. That is my own gate from #76, filed against myself.

The decision — option (a) from #77

Requiring the peer to type-check ./sdk-server is honest: that subpath exists to hand a config object to the Agent SDK, so a consumer using it has the SDK by definition. The gate now enforces both halves:

arm install shape must
root tarball, no optional peer type-check clean
./sdk-server tarball + optional peer type-check clean

It deliberately does not assert that ./sdk-server fails without the peer. That would freeze today's behaviour into the gate and turn a future switch to self-contained declarations (option (b)) into a spurious release failure.

The CHANGELOG note from d2f8b96 now says which option was chosen, as #77 asked.

Negative controls — run locally against a real build + pack, before this was pushed

A guard that has only ever been observed succeeding is indistinguishable from a guard that always succeeds. Three arms, all recorded:

NC1  leak the optional peer into a root-reachable declaration
     types ok: ./dist/index.d.ts, ./dist/sdk-server.d.ts          <- old check STILL GREEN
     ::error title=root entry, optional peer NOT installed does not type-check for a consumer
     node_modules/@wave-av/mcp-server/dist/index.d.ts(1,53): error TS2307   exit 1

NC2  point the subpath declaration at a nonexistent module
     types ok: ./dist/index.d.ts, ./dist/sdk-server.d.ts          <- old check STILL GREEN
     type resolution ok: root entry, optional peer NOT installed  <- root arm correctly unaffected
     ::error title=./sdk-server subpath, optional peer installed does not type-check for a consumer
     node_modules/@wave-av/mcp-server/dist/sdk-server.d.ts(1,53): error TS2307   exit 1

NC3  arm with no compiler installed
     ::error title=type-resolution arm cannot run::... the arm never type-checked anything   exit 1

happy path on unmodified HEAD
     type resolution ok: root entry, optional peer NOT installed
     type resolution ok: ./sdk-server subpath, optional peer installed        exit 0

The types ok line printing immediately above each failure is the point of the PR: that is the old gate being green on a package a consumer cannot use.

Also green locally: npm run lint, npm run type-check, actionlint, shellcheck.

Implementation notes worth a reviewer's eye

  • skipLibCheck: false in the probe tsconfig is load-bearing. With it on, tsc does not look inside node_modules declarations at all, and both arms would pass unconditionally.
  • Because of that, tsc also visits @modelcontextprotocol/sdk and the Agent SDK's own declarations. Diagnostics that do not name @wave-av/mcp-server are reported as ::warning, not failures — a gate that blocks our release on somebody else's .d.ts is a gate that gets switched off. It currently emits 22 such warnings, all real defects in the Agent SDK's bundled sdk.d.ts (TS2304 on names it never declares). Worth reporting upstream; not ours to gate on.
  • types: ["node"], not []. An empty list is not a purer test, just a less realistic consumer — it buried the real signal under ~140 diagnostics about the peer needing @types/node.
  • Versions come from package-lock.json, not require('<pkg>/package.json'). The first local run failed with ERR_PACKAGE_PATH_NOT_EXPORTED: the Agent SDK ships an exports map with no ./package.json entry — the same trap the bin check in this file already documents for our own package. A package missing from the lockfile now fails loudly rather than becoming an empty version string that installs whatever latest happens to be.
  • Each arm gets its own throwaway project. Re-running npm install --no-save inside the existing smoke dir rebuilds that tree from its empty package.json and can drop the tarball itself.
  • The probes use typeof import(...) — a purely type-level reference. The root entry is the executable (#!/usr/bin/env node, calls server.connect() at top level); a real import would start the MCP server and hang the job.

Honest scope limit

dist/index.d.ts is currently export {}; — the root entry has no library surface — so the root arm today proves that the root types target resolves and nothing past it. That is thin because the package is thin at root, not because the check is lax; NC1 shows it bites the moment anything is reachable from root. Stated in a comment in root.ts so nobody reads more into it later.

Out of scope but bundled — say if you want it split

.gitignore gains *.tgz (this smoke packs one into the repo root, so anyone reproducing it locally leaves a publishable tarball untracked) plus the usual local-secret and OS-cruft entries, which this public repo had none of. Nothing of that kind has ever been committed here — the entries exist so a stray git add -A cannot be the first time. Same finding as wave-av/adk#71.


Open in Devin Review

View with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is enabled.


Note

Low Risk
Changes are confined to release CI smoke scripts and repo hygiene; they do not alter runtime MCP server behavior or publish credentials beyond adding pre-publish checks.

Overview
Extends the release verify e2e-smoke so it proves published .d.ts files resolve for consumers, not only that they exist in the tarball (#77).

After npm pack and the existing tarball checks, CI runs two consumer-side tsc arms in separate throwaway projects: root with only the tarball (no optional @anthropic-ai/claude-agent-sdk peer), and ./sdk-server with the tarball plus that peer. typescript, @types/node, and the peer versions are read from package-lock.json so the gate does not float on latest.

New scripts/smoke/consumer-types supplies type-only probes (typeof import(...)), tsconfigs with skipLibCheck: false, and run-arm.sh, which fails on diagnostics in @wave-av/mcp-server or probe files while downgrading errors inside other node_modules declarations to warnings. test-classifier.sh pins that classification so mis-wired tsconfig errors cannot pass vacuously.

CHANGELOG documents the #77 contract; .gitignore adds *.tgz, .env*, and .DS_Store for local pack/smoke reproduction.

Reviewed by Cursor Bugbot for commit 291cfed. Configure here.


Summary by cubic

Adds consumer-side type checking to the release workflow so the packed tarball’s .d.ts resolve for real consumers. Implements #77 (root resolves without the optional peer; ./sdk-server resolves with @anthropic-ai/claude-agent-sdk) and hardens the gate to fail on misconfigured tsc runs.

  • Refactors
    • Adds two consumer type-check arms in release CI.
    • Packs and installs the real tarball into isolated throwaway projects; runs tsc with skipLibCheck: false.
    • Pins typescript, @types/node, and the peer versions from package-lock.json.
    • Tightens diagnostics: fails on errors in @wave-av/mcp-server or probe files; only excuses errors located under node_modules; fails the arm on unclassified tsc errors; adds scripts/smoke/consumer-types/test-classifier.sh to lock this behavior.
    • Adds consumer probes and tsconfigs under scripts/smoke/consumer-types; updates .gitignore and CHANGELOG.md.

Written for commit 291cfed. Summary will update on new commits.

Review in cubic

Note

Add consumer-side TypeScript type-checking of the packed tarball to the release gate

  • Adds two smoke arms to the release workflow: one that type-checks the root entry from the packed tarball without the optional peer, and one that type-checks the ./sdk-server subpath with @anthropic-ai/claude-agent-sdk installed.
  • Adds run-arm.sh which runs tsc with skipLibCheck disabled and classifies diagnostics: errors from this package's tarball or probe files are fatal, errors under node_modules from third-party declarations are downgraded to warnings.
  • Probe files (root.ts, sdk-server.ts) use type-only imports to force declaration resolution without executing the package.
  • Behavioral Change: the release now fails if the packed tarball's declarations do not resolve for consumers, but passes even when tsc exits non-zero due solely to third-party declaration errors.

Macroscope summarized 291cfed.

…oses #77)

The e2e-smoke asserted every declared `types` target EXISTS in the tarball.
It never asserted they RESOLVE — and only the second is a thing a consumer
experiences. `dist/sdk-server.d.ts` references a type from
@anthropic-ai/claude-agent-sdk, an OPTIONAL peer dependency, so the existence
check stays green on a package that fails to type-check for anyone who did not
install that peer.

Adopts option (a) from #77: requiring the peer to type-check ./sdk-server is
honest, because that subpath exists to build a config for the Agent SDK. The
gate now enforces both halves of the contract:

  root         must type-check WITHOUT the optional peer
  ./sdk-server must type-check WITH it

It deliberately does NOT assert that ./sdk-server fails without the peer —
that would freeze current behaviour into the gate and turn a future switch to
self-contained declarations (option (b)) into a spurious release failure.

Each arm installs the real packed tarball into its own throwaway project, so
its install shape is exactly what it claims to test; re-running
`npm install --no-save` in the existing smoke dir would rebuild that tree from
its empty package.json and could drop the tarball itself. Compiler, node types
and the peer are pinned to the versions package-lock.json already resolves,
read from the lockfile rather than `require("<pkg>/package.json")` — the Agent
SDK ships an exports map with no "./package.json" entry, so requiring its
manifest as a subpath throws ERR_PACKAGE_PATH_NOT_EXPORTED. A package missing
from the lockfile fails loudly instead of becoming an empty version string that
installs whatever `latest` happens to be.

skipLibCheck is off in the probe tsconfig — with it on, tsc never looks inside
node_modules declarations, which is the entire class being tested. That also
makes tsc visit dependency declarations, so diagnostics that do not name
@wave-av/mcp-server are reported as warnings: a gate that fails a release on
somebody else is a gate that gets switched off.

Proven by negative control before merge, both against a real build+pack:
  NC1  leak the optional peer into a root-reachable declaration
       -> `types ok` still green, root arm FAILS (TS2307)
  NC2  point the subpath declaration at a nonexistent module
       -> `types ok` still green, ./sdk-server arm FAILS (TS2307)
  NC3  arm with no compiler installed -> fails loudly, does not pass vacuously
  happy path on unmodified HEAD -> both arms green

Also extends .gitignore: the smoke packs a *.tgz into the repo root, and this
public repo had no entries for local-secret files or OS cruft. None has ever
been committed here — the entries exist only so a stray `git add -A` cannot be
the first time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 2, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 635218c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

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

@cursor

cursor Bot commented Aug 2, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_6a4c3044-db95-4fb8-9e35-6e05af426543)

@yakimoto

yakimoto commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 14 minutes

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6ce8645f-8d67-45c1-ac7a-9558284cf547

📥 Commits

Reviewing files that changed from the base of the PR and between 8411936 and 291cfed.

📒 Files selected for processing (11)
  • .github/workflows/release.yml
  • .gitignore
  • CHANGELOG.md
  • scripts/smoke/consumer-types/package.json
  • scripts/smoke/consumer-types/root.ts
  • scripts/smoke/consumer-types/run-arm.sh
  • scripts/smoke/consumer-types/sdk-server.ts
  • scripts/smoke/consumer-types/test-classifier.sh
  • scripts/smoke/consumer-types/tsconfig.base.json
  • scripts/smoke/consumer-types/tsconfig.root.json
  • scripts/smoke/consumer-types/tsconfig.sdk-server.json

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

devin-ai-integration[bot]

This comment was marked as resolved.

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
@yakimoto

yakimoto commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 39 minutes.

The type-resolution arm downgraded a whole class of its OWN mis-wiring to a
warning and then exited 0 claiming "type resolution ok", having compiled
nothing. Reported by Devin on #78; reproduced end-to-end before changing
anything.

The excuse bucket was keyed on "the line carries a file location", on the
premise -- written into the comment it replaced -- that tsc reports setup
errors without one. It does not:

  $ tsc -p tsconfig.base.json --pretty false
  tsconfig.base.json(4,5): error TS5023: Unknown compiler option 'foo'.
  RC=2

That line is not OURS, matches the old DEP_DIAG_RE, so it landed in OTHERS as a
::warning, GLOBAL stayed empty, and the RC!=0 branch printed "type resolution
ok" and exited 0. Same for TS5024, TS6046, TS5012. A release could ship
advertising consumer-verified types with nothing verified -- the failure mode
the arm exists to prevent, arriving through the arm itself.

The bucket is now an allowlist: a diagnostic is excused only if its path names a
directory under node_modules. Everything else -- located or not -- is GLOBAL and
fatal. node_modules/ is anchored to a directory boundary so a sibling that
merely ends in the name (my-node_modules/app.ts) buys no excuse.

Receipts, real tsc 5.9.3 against a mis-wired arm:

  old run-arm.sh -> "type resolution ok ... exited 2" EXIT=0
  new run-arm.sh -> "the arm verified nothing"        EXIT=1

Negative control, a genuine upstream regression in a dependency's own .d.ts,
which must stay excused so the gate does not become one that gets switched off:

  node_modules/fakedep/index.d.ts(1,30): error TS2304 -> warning, EXIT=0

test-classifier.sh pins all eleven cases and is read straight out of run-arm.sh
so it cannot drift from what it pins. It fails 5 of 11 against the old pattern,
which is the point of adding it.

Refs #77.
@cursor

cursor Bot commented Aug 2, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_28e18d60-2b27-4716-bbfc-734971d0b752)

@macroscopeapp

macroscopeapp Bot commented Aug 2, 2026

Copy link
Copy Markdown

Approvability

Verdict: Approved 291cfed

CI/CD-only changes adding type-resolution checks to the release workflow. No production code affected. The author owns all changed files. The open review comment describes a scenario the code explicitly handles with test coverage.

You can customize Macroscope's approvability policy. Learn more.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Open in Devin Review

Comment on lines +1 to +16
#!/usr/bin/env bash
# Pin the diagnostic classification in run-arm.sh.
#
# The bug this drill exists for: the excuse bucket used to be keyed on "the line
# carries a file location", on the belief that tsc reports setup errors without
# one. It does not -- `tsconfig.base.json(4,5): error TS5023` has a location --
# so a mis-wired arm that compiled nothing was downgraded to a warning and the
# gate exited 0 claiming "type resolution ok". Case 2 is that bug; it fails
# against the old regex and passes against the current one.
set -uo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Mirror the two patterns under test, read straight out of run-arm.sh so this
# drill cannot drift away from the thing it pins.
eval "$(grep -E '^readonly (OURS_RE|DEP_DIAG_RE)=' "$HERE/run-arm.sh")"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Classifier drill is never executed by any workflow

scripts/smoke/consumer-types/test-classifier.sh pins the two regexes in scripts/smoke/consumer-types/run-arm.sh:28 and scripts/smoke/consumer-types/run-arm.sh:57, but no workflow invokes it: .github/workflows/lint.yml only runs npm run lint / npm run type-check (both scoped to src/), .github/workflows/release.yml calls only run-arm.sh, and package.json declares no test script (the release gate even emits a no unit tests warning for exactly this reason). So the regression drill can silently rot the next time the classification logic is edited. Consider wiring it into the lint workflow or a test script.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

e2e-smoke asserts the .d.ts exists but never type-checks it — sdk-server.d.ts now references an optional peer dep

1 participant