Skip to content

test: guard exports targets against having no source entry - #740

Merged
sroussey merged 1 commit into
claude/wonderful-turing-rjtcnx-ai-typesfrom
claude/optimistic-goldberg-gzrguj-exports-guard
Aug 10, 2026
Merged

test: guard exports targets against having no source entry#740
sroussey merged 1 commit into
claude/wonderful-turing-rjtcnx-ai-typesfrom
claude/optimistic-goldberg-gzrguj-exports-guard

Conversation

@sroussey

Copy link
Copy Markdown
Collaborator

Stacks onto #717 (claude/wonderful-turing-rjtcnx-ai-types). Base this PR's review on the diff of the single commit here; merge #717 first.

The regression class the existing guard cannot see

packages/test/src/test/util/ExportTypesPairing.test.ts pairs each exports condition branch's types string against the implementation string beside it. That comparison is purely textual — declarationFor() is a string transform and isViolation() compares two strings. Nothing ever touches the filesystem for a declared target, and nothing correlates a target to a source entry.

So a branch can be perfectly self-consistent and still name files that nothing builds. The concrete case: providers/anthropic has no src/*.browser.ts and no browser condition. Paste providers/openai's (correct) block into it —

"browser": { "types": "./dist/ai.browser.d.ts", "import": "./dist/ai.browser.js" }

— and the .d.ts correctly describes the .js, so every existing assertion passes while a browser consumer resolves to a module that is never emitted. The same hole is open for google-gemini, huggingface-inference and huggingface-transformers.

What this adds

Two repo-wide assertions in the same file:

  • declares only targets a source entry file can emit — every types target and every implementation target must derive to a src/<stem>.ts (or .tsx) that exists. ./dist/storage/bun.jssrc/storage/bun.ts; .d.cts/.d.mts/.cjs/.mjs suffixes strip whole. A target whose layout the derivation cannot describe (anything outside ./dist/) is reported, not skipped — silently skipping it would reopen the hole this closes.
  • **declares \types` before the implementation in the same branch** — Node stops at the first matching condition, so a typeskey declared after the implementation key is never reached. Recorded at collection time asDeclaredBranch.typesBeforeImplementation`.

Plus pure unit fixtures for the new derivation (nested stems, .d.cts stripping, the underivable case), so findViolations stays filesystem-free and the existing synthetic-manifest tests are unaffected.

Comment corrections (no behavior change)

  • The header block and the shorthand comment both claimed a branch with no types "falls through to an outer one". It does not: exports resolution stops at the branch that matched, and TypeScript then looks for a declaration beside the resolved implementation (.js.d.ts). The bug fix(providers): point each export condition's types at its own declaration #717 fixed was an explicitly wrong types, not a fall-through.
  • ALLOWED_MISMATCHES' JSDoc described only the mismatch case, but findViolations filters the allowlist over all violations including reason: "missing". Prose fixed; name and behavior unchanged.

Honest caveat

The src correlation is a proxy for "the build emits this", not a check of the build script's entry list. The true emitter is each package's build-code / build-browser entry array — a package that has src/ai.browser.ts but forgot to add it to build-browser still passes this test. Parsing build scripts is the stronger check and is out of scope here; the src check catches the copy-paste case that actually occurs.

.tsx is accepted as a candidate extension although the repo has zero .tsx files today — one array element, so a future React entry does not trip the guard.

Verification

Baseline (unmodified tree): 163 branches across 38 manifests contributing branches (39 manifests carry an exports key; examples/web has exports: {} and contributes none). All 163 declare a types key, so all 163 exercise both new assertions, and all 163 pass — zero manifest churn.

Four deliberate breakages, each reverted afterwards (git status clean before commit):

# Breakage Result
1 Phantom browser block added to providers/anthropic exports["./ai"] declares only targets a source entry file can emit fails with exactly two entries naming that branch — while the pre-existing pairs every \types` target…` assertion still passes. That contrast is the whole justification.
2 "types" moved below "import" in providers/openai exports["./ai"] declares \types` before the implementation in the same branch` fails naming that branch
3 One providers/xai import repointed to ./lib/ai.js the unrecognized assertion fails naming the branch and target
4 Anti-vacuity the existing finds condition branches to check passes with branches.length = 163 (> 50)

ExportTypesPairing.test.ts: 13/13 pass. bun scripts/test.ts util vitest: 50 files / 735 pass, 10 skipped. bun run test:vitest:integration: 49 files / 1615 pass, 42 skipped. npx tsc -b: error set byte-identical with and without this change (all pre-existing, none in the touched file).

Test-file-only change — no source or manifest files are touched.


🤖 Generated with Claude Code

https://claude.ai/code/session_01PDvMMv78PuEw4T5atLeJeS


Generated by Claude Code

`ExportTypesPairing.test.ts` only compared a branch's `types` string to the
implementation string beside it, so a branch could be internally consistent
and still name files nothing builds. Copying another package's `browser`
block into a manifest that has no `*.browser.ts` declares a matching
`.d.ts`/`.js` pair for a build that does not exist, and every existing
assertion passes.

Two repo-wide assertions close that:

- every `types` and implementation target must derive to a `src/<stem>.ts`
  (or `.tsx`) that exists; a target whose layout the derivation cannot
  describe is reported rather than skipped.
- `types` must precede the implementation key in the same object, since Node
  stops at the first matching condition.

The `src` correlation is a proxy for "the build emits this", not a check of
each package's build-script entry list: a source file that exists but was
never added to `build-code` still passes. It catches the copy-paste case.

All 163 branches across the workspace pass both today, so no manifest
changes. Also corrects three comments: a branch with no `types` is not typed
by an outer one (resolution stops at the matched branch and TypeScript looks
beside the resolved file), and `ALLOWED_MISMATCHES` silences missing
declarations too, not just mismatched ones.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDvMMv78PuEw4T5atLeJeS
@github-actions

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 64.44% 31812 / 49363
🔵 Statements 64.25% 32914 / 51228
🔵 Functions 65.51% 5986 / 9137
🔵 Branches 53.47% 16418 / 30704
File CoverageNo changed files found.
Generated in workflow #2971 for commit 984bedb by the Vitest Coverage Report Action

@sroussey
sroussey merged commit d86050a into claude/wonderful-turing-rjtcnx-ai-types Aug 10, 2026
10 checks passed
@sroussey
sroussey deleted the claude/optimistic-goldberg-gzrguj-exports-guard branch August 10, 2026 16:02
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.

2 participants