Derive workspace groups; check bundle identity on the dist target - #797
Merged
sroussey merged 1 commit intoAug 15, 2026
Conversation
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.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UW1Qr5mxetAQr61YKEY9nz
sroussey
merged commit Aug 15, 2026
758c0b2
into
claude/coverage-dist-bundle-fix-ew0vj8
11 of 12 checks passed
sroussey
added a commit
that referenced
this pull request
Aug 16, 2026
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
deleted the
claude/optimistic-goldberg-onotd9-coverage-derivation
branch
August 24, 2026 18:48
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #741 (
claude/coverage-dist-bundle-fix-ew0vj8). Three findings from reviewing that PR, fixed together because they share a root cause: a hand-copied list of workspace groups, and a coverage denominator that was not derived from it.H1 — bundle-identity checking is lost for every non-unit tier
Seven vitest jobs run, and only
test-vitest-distsetsWORKGLOW_TEST_TARGET=dist. It runs the unit tier. Every integration tier now resolves source, so nothing anywhere checks that the built bundles are wired correctly beyond "the entry loads and exports at least one symbol".The mechanism is CLASS identity, not registry identity
The review premise named a registry-identity check. That check would be vacuously green — do not ask for it.
packages/util/src/di/Container.tsstashesglobalContaineronSymbol.for("@workglow/util/di/globalContainer")precisely so duplicated bundle copies resolve one instance, andcreateServiceTokenreturns a plain string id. A duplicated copy of@workglow/aitherefore resolves the same registry; a===assertion on it can never fail.What actually breaks under inlining is class identity, exactly as
packages/task-graph/src/test-entry.tsdocuments it.registerAnthropicInlineconstructsnew AnthropicQueuedProvider(...)from a relative import inside theai-runtimemodule graph, while that class extends a base built fromAiProviderimported by specifier. Inline@workglow/aiintoai-runtime.js— a bundler flag, a droppedexternal, anexport *rewritten toexport { … } from— and the constructed instance stops beinginstanceoftheAiProviderevery consumer holds, while every existing check stays green.Two new UNIT-tier files, zero new CI jobs
Both run inside the two existing unit jobs (seconds against the current 232s step). Crucially,
register*Inlineneeds no API key: it constructs the provider and callsregisterProviderInline, which is registry bookkeeping and a strategy resolver — no network.PublishedEntryIdentity.test.ts— for every package publishing both./aiand./ai-runtime(16 today), imports<pkg>/ai-runtime, finds itsregister*Inlineexport, calls it, and asserts every provider it registered is aninstanceofanAiProviderthat@workglow/aipublishes and serves ≥1 run function.PublishedEntryExportParity.test.ts— imports each published entry alongside the source module it was built from and compares the export name sets. A bundle that lost a re-export still resolves and still evaluates, so the existing> 0bound passes over a bundle carrying one symbol out of ninety; names are the only observable that says the entry is intact.Both files note in-file that under the default
sourcetarget they pass trivially, so a green source run is not a bundle check — the run that means something istest-vitest-dist.Verified non-vacuous
Rebuilt
@workglow/anthropic'sai-runtimewith@workglow/aiinlined instead of external, then ran the dist target:Bundle restored; the check is green again.
Why not an integration-tier dist job
Rejected on cost, not minutes. That tier makes 131 live-API calls per run, including a 53.8s two-image OpenAI generation — a second run doubles vendor spend indefinitely. And it buys nothing on fork PRs, where
secrets.WORKGLOW_SECRETS_PASSPHRASEis unavailable, sodescribe.skipIfmeansregister()never runs at all.Two things found along the way
@workglow/aipublishes twoAiProviderclasses..and./workerare separatebun buildinvocations, so each bundle carries its own copy, and the five providers with a worker runtime (chrome-ai,tf-mediapipe,cactus,huggingface-transformers,node-llama-cpp) extend the worker copy on purpose. A consumer holdingAiProviderfrom@workglow/aiand testing a Chrome AI provider withinstanceofgetsfalsetoday. That seam is pre-existing, intentional, and belongs to the@workglow/aibuild rather than to this check, so the test accepts either published class and pins the count at ≤2. The third copy — one inlined into a provider's own bundle — matches neither and is what fails.NEEDS_NATIVE_RUNTIMEexemption map ships EMPTY. The four providers it was expected to need (node-llama-cpp,huggingface-transformers,cactus,stable-diffusion-server) all register cleanly with no native runtime present: every SDK, binding and server probe sits behind a run-fn, reached only by an actual generation call. Exempting them would have been a false statement costing five of sixteen candidates their coverage. The map stays as the seam, with the staleness guard, so a provider that genuinely cannot register has somewhere to go.M1 — the workspace group list was hardcoded in four places
WORKSPACE_GROUPS(scripts/lib/workspaceSource.ts), a verbatim copy inPublishedEntryImports.test.ts,PACKAGE_GROUPS(scripts/lib/testDiscovery.ts), and the coverageincludeglobs. The rootworkspacesfield was read by nothing butscripts/lib/util.ts. Adding a group topackage.jsontherefore silently no-oped everywhere: nothing errors, the walks just never look.New
scripts/lib/workspaceGroups.tsowns the derivation and nothing else — neither existing module should, for the same reasontestDiscovery.tsgives for owning its own ("Both must agree, so neither owns it"). Node-portable, noBun.Glob, becausevitest.config.tsloads it under Node. It throws on a pattern that does not reduce to exactly one scannable directory, rather than guessing a prefix and recreating the silent no-op.PublishedEntryImports.test.tscannot import fromscripts/— its own comment explains why (packages/testis a composite project rooted at./src; importing fromscripts/pulls those files into its program and breaksbuild-types). It re-derives in three lines from the same field, with a comment saying it is duplicated derivation code, not a duplicated list. The two new test files do the same.M2 —
examples/webdiluted the denominatorConfirmed:
publishConfig.access: "none",exports: {}, nomain/bin. The justifying comments atvitest.config.tsandscripts/workspaceSource.test.tsclaimed all three example packages were published and non-private — factually wrong, and both are rewritten.WorkspacePackagegrows apublishesfield (falseonly foraccess: "none"), and non-publishing workspaces are subtracted incoverage.excludeby package path, so the group globs stay derived and the exception carries its own reason. The gate isaccess: "none", notprivate:packages/test,providers/awsandproviders/cloudflareare allprivate: true, and aws/cloudflare have real suites that must stay counted.One correction to the premise:
examples/web/srcholds 37.ts/.tsxfiles, of which 3 are tests (nodeUsage.test.ts,TaskNode.test.ts,UsageStatus.test.ts) — not zero, as stated. It has atestscript and awebvitest project. That does not change the decision (the denominator is published API, and this package publishes none), only the wording, and no comment in this PR claims otherwise.Also included
coverage.excludesplicescoverageConfigDefaults.exclude, notconfigDefaults.exclude— which is vitest's test-file list. It is[]in vitest 4 whileconfigDefaults.excludeis not, so the swap would have silently dropped**/node_modules/**and**/.git/**; both are now stated explicitly.configDefaultsis kept forshared.exclude, which genuinely is a test-file list.**/testing/**dropped fromcoverage.exclude. Those 11 files are published API (@workglow/task-graph/test,@workglow/util/test— repository contracts, shared fake tasks, testing logger).**/__tests__/**already removes the one test file among them.listWorkspacePackages's barecatchnowcontinues only onENOENT(the ordinary "not a package dir" case) and re-throws anything else naming the path — a JSON syntax error orEACCESsilently dropped a package, which makes the source-rewrite plugin no-op for it and collapses that one package's coverage back ontodist/*..map(). One shared, stateless instance instead of 12 projects × 41 manifests ≈ 500 file reads at config load.workspaceSourcePlugintakes an optional second parameter defaulting to a fresh scan, so no caller changes.Skipped deliberately: Windows separator portability. The repo is POSIX-only by construction (
rm -rfinclean/build-clean,for f in …; dointypecheck:tests, bun engines), so fixing separators in these files would be misleading maintenance for a platform nothing else supports.Tests
The line-95 coverage guard becomes tautological once both sides derive, so it is replaced. Its original requirement — "must fail when a group is added to
package.jsonand nowhere else" — is no longer satisfiable literally, because with derivation nothing else needs to change. The guard's real job is to fail when the derivation is broken or bypassed:derives the groups from the root workspaces field, not from a list— parsespackage.jsonindependently inside the test; fails the moment the array is hardcoded back.finds at least one package in every declared workspace group— fires on a mishandled pattern or a typo'd directory.builds the coverage denominator from those same groups— reads the real config and comparescoverage.includeexactly, so a hand-edited glob is caught.mkdtemp:["./packages/*","./integrations/*"]→["packages","integrations"];["./a/**/c"]throws.distinguishes a malformed manifest from a non-package directory.keeps packages that publish nothing out of the denominator— asserts theexamples/web/srcexclusion and re-readsexamples/web/package.jsonto assertpublishConfig.access === "none", so the exception dies with the property that justifies it.counts every package that does publish— noaccess: "public"package'ssrcis excluded.attaches one plugin instance to every project— reference equality across all projects, the only thing stopping the hoist being undone.Do not read the PR coverage comment as a regression. Dropping
examples/web's 37 files from the denominator raises the figure; un-excluding the 11 publishedtesting/files nudges it. Both are corrections to what the denominator should have been.Verification (actual output)
The dist run is the one that matters for H1: it needs the built bundles and no API keys.
Generated by Claude Code