Skip to content

Add workspace source resolution for accurate coverage reporting - #741

Draft
sroussey wants to merge 15 commits into
mainfrom
claude/coverage-dist-bundle-fix-ew0vj8
Draft

Add workspace source resolution for accurate coverage reporting#741
sroussey wants to merge 15 commits into
mainfrom
claude/coverage-dist-bundle-fix-ew0vj8

Conversation

@sroussey

Copy link
Copy Markdown
Collaborator

Summary

Adds a Vite plugin that resolves workspace package imports from built bundles to source files during testing, enabling accurate v8 coverage attribution and eliminating duplicate module identities from mixed import graphs.

Changes

  • scripts/lib/workspaceSource.ts — New module providing:

    • listWorkspacePackages(): Scans workspace groups (packages/, providers/, examples/) to enumerate all packages
    • distToSource(): Maps dist entry points back to their source counterparts (e.g., packages/ai/dist/node.jspackages/ai/src/node.ts)
    • workspaceSourcePlugin(): Vite resolver plugin that intercepts workspace specifier resolution and rewrites dist paths to source paths after normal resolution completes
  • scripts/workspaceSource.test.ts — Comprehensive test suite ensuring:

    • All workspace packages are discovered
    • Every published runtime entry has a source counterpart (fails if any entry lacks a source twin)
    • Dist-to-source mapping works correctly
    • Non-existent entries and source files are left unmapped
  • vitest.config.ts — Integration:

    • Attaches workspaceSourcePlugin to each test project when WORKGLOW_TEST_TARGET !== "dist"
    • Explicitly configures coverage denominator to packages/*/src/**/*.{ts,tsx} and providers/*/src/**/*.{ts,tsx} rather than relying on vitest's default (which omits untested modules)
    • Excludes dist, test harness, test files, and type declarations from coverage reporting
  • .gitignore — Adds coverage output directories (coverage/, .nyc_output/)

  • .claude/CLAUDE.md — Documents the coverage strategy and WORKGLOW_TEST_TARGET environment variable

Implementation Details

The plugin works by letting normal Vite resolution run first (preserving conditional exports behavior for node/browser/bun targets), then rewriting only the resolved path. This approach requires no per-package configuration and automatically covers all packages and subpath exports.

The test suite uses stubSpecsFor() from sourceStubs.ts to enumerate the same entry points that use-source stubs from, ensuring the two mechanisms cannot drift into disagreement about what a package exports.

WORKGLOW_TEST_TARGET=dist restores the old behavior for verifying bundle integrity; the Bun runner's native exports resolution provides an additional guard via the nightly parity workflow.

https://claude.ai/code/session_016khjNuSErZBHzW3aUw2SP4

@sroussey
sroussey force-pushed the claude/coverage-dist-bundle-fix-ew0vj8 branch from 8d27385 to ced2825 Compare August 11, 2026 03:08
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 72.59% 30710 / 42302
🔵 Statements 71.45% 32902 / 46046
🔵 Functions 73.01% 6359 / 8709
🔵 Branches 62.29% 16771 / 26921
File CoverageNo changed files found.
Generated in workflow #3207 for commit 0cd71d6 by the Vitest Coverage Report Action

claude and others added 9 commits August 16, 2026 16:31
`packages/test` reaches everything it exercises by package specifier, which
`exports` resolves to `dist/node.js`. Under v8 coverage that bundle is the
file instrumented, so executed lines were attributed to `packages/ai/dist/*`
and `packages/ai/src/**` read as barely covered — a package scored worse the
more of its behavior lived behind its public entry point.

Measured on the `ai-model` section: every executed line landed on a dist
bundle (task-graph/dist 29.7%, ai/dist 23.8%, util/dist 14.0%) and
`packages/ai/src` was absent from the report entirely.

The fix is a resolver, not a per-package config. `workspaceSourcePlugin` lets
normal resolution run first — so conditional exports still pick the
node/browser/bun target — and rewrites only the result, `<pkg>/dist/<entry>.js`
to `<pkg>/src/<entry>.ts`. Every package, every subpath export, and every
package added later is covered with no list to maintain. Unlike `use-source`
it writes nothing into `dist`, so it cannot clobber a build or leave a tree
that needs `use-dist` afterwards. `WORKGLOW_TEST_TARGET=dist` opts out; the
Bun runner still resolves `exports` natively, so the nightly parity workflow
exercises the bundles either way.

The coverage denominator is now stated explicitly rather than left to
vitest's default of "files loaded during the run" — that default omits the
modules no test imports at all, which are exactly the ones a coverage report
exists to surface, and makes a package's file list depend on which section CI
happened to run.

`scripts/workspaceSource.test.ts` fails if any published runtime entry lacks a
source counterpart: such an entry keeps resolving to its bundle, and the only
symptom is one package's coverage collapsing back onto `dist/*`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016khjNuSErZBHzW3aUw2SP4
The source-redirect plugin rewrites the RESULT of resolution, so resolution
itself still goes through the package's `exports`, which point at ./dist/*.
With no built entry there, Vite's resolution fails first, `this.resolve` yields
nothing, the plugin returns null, and the run dies with a generic
`Cannot find package '@workglow/ai/worker' imported from …` that blames the
manifest and names neither the plugin nor anything to do about it.

Keeps the workspace list as WorkspacePackage rather than bare names, so the
owning directory is available at the point resolution fails, and throws a
message naming the specifier, the owner, the importer and the remedy. Throwing
rather than warning is right: an unresolvable @workglow/* specifier already
fails the run, so this replaces a misleading message with an actionable one.

The remedy branches on whether the owner's dist holds any built entries, since
"never built" and "a new exports subpath was added without rebuilding" call for
different actions and the second reads as wrong advice to someone looking at a
populated dist. An empty dist directory — what `bun run clean` and
`use-dist --no-build` both leave behind — counts as never built.

ownerOf and unresolvedWorkspaceMessage are separated out as pure functions
because resolveId needs Vite's plugin context to drive and cannot be unit
tested; the message was additionally verified end to end by emptying dist and
running a suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H797qbH356jjznKgUax63o
Resolving @workglow/* to src is what makes the coverage numbers mean anything,
but the plugin is attached to every project unconditionally — not only to
coverage runs — so with the default in force no vitest job resolves a specifier
through `exports` at all. There is also no `bun test` job in the blocking
workflow. So after the source-resolution change, nothing that can block a merge
loads a built bundle: a `bun build` entry that silently dropped a re-export
would reach main and surface only in the nightly Bun parity run, which is
explicitly informational, runs on a cron, and excludes six sections.

Adds test-vitest-dist: reuses the existing build-output artifact and runs the
unit tier with WORKGLOW_TEST_TARGET=dist. It is in cleanup's needs list, since
cleanup deletes the artifact it downloads.

Scoping the plugin to coverage runs instead would not have worked: scripts/test.ts
adds --coverage whenever CI is set, so in CI every run is a coverage run and
would still resolve to src.

Also skips --coverage for a dist-targeted run. The denominator names package
sources, so such a run reported all ~1286 of them at 0% — not a measurement of
anything, and it is what lets the new job reuse test:vitest:unit unchanged and
produce no fragment for merge-vitest-coverage.

The CLAUDE.md and vitest.config.ts notes claimed bundle integrity was covered by
the nightly parity run; both now say what actually guards it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H797qbH356jjznKgUax63o
The coverage-flag test spawned the runner with `{...process.env, CI: "1"}` and
let WORKGLOW_TEST_TARGET come from the ambient environment. The new
test-vitest-dist job exports that variable for its whole step, so inside that
job the source-target case inherited `dist` and became a second copy of the
dist case — asserting `--coverage` is present while the runner correctly
omitted it. It failed in the one job it was added to support.

Both cases now state the target explicitly, so the assertions hold whatever the
runner is invoked under.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H797qbH356jjznKgUax63o
`examples` is a first-class workspace group in both workspaceSource.ts and
testDiscovery.ts, and all three example packages — @workglow/cli, @workglow/eval
and @workglow/web — are published with none marked private. They carry 24 test
files between them. The denominator listed only packages/* and providers/*, so
their source was rewritten to src, executed by their own tests, and then left
out of the measurement entirely: a coverage run from examples/cli reported
`All files 0%` with no file rows at all.

Pins coverage.root to the config's own directory rather than making the globs
absolute. coverage.root is the documented base for include/exclude, and this
config is invoked from package directories too
(vitest run --config ../../vitest.config.ts), where a repo-relative glob would
otherwise match nothing. It also does not depend on whether vitest accepts
absolute glob patterns.

Excludes examples/*/src/test/**: those dirs hold the example suites plus the
occasional non-`.test.` helper the filename rules cannot catch, and counting a
test helper is what the adjacent excludes already exist to prevent.

Guards the invariant that broke, in workspaceSource.test.ts: every entry in
WORKSPACE_GROUPS must be a prefix of some coverage.include glob, read from the
actual config so the two cannot drift apart again. A missing group is invisible
in a coverage report — it shows a shorter file list, not an error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H797qbH356jjznKgUax63o
…pecifier causes (#759)

Two guards on the dist-targeted test path.

`WORKGLOW_TEST_TARGET` had two independent readers, each treating
anything that was not literally "dist" as source, silently. A workflow
edited to "dist " or Dist turned test-vitest-dist into a byte-identical
rerun of test-vitest-unit: green, a full runner slot spent, no bundle
coverage. `resolveTestTarget` is now the single reader — unset and empty
mean source, everything else throws naming the value, with no
lowercasing and no prefix matching.

`unresolvedWorkspaceMessage` offered only "never built" or "stale dist",
but resolution fails from the IMPORTER's node_modules: under isolated
linking an undeclared workspace dependency, or a subpath absent from the
owner's exports, fails while the owner's dist is fully populated. The
causes are now ranked, so the built/stale pair is offered last and
labelled as the remaining likely cause.


Claude-Session: https://claude.ai/code/session_01RomTUtZSTgUbFCYqFs4pcu

Co-authored-by: Claude <noreply@anthropic.com>
`resolveId` — the one function the workspace-source plugin exists for — had
no test, and nothing asserted the plugin reaches the generated projects.
`workspaceSource.test.ts` covered only the pure helpers, and both CI modes
pass under either resolution: `test-vitest-unit` and `test-vitest-dist` run
the same files and the same assertions, differing only in which files load,
so no suite distinguished "resolved to src" from "resolved to dist".

The failure that leaves is silent. Hoisting `plugins:
[workspaceSourcePlugin(__dirname)]` from the per-project object to the root
`defineConfig` — a natural "one instance instead of N" cleanup, and the
exact mistake the comment there warns about — makes every project resolve
`@workglow/*` through `exports` to dist again. All tests pass,
merge-vitest-coverage succeeds, and the only symptom is entry-point
behavior reading as uncovered. The same silence covers a regression inside
`resolveId`.

Extract the hook body into `resolveWorkspaceSourceId(packages, context,
source, importer, options)`, which takes the plugin context as a parameter
so a recording stub can stand in for Vite; `workspaceSourcePlugin` becomes
a one-line adapter. `WorkspaceResolveContext` declares `resolve` with
method syntax on purpose — parameter bivariance is what lets Vite's real
`PluginContext` satisfy it — and is generic in the result so the adapter's
return type stays `ResolvedId`, which is what Vite's `resolveId` hook is
declared to return. Both together are what keep the call site cast-free
(checked against the installed vite typings, where the non-generic form
needs one).

New tests: the rewrite, a non-workspace specifier short-circuiting before
`this.resolve`, an external resolution left alone, dist output with no
source twin left on the built file, the unresolved-specifier diagnostic
(a branch with zero execution anywhere before this), and `skipSelf: true`
plus verbatim forwarding of the hook's own options. Plus a "plugin
attachment" block that re-imports the real `vitest.config.ts` and asserts
every project carries `workglow:workspace-source` under the default target
and none does under `dist`.

Both directions stub `WORKGLOW_TEST_TARGET` explicitly and unstub
afterwards: `test-vitest-dist` runs this file with that variable ambient,
so a test reading the ambient value would pass in one CI job and fail in
the other.
`test-vitest-dist` is the one blocking job that resolves `@workglow/*`
through `exports`, but it runs the UNIT tier only — while the source
rewrite applies to EVERY vitest job. The integration/rag/provider suites
previously loaded the bundles and now load src, so a bundle reachable only
from an `.integration.test.ts` file lost its blocking check.

Two entries lost every check: `@workglow/openrouter/ai-runtime` and
`@workglow/huggingface-inference/ai-runtime`, imported only from
provider-api integration files, whose section the nightly Bun parity run
also excludes. Concretely: a `bun build` change dropping
`registerOpenRouterInline` from `providers/openrouter/dist/ai-runtime.js`
leaves the file in place, satisfies the dist-must-exist requirement, passes
all of CI, and breaks consumers only after publish.

`PublishedEntryImports.test.ts` makes the check total instead of
tier-shaped: it enumerates every workspace manifest's `exports`, resolves
each subpath under the Node conditions only (`node`/`import`/`default`,
walked in declaration order the way Node does, so `types`/`browser`/`bun`
are stepped over rather than entered), and dynamically imports each
resulting specifier, asserting the module is non-empty. Under
`WORKGLOW_TEST_TARGET=dist` that one unit-tier file loads every published
bundle; under the default target it costs nothing, since it loads the same
source the rest of the suite already does.

Adding `workglow` to `packages/test`'s devDependencies is the larger half:
it brings the meta-package's own entries and, transitively, the provider
bundles those re-export.

The enumeration is local rather than shared with `scripts/lib/sourceStubs`:
`stubSpecsFor` returns dist targets rather than import specifiers, and
`packages/test` is a `composite` project rooted at `./src`, so importing
from `scripts/` would put those files in its program and break
`build-types`.

Anti-vacuity assertions (over 60 entries across over 20 packages, every
target `./dist/**.js`) keep a mis-typed walk from passing as a short list,
and both exemption maps are staleness-checked against the enumeration. Two
exemptions, each with its reason: `@workglow/cli` (uncheckable — an example
app `packages/test` does not depend on, so under isolated linking the
specifier does not resolve from here at all) and `workglow/auto-bootstrap`
(imported, but exempt from the non-empty assertion: it registers providers
as a side effect and exports nothing by design). New packages default to
checked.
Three findings from the coverage-derivation review, fixed together because
they all come from the same place: a hand-copied list of workspace groups
and a denominator that was not derived from it.

H1 — bundle-identity checking was lost for every non-unit tier. Only
`test-vitest-dist` sets `WORKGLOW_TEST_TARGET=dist`, and it runs the unit
tier, so nothing checks that the built bundles are wired correctly beyond
"the entry loads and exports something". The failure that hides there is
CLASS identity: a provider's `register*Inline` constructs its provider class
from a relative import while that class extends `AiProvider` imported by
specifier, so inlining `@workglow/ai` into `<provider>/ai-runtime.js`
produces an object that is no longer `instanceof` the base every consumer
holds — and nothing goes red.

Two new UNIT-tier files strengthen the dist sweep rather than adding a CI
job (`register*Inline` needs no API key, so this is affordable where it
already runs):

- `PublishedEntryIdentity.test.ts` registers every provider publishing both
  `./ai` and `./ai-runtime` and asserts each registered provider is an
  instanceof an `AiProvider` that `@workglow/ai` publishes, and serves at
  least one run function. Verified non-vacuous by rebuilding
  `@workglow/anthropic`'s `ai-runtime` with `@workglow/ai` inlined: the
  check fails, naming the package.
- `PublishedEntryExportParity.test.ts` imports each published entry
  alongside the source it was built from and compares export NAME sets. A
  bundle that lost a re-export still resolves and evaluates, so the existing
  "exports something" bound passes over it.

Asserting on the SERVICE registry instead would have been vacuous: the
global DI container is stashed on a `Symbol.for` key so duplicated bundle
copies share one instance, and tokens are plain strings.

M1 — the workspace group list was written out by hand in four places.
`scripts/lib/workspaceGroups.ts` now derives it from the root manifest's
`workspaces` field (Node-portable, since `vitest.config.ts` loads it under
Node) and throws on a pattern that does not reduce to one scannable
directory. `WORKSPACE_GROUPS` is gone; `PACKAGE_GROUPS` and the coverage
`include` globs derive from it. `PublishedEntryImports.test.ts` cannot
import from `scripts/` (composite project rooted at `./src`), so it
re-derives locally, with a comment saying why.

M2 — `examples/web` diluted the denominator. It declares
`publishConfig.access: "none"`, `exports: {}`, and no `main`/`bin`: none of
its source is published API. `WorkspacePackage` grows a `publishes` field
and non-publishing workspaces are subtracted from `coverage.exclude` by
path. The gate is `access: "none"`, NOT `private` — `packages/test`,
`providers/aws` and `providers/cloudflare` are private and the latter two
carry real suites. The two comments justifying the old behavior were
factually wrong and are rewritten.

Also:

- `coverage.exclude` splices `coverageConfigDefaults.exclude`, not
  `configDefaults.exclude` (a test-file list); the two entries the latter
  was silently supplying are now stated.
- `**/testing/**` dropped from `coverage.exclude`: those 11 files are
  published API (`@workglow/task-graph/test`, `@workglow/util/test`).
- `listWorkspacePackages` re-throws a non-ENOENT manifest error naming the
  path instead of dropping the package silently.
- The workspace scan and the source-resolving plugin are hoisted out of the
  per-project map — one shared, stateless instance instead of ~500 manifest
  reads at config load.

The line-95 coverage guard was tautological once both sides derive, so it is
replaced by tests that fail when the derivation is bypassed, when a declared
group scans to nothing, when the denominator stops matching those groups,
when the plugin stops being shared, and when the `examples/web` exclusion
outlives the property that justifies it.

Reported coverage numbers will move: dropping `examples/web`'s 37 files
raises the figure, un-excluding the 11 published `testing/` files nudges it.

Claude-Session: https://claude.ai/code/session_01UW1Qr5mxetAQr61YKEY9nz

Co-authored-by: Claude <noreply@anthropic.com>
@sroussey
sroussey force-pushed the claude/coverage-dist-bundle-fix-ew0vj8 branch from 758c0b2 to 27b4958 Compare August 16, 2026 16:34
claude and others added 5 commits August 16, 2026 16:35
…st target

Every vitest project now attaches the source-resolving plugin unconditionally,
so under the default target nothing resolves `@workglow/*` through `exports`.
`publish-all`'s test step therefore stopped touching the bundles it is about to
version-bump and push, and `WORKGLOW_TEST_TARGET=dist` existed only as an inline
`env:` block on one CI job.

- `test:vitest:dist` is the one definition of the dist target, unit tier only
  (matching the CI job; the other tiers want keys, databases and model
  downloads). The variable is set INSIDE the script, so dropping it now means
  deleting the script call.
- `publish-all` runs it after `rebuild`/`format` and before `bunset`.
- The `test-vitest-dist` job invokes the script instead of restating the
  variable.

The dist run also had no precondition. `use-source` writes stubs carrying
`SOURCE_STUB_SENTINEL` and nothing read it, so under a stubbed dist
`@workglow/ai` and `@workglow/ai/worker` collapse onto one source module: every
cross-entry `instanceof` succeeds trivially and export-name parity compares a
file with itself. `assertNoSourceStubs` now runs at CONFIG LOAD under the dist
target, which kills the whole run before any suite can pass vacuously.
`containsSourceStubSentinel` is the node-portable half of the existing Bun-only
`isSourceStub`, since vitest.config.ts is loaded by Vite under node.

`shared.env` hands the validated target down to the workers: `packages/test` is
a composite program rooted at ./src and cannot import scripts/lib/*, so
re-deriving it there would reintroduce the silent-comparison bug
`resolveTestTarget` exists to remove.

`scripts/` was in no CI type gate — `typecheck:budget` globs packages|providers
and `typecheck:tests` globs packages/*/tsconfig.test.json — while now holding
vitest.config.ts's resolution logic. `tsconfig.scripts.json` + `typecheck:scripts`
close that, and the first run surfaced four real errors, all fixed here:
three `WorkspacePackage` literals missing the required `publishes`, a
`TestProjectConfiguration` callback annotation that ignores the union's string
variant, a missing root `vite` devDependency behind
`import type { Plugin } from "vite"`, and a `coverage.root` key that vitest 4
neither types nor reads (its provider derives coverage roots from the resolved
project configs), so it was inert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lgxtp7mQECdh7F2UT9CVwN
…nk it

The sweep skipped the exact regression it exists to catch. A candidate whose
`ai-runtime` module exports no `register*Inline` was pushed onto
`withoutInlineRegistrar` and `continue`d, contributing NO assertion, and the
only place that surfaced was a message printed on failure. Sixteen workspace
packages publish both `./ai` and `./ai-runtime`, and every bound was `> 4`, so
eleven of the sixteen could drop their registrar and leave the file green.

Measured rather than reasoned: renaming `registerAnthropicInline` and rebuilding
the provider left all six assertions passing, with `@workglow/anthropic` gone
from the sweep and nothing saying so.

- `NO_INLINE_REGISTRAR` declares the skips, beside `NEEDS_NATIVE_RUNTIME`. Sole
  member today is `@workglow/mlx`, whose registrar is `registerMlx` — no
  `Inline` suffix — because `MlxProvider` stays unavailable until an mlx-lm
  runtime is bundled. `withoutInlineRegistrar` is compared for EQUALITY against
  its keys, so an undeclared skip fails.
- The exemption-pinning test covers both maps: every key names a real candidate,
  every reason is longer than a word.
- `MINIMUM_RUNTIME_CANDIDATES` replaces `candidates.length > 4`, and
  `checkable.length` is now an equality against candidates minus the declared
  native-runtime exemptions rather than a second floor.
- The `providers.size > 4` bound becomes two statements that cannot be satisfied
  by a shrunken sweep: the registered package set EQUALS the checkable set minus
  the declared no-registrar packages, and each registration is checked for at
  least one provider name, collected into an offenders array so one no-opping
  registrar reports itself.
- The base-class allowance is now two-sided and target-keyed: `dist` requires
  exactly 2 distinct classes (two bundles really loaded), `source` exactly 1
  (the resolution plugin really attached). `<= 2` was satisfied by either, so it
  could not tell a real dist run from a source run mislabelled as one. This is
  the in-process proof that the loaded modules are bundles.

With the rename still applied the new file fails, naming `@workglow/anthropic`
in both the undeclared-skip check and the registered-package set; reverted and
rebuilt, it passes on both targets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lgxtp7mQECdh7F2UT9CVwN
…d74u5n-published-entry-guards

test: declare the identity sweep's skips instead of letting them shrink it
…import

`vitest.config.ts` called `assertNoSourceStubs` as a MODULE-LEVEL side effect
whenever the target was `dist`, and `scripts/workspaceSource.test.ts` imports
that module with `WORKGLOW_TEST_TARGET` stubbed to `dist` to check plugin
attachment. `scripts/*.test.ts` is unit-tier, so on a `use-source` tree an
ordinary `bun run test:vitest:unit` died there with advice to run `use-dist` —
undoing the documented no-build dev mode the run had never left. CI stayed
green because CI trees are really built. Two further call sites in the same
file import the config with AMBIENT env, so under `test:vitest:dist` they
re-ran the whole scan too.

The check is now a `configResolved` hook on a guard plugin attached to every
project under `dist`. Vitest resolves each project's config at startup, so the
run still dies before any suite can report a pass over stubs; the verdict is
computed once and re-thrown, so one scan serves all twelve projects instead of
printing a 41-entry message twelve times.

Rejected alternatives: `buildStart` (vitest does not reliably drive build hooks
for a test-only server, while `configResolved` is guaranteed by `resolveConfig`
per project); exporting `buildProjects(target)` (leaves the config module
itself un-exercised, which is what makes the attachment test worth having); an
early check in `scripts/test.ts` (misses `vitest run --project <n>` and adds a
second place that knows the rule).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJRf3YFa8DjmjsZvXz8xDT
… anchor coverage globs

Four related follow-ups to the dist-target work.

**The parity sweep is skipped under the source target.** Under `source` the
resolving plugin rewrites `import(specifier)` to exactly the path
`sourceCounterpart()` computes, so both sides are the SAME module and ~90 cases
assert `X === X`. They now skip, so the report distinguishes "checked" from
"not applicable" instead of showing 97 green rows that compared nothing.
Nothing stops being loaded: `PublishedEntryImports.test.ts` imports every
published specifier unconditionally and does carry signal under source. An
anti-vacuity test asserts the target is one of the two known values, so a broken
`test.env` cannot make every case skip and leave `test-vitest-dist` green having
compared nothing.

**The stub guard now covers every entry `use-source` writes.** It read only
`exports`, but `use-source` also stubs `bin` targets, and two are named by no
export entry at all: `examples/eval` declares NO `exports`, only a `bin`, and
`examples/cli`'s `bin` is not among its export targets. Both were stubbed and
unguarded, while `publish-workspaces.ts` (a full dist walk) did refuse them.
`guardedDistTargets` adds them — deliberately not via `stubSpecsFor`, which also
maps each target to a source counterpart and throws for a target that is neither
`.js` nor `.d.ts`; the guard has no use for that mapping and should not acquire
a new way to fail during config resolution.

**An unreadable entry is unproven, not built.** `probeSourceStub` returns three
states, because the two callers fail safe in opposite directions: removal must
not delete an artifact it could not read, while the dist guard must not accept
one as a real bundle. The guard's message has a section for each.

**Coverage globs are anchored at the repo root.** A `--project` run — every
package's own `test` script, and `turbo run test -- --coverage` — starts in a
package directory, so a relative pattern's meaning depends on which directory
the provider resolves it against. Measured on `@vitest/coverage-v8` 4.1.10 the
two forms give an identical denominator from both the repo root and a package
directory, so this is defensive rather than a fix for an observed miss; it is
worth pinning because a denominator that silently loses its untested half reads
as a BETTER number, not as an error. The per-package `/src/**` exclusions are
anchored to match, or they subtract from nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJRf3YFa8DjmjsZvXz8xDT
…hook

Run the dist stub guard in a plugin hook; widen it; anchor the coverage globs
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