fix(providers): point each export condition's types at its own declaration - #717
Draft
sroussey wants to merge 17 commits into
Draft
fix(providers): point each export condition's types at its own declaration#717sroussey wants to merge 17 commits into
sroussey wants to merge 17 commits into
Conversation
Coverage Report
File CoverageNo changed files found. |
sroussey
marked this pull request as draft
August 7, 2026 21:55
This was referenced Aug 8, 2026
sroussey
force-pushed
the
claude/wonderful-turing-rjtcnx-ai-types
branch
from
August 13, 2026 03:55
d86050a to
f8a9718
Compare
This was referenced Aug 13, 2026
sroussey
added a commit
that referenced
this pull request
Aug 15, 2026
…els dropped (#795) * fix(providers): restore the runtime-agnostic exports the browser barrels dropped Five providers keep a hand-maintained `src/ai/index.browser.ts` beside `src/ai/index.ts`. Four of them omitted modules that carry no platform-specific code, so a `customConditions: ["browser"]` consumer could not import them at all. The omissions were invisible until the exports-map fix in #717 routed browser consumers to the browser declarations; they now read as `TS2305`. That they are accidental is evidenced by the bundles themselves: every omitted module is ALREADY compiled into that provider's browser bundle via the runtime entry (e.g. `registerOpenAi`, which IS in the browser barrel, imports `registerOpenAiImageValidator`), and no `src` tree of the five contains a single `node:` import. Only the `export *` line was missing. Restored, per provider: - deepseek: `assertNotTruncatedByReasoning`, `DEEPSEEK_DEFAULT_REASONING_ALLOWANCE`, `resolveMaxTokens` (`DeepSeek_Client`); `DeepSeek_ModelSearch_Stream` (`DeepSeek_ModelSearch`); `DeepSeekToolChoiceNotHonoredError`, `assertToolChoiceHonored`, `isForcingToolChoice` (`DeepSeek_ToolCalling`) - openai: `registerOpenAiImageValidator` (`OpenAI_ImageValidation`); `OpenAI_ModelSearch_Stream` (`OpenAI_ModelSearch`) - xai: `Xai_ModelSearch_Stream` (`Xai_ModelSearch`) - openrouter: `openRouterWorkerRunFnSpecs`, `deriveCapabilitiesFromMeta`, `inferOpenRouterCapabilities`, `OPENROUTER_RUN_FN_SPECS` (`OpenRouter_Capabilities`); `OpenRouterRawModel`, `OPENROUTER_FALLBACK_MODELS`, `fetchOpenRouterModels`, `mapOpenRouterModels`, `OpenRouter_ModelSearch_Stream` (`OpenRouter_ModelSearch`) `DeepSeek_ToolCalling` keeps the NAMED form the node barrel uses, which is what holds `DeepSeek_ToolCalling_Stream` out of the main-thread barrel on both platforms. ollama is deliberately unchanged: its `Ollama_ModelSearch` is exported by NEITHER barrel, so the two agree, and `_testOnly` is its only delta. `_testOnly` stays node-only everywhere — it is `@internal`, for `@workglow/test` alone, and belongs behind a `./test` entry the way `packages/ai` already did it. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(providers): re-export the node entry from the browser entry where no split exists `providers/llamacpp-server` and `providers/stable-diffusion-server` each carried a `src/ai.browser.ts` byte-identical to `src/ai.ts` and a `src/ai-runtime.browser.ts` byte-identical to `src/ai-runtime.ts`, all four naming RELATIVE specifiers (`./ai/index`, `./ai/runtime`). A relative specifier is resolved once, by the importing file's own path, and nothing in this toolchain substitutes `X.browser.ts` for `X.ts` on one: `--target=browser` changes the compile target, not the resolver, and a manifest's `browser` field applies to bare specifiers. So both entries already pulled in the same module graph — the declaration split was nominal, two `.d.ts` files kept equal only by hand. Each `.browser.ts` now re-exports its node peer, which cannot drift. These packages keep their `browser` condition: `--target=browser` produces a genuinely different bundle, so the entry earns its keep even though the source graph is shared. Bundle output is unchanged, byte for byte, for all four entries. The nine `packages/workglow/src/*.browser.ts` shims look like the same shape and are deliberately left alone — they re-export a BARE specifier (`@workglow/openai/ai`), which is re-resolved under the consumer's own conditions at every hop, so the two identical files land on different modules and their being identical IS the mechanism. Co-Authored-By: Claude <noreply@anthropic.com> * test(exports): flag a duplicate browser entry and pin browser barrel parity Three guards, all source-only (no build, works under `use-source`): - `duplicateBrowserEntryViolations` (ExportTypesPairing.test.ts) reports a `src/*.browser.ts` identical to the `src/<stem>.ts` beside it whose specifiers are ALL relative. The relative/bare distinction is the whole rule: a relative specifier resolves once, so both entries are the same module graph and the split is nominal; a bare one is re-resolved under the consumer's conditions at every hop, so two identical files land on different modules — which is why the `packages/workglow` shims are correctly identical and must never be reported. Both branches carry a fixture, since no violation survives in the tree. - `ExportBarrelParity.test.ts` parses the top-level re-exports of each provider's `src/ai/index.ts` and `src/ai/index.browser.ts` and asserts `node \ browser` equals a pinned `INTENTIONAL_NODE_ONLY` fixture (`_testOnly` for each of the five providers), with a staleness check so a pin that stops describing a real difference fails. The parser is regex-based, so an unclassifiable statement is REPORTED rather than skipped — the same call `buildEntryViolations` makes for an underivable dist stem. - `findBrowserBlock` replaces a top-level `?.browser` lookup in `browserSplitViolations`, which disagreed with its own recursing sibling `nodeImportTarget`: a `{ import: { browser: {…}, default: … } }` resolved its node target through the recursion while the browser block sat one level down, invisible to every rule keyed on it. Fixture added for the no-implementation case reached that way. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
…ation Twenty-five condition branches across nine workspace manifests declared a `types` target that did not belong to the implementation named beside it. Fifteen are in `providers/*` (openai, ollama, xai, deepseek, openrouter, llamacpp-server, stable-diffusion-server, cactus), where a `"browser"` block loaded `dist/ai.browser.js` but typed it with `dist/ai.d.ts`; ten are in `packages/workglow` (the nine provider re-export shims plus `./worker`, whose default branch typed `worker-node.js` with `worker-browser.d.ts`). The mismatch is not cosmetic for all of them. Comparing the resolved export surfaces of the emitted declarations shows seven provider branches where the browser build genuinely exports less than the node build — `_testOnly`, `registerOpenAiImageValidator` and `OpenAI_ModelSearch_Stream` (openai/ai), `_testOnly` (ollama/ai), two stream factories (ollama/ai-runtime), the reasoning and tool-choice helpers (deepseek/ai), ten model-search symbols (openrouter/ai), two (xai/ai), and a changed `getCactusModelCacheInfo` signature (cactus/ai-runtime). Importing `@workglow/openai/ai` under the `browser` condition compiled clean against `_testOnly`; the browser bundle's runtime exports do not include it. The remaining eighteen branches share a surface today and are corrected for consistency, since "identical right now" is exactly how the seven drifted. No build change is needed: `tsgo` already compiles all of `src/**/*`, so every `*.browser.d.ts` is emitted, and the browser bundles come from the existing per-package `build-browser` script. This is a manifest-only fix. `ExportTypesPairing.test.ts` walks every condition branch of every workspace manifest and asserts each `types` pairs with the implementation beside it, with an empty, documented allowlist so the class of drift cannot return silently. Consumer-visible change: browser builds of the seven differing entries now see the browser type surface, so code importing a node-only symbol under the `browser` condition starts failing to compile — correctly, since that symbol was already `undefined` at runtime. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The exports-map guard only recorded a branch when the node was an object that
already had a `types` string, so the two shapes that trigger the very defect it
guards against slipped past it silently:
- `"browser": "./dist/ai.browser.js"` (string shorthand) returned at the
`typeof node !== "object"` bail-out; and
- `"browser": { "import": "./dist/ai.browser.js" }` (object, no `types`) was
walked past.
Either one resolves through to the OUTER `types`, type-checking browser
consumers against the node build — the mismatch this guard exists to catch —
with the suite green. A branch that names an implementation must now declare
the declaration beside it, whether it declares a wrong one or none at all; both
kinds feed the single `mismatched` array, so the assertion and the
`ALLOWED_MISMATCHES` semantics are unchanged.
`declarationFor` also derived the wrong extension for `.cjs`/`.mjs`: it mapped
both to `.d.ts`, so a correct node16 manifest pairing `"require":
"./dist/x.cjs"` with `"types": "./dist/x.d.cts"` would have failed and pushed
the author toward the allowlist or toward an extension TypeScript will not
honor. `.cjs` is described by `.d.cts` and `.mjs` by `.d.mts`. No manifest uses
either extension today; this is pre-emptive.
The walk also now descends into an implementation key holding an object — the
`{ "import": { "types": …, "default": … } }` dual-package form — which it
previously skipped wholesale, so that shape was unchecked for the same reason.
Because no manifest violates any of these rules, the new code paths would ship
untested. The walk is extracted into `findViolations(manifest, exportsMap)`,
used by both the repo scan and five fixtures that exercise each path directly:
the two shapes above (one violation each, naming `ai.browser`), a correct
`.cjs`/`.d.cts` and `.mjs`/`.d.mts` pairing (none), a `.cjs` declared by a
`.d.ts` (one), and `"./package.json": "./package.json"`, which the module-path
test keeps out of the missing-declaration bucket (none). The `> 50` vacuity
guard on the real scan is unchanged; the scan still reports 0 violations over
163 collected branches.
Two smaller cleanups: `repoRoot` used `new URL(import.meta.url).pathname`,
which yields a wrong root for a percent-encoded path, and is now
`fileURLToPath` like the other tests in this package; and the workspace group
list was hardcoded `["packages", "providers", "examples"]`, so a fourth group
would have been dropped from the scan with no failure — it is now derived from
the root manifest's own `workspaces` globs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6huUY7hSkRbjun1P9HKsz
Correct `exports` manifests pair each condition's `types` with the implementation beside it, but TypeScript's own condition set is ["import", "types"] under moduleResolution "bundler" and ["node", "import", "types"] under node16/nodenext — "browser" is in neither. A browser app therefore bundles dist/browser.js while tsc type-checks it against the node declarations unless the consumer sets customConditions: ["browser"]. Document that in the Conditional Exports section and in the multi-runtime resolution rules, and pin examples/web's opt-in with an assertion so deleting that one line fails loudly instead of silently reverting the example project to node declarations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oVdDSRMJeALBPLDQf3DgH
`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
Two coverage gaps in the exports guard. Both are latent — no workspace manifest
violates either rule today (168 branches across 38 manifests, 0 new violations),
which is exactly why they went unnoticed.
Only the first implementation key per object was examined.
`IMPLEMENTATION_KEYS.find(...)` stopped at `import`, and the recursion skipped
string-valued implementation keys, so a flat dual-package object
`{types, import: "./a.js", require: "./a.cjs"}` never had its `require` paired
against anything — a `.cjs` declared by a `.d.ts` sailed through. The file's own
`.cjs` fixtures use the nested `require: {types, default}` form, which is what
hid it. Now every string-valued implementation key yields a branch.
That makes one object produce several branches, which would collide in `label()`
— the key for ALLOWED_MISMATCHES and the staleness check — so the
implementation key joins the branch identity and the label reads
`[condition > key]`. The shorthand form keeps its bare `[condition]`: its value
IS the implementation, so there is no key to name. ALLOWED_MISMATCHES ships
empty, so no allowlist migration is needed; only failure text changes.
Condition ORDER across sibling keys was never checked. `typesBeforeImplementation`
compares indices within one object, so a map whose branches are each internally
well formed but ordered `{types, browser: {…}, import}` passes every existing
check while TypeScript matches the outer `types` and never looks at `browser` —
the browser-typed-as-node bug this file exists to prevent, expressed through
ordering rather than through a wrong target. `{import, browser: {…}}` is the
runtime equivalent. Adds `orderViolations`, covering object and string-shorthand
condition keys alike, and asserts it over every workspace manifest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H797qbH356jjznKgUax63o
…756) Every check in ExportTypesPairing is internally self-consistent: it compares a branch against itself. A `browser` block that duplicates the default branch therefore passes all six while routing browser consumers to the NODE bundle. Adds `browserSplitViolations`, keyed on the one piece of evidence that tells a legitimate duplicate from a bug — whether a `<stem>.browser.ts` source entry exists beside it. The probe is injected so fixtures drive both sides. chrome-ai and tf-mediapipe have no such entry, so their duplicate blocks stay green and are left untouched; the rule fires the moment someone adds the source entry. Also widens `orderViolations`: it counted only string-valued implementation keys as shadowers, so a nested `"import": { default: … }` could hide a later `"browser"` unflagged. An object counts when it resolves unconditionally; one with no unconditional target does not, because Node falls through it. Claude-Session: https://claude.ai/code/session_01RomTUtZSTgUbFCYqFs4pcu Co-authored-by: Claude <noreply@anthropic.com>
Both are cases where the guard test does not guard what it claims. A `browser` block declaring only `types` was invisible to ALL SIX rules. `collectBranches` records a branch only when a string-valued implementation key is present, so such a block produces no branch at all — not even for the source-entry rule. Bundlers, meanwhile, enter the `browser` condition, match nothing, and fall through to the outer `import`, bundling the node build while tsc (customConditions ["browser"]) types against the browser declarations. That is exactly the browser-typed-as-node inversion this file exists to prevent, reached by omission rather than by a wrong target. `browserSplitViolations` now decides the empty-implementation case BEFORE the browser-source-entry guard. Hoisting past that guard is deliberate: the `packages/*` layout (stem `node`, no `src/node.browser.ts`) never reaches the guarded code, so the bug would otherwise go unreported for every package in the repo. Fixtures cover the openai manifest with its browser `"import"` deleted, the `packages/storage` layout with the `never` probe, and a healthy block that must stay silent — the first two assert `findViolations` and `orderViolations` are `[]` first, the go-red proof. "Source entry exists" proved only the `.d.ts` half. `build-types` runs `tsgo` over the whole `src` tree, so the file's existence is what makes the declaration appear; the `.js` comes from hand-written entry lists in each package's `build*` scripts, which nothing read. Add `providers/foo/src/ai.browser.ts` and a `browser` block, forget to append `./src/ai.browser.ts` to `build-browser`, and every check passed while `dist/ai.browser.js` was never emitted. `buildEntryViolations` collects every `.js`/`.cjs`/`.mjs` implementation target, derives its source entry from the dist stem, and requires a whole-token match in the joined text of the package's `build*` scripts — so duckdb's nested `--outdir` matches and `./src/ai.browser.ts` does not satisfy the stem `ai`. Unrecognized layouts are reported, not skipped. Zero violations across every manifest today, with exactly one exemption: `packages/workglow`, whose build is glob-driven. That exemption is pinned in `GLOB_BUILT_PACKAGES` and guarded by a test asserting the package really does hand its build to a repo-local `*.ts` program, so it dies if the package goes back to naming its entries. This lives in the test file that reviews the manifest rather than in a `publish-workspaces.ts` prepack assertion: a prepack assertion fires after review and after merge, and a guard that does not guard has to be able to fail the PR that introduces it. Also corrects the comment above "declares only targets a source entry file can emit", which claimed the source entry was the cheapest evidence the build produces the target at all — true of the declaration only.
…els dropped (#795) * fix(providers): restore the runtime-agnostic exports the browser barrels dropped Five providers keep a hand-maintained `src/ai/index.browser.ts` beside `src/ai/index.ts`. Four of them omitted modules that carry no platform-specific code, so a `customConditions: ["browser"]` consumer could not import them at all. The omissions were invisible until the exports-map fix in #717 routed browser consumers to the browser declarations; they now read as `TS2305`. That they are accidental is evidenced by the bundles themselves: every omitted module is ALREADY compiled into that provider's browser bundle via the runtime entry (e.g. `registerOpenAi`, which IS in the browser barrel, imports `registerOpenAiImageValidator`), and no `src` tree of the five contains a single `node:` import. Only the `export *` line was missing. Restored, per provider: - deepseek: `assertNotTruncatedByReasoning`, `DEEPSEEK_DEFAULT_REASONING_ALLOWANCE`, `resolveMaxTokens` (`DeepSeek_Client`); `DeepSeek_ModelSearch_Stream` (`DeepSeek_ModelSearch`); `DeepSeekToolChoiceNotHonoredError`, `assertToolChoiceHonored`, `isForcingToolChoice` (`DeepSeek_ToolCalling`) - openai: `registerOpenAiImageValidator` (`OpenAI_ImageValidation`); `OpenAI_ModelSearch_Stream` (`OpenAI_ModelSearch`) - xai: `Xai_ModelSearch_Stream` (`Xai_ModelSearch`) - openrouter: `openRouterWorkerRunFnSpecs`, `deriveCapabilitiesFromMeta`, `inferOpenRouterCapabilities`, `OPENROUTER_RUN_FN_SPECS` (`OpenRouter_Capabilities`); `OpenRouterRawModel`, `OPENROUTER_FALLBACK_MODELS`, `fetchOpenRouterModels`, `mapOpenRouterModels`, `OpenRouter_ModelSearch_Stream` (`OpenRouter_ModelSearch`) `DeepSeek_ToolCalling` keeps the NAMED form the node barrel uses, which is what holds `DeepSeek_ToolCalling_Stream` out of the main-thread barrel on both platforms. ollama is deliberately unchanged: its `Ollama_ModelSearch` is exported by NEITHER barrel, so the two agree, and `_testOnly` is its only delta. `_testOnly` stays node-only everywhere — it is `@internal`, for `@workglow/test` alone, and belongs behind a `./test` entry the way `packages/ai` already did it. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(providers): re-export the node entry from the browser entry where no split exists `providers/llamacpp-server` and `providers/stable-diffusion-server` each carried a `src/ai.browser.ts` byte-identical to `src/ai.ts` and a `src/ai-runtime.browser.ts` byte-identical to `src/ai-runtime.ts`, all four naming RELATIVE specifiers (`./ai/index`, `./ai/runtime`). A relative specifier is resolved once, by the importing file's own path, and nothing in this toolchain substitutes `X.browser.ts` for `X.ts` on one: `--target=browser` changes the compile target, not the resolver, and a manifest's `browser` field applies to bare specifiers. So both entries already pulled in the same module graph — the declaration split was nominal, two `.d.ts` files kept equal only by hand. Each `.browser.ts` now re-exports its node peer, which cannot drift. These packages keep their `browser` condition: `--target=browser` produces a genuinely different bundle, so the entry earns its keep even though the source graph is shared. Bundle output is unchanged, byte for byte, for all four entries. The nine `packages/workglow/src/*.browser.ts` shims look like the same shape and are deliberately left alone — they re-export a BARE specifier (`@workglow/openai/ai`), which is re-resolved under the consumer's own conditions at every hop, so the two identical files land on different modules and their being identical IS the mechanism. Co-Authored-By: Claude <noreply@anthropic.com> * test(exports): flag a duplicate browser entry and pin browser barrel parity Three guards, all source-only (no build, works under `use-source`): - `duplicateBrowserEntryViolations` (ExportTypesPairing.test.ts) reports a `src/*.browser.ts` identical to the `src/<stem>.ts` beside it whose specifiers are ALL relative. The relative/bare distinction is the whole rule: a relative specifier resolves once, so both entries are the same module graph and the split is nominal; a bare one is re-resolved under the consumer's conditions at every hop, so two identical files land on different modules — which is why the `packages/workglow` shims are correctly identical and must never be reported. Both branches carry a fixture, since no violation survives in the tree. - `ExportBarrelParity.test.ts` parses the top-level re-exports of each provider's `src/ai/index.ts` and `src/ai/index.browser.ts` and asserts `node \ browser` equals a pinned `INTENTIONAL_NODE_ONLY` fixture (`_testOnly` for each of the five providers), with a staleness check so a pin that stops describing a real difference fails. The parser is regex-based, so an unclassifiable statement is REPORTED rather than skipped — the same call `buildEntryViolations` makes for an underivable dist stem. - `findBrowserBlock` replaces a top-level `?.browser` lookup in `browserSplitViolations`, which disagreed with its own recursing sibling `nodeImportTarget`: a `{ import: { browser: {…}, default: … } }` resolved its node target through the recursion while the browser block sat one level down, invisible to every rule keyed on it. Fixture added for the no-implementation case reached that way. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
sroussey
force-pushed
the
claude/wonderful-turing-rjtcnx-ai-types
branch
from
August 16, 2026 16:31
6efb8a2 to
d02a046
Compare
Both guards landed in #717 look only at the layer they were written for, and the same defect class survives one level below. `ExportBarrelParity` keyed on the hard-coded `src/ai/index{,.browser}.ts` pair, so it never saw `src/ai/runtime.browser.ts` — the barrel a browser consumer gets under `@workglow/<vendor>/ai-runtime`. It now walks `providers/<vendor>/src` recursively and pairs every `<stem>.browser.ts` with the `<stem>.ts` beside it (~53 pairs, against 5 before). Three things that widening needs: - Specifier keys strip a trailing `.browser`, or every runtime pair reports as total drift (`* from "./common/Ollama_Client"` against `* from "./common/Ollama_Client.browser"`) and the guard says nothing. That makes a star-export comparison across a `.browser` sibling NOMINAL — and the recursive scan is what closes it, since `Ollama_Client.browser.ts` vs `Ollama_Client.ts` is itself a compared pair now. - A browser file whose entire surface is `* from "./<nodeStem>"` is skipped: it IS the node surface, and it is the shape #717 wants (llamacpp-server, stable-diffusion-server), which the widened scan now reaches. - `INTENTIONAL_NODE_ONLY` is rekeyed from the package dir to the browser FILE path, since a package now contributes several pairs. The vacuous-pass assertion (`pairs` equals the pinned keys) is removed rather than adjusted: 53 pairs against 5 pins makes equality flatly wrong, and any form of it means a correct new provider fails until somebody registers it as needing no exemption. Its two roles are stated separately instead — the scan found the tree, and it recursed into it. `ExportTypesPairing`'s `duplicateBrowserEntryViolations` scan is likewise recursive over `src` rather than a flat `readdirSync`. The relative/bare distinction is unchanged and is still the whole rule; all nine `packages/workglow` bare-specifier shims still classify BARE and stay unreported. Scope stays at `providers/` deliberately: `packages/` pulls in `packages/tasks/src/task/image/imageTextRender.browser.ts`, a genuine implementation split (one factory against the node module's fifteen names) needing a fifteen-name exemption for no benefit. This commit is intentionally red. It reports exactly two real defects, fixed in the commit that follows: providers/ollama/src/ai/runtime.ts exports * from "./common/Ollama_StructuredGeneration", * from "./common/Ollama_TextGeneration" but providers/ollama/src/ai/runtime.browser.ts does not providers/openrouter/src/ai/runtime.browser.ts is identical to providers/openrouter/src/ai/runtime.ts and names only relative specifiers, so both entries resolve the same module graph and the declaration split is nominal Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lgxtp7mQECdh7F2UT9CVwN
…ds found Both are the defect class #717 fixed at the entry layer, surviving one directory down in `src/ai/`. **ollama** — `runtime.ts` exports `Ollama_TextGeneration` and `Ollama_StructuredGeneration`; `runtime.browser.ts` did not. This is a source-compatibility fix, not a cosmetic one: before #717 the browser `types` condition pointed at the NODE declarations, so a browser consumer importing `createOllamaTextGenerationStream` from `@workglow/ollama/ai-runtime` compiled fine and got `undefined` at runtime. With #717's accurate types it becomes a hard TS2305 for anyone with `customConditions: ["browser"]`. Restoring the two `export *` lines is what makes the declaration honest. Neither module has a `.browser` variant, and neither imports a node builtin — both are already compiled into the browser bundle via `Ollama_JobRunFns.browser`. Verified: the rebuilt `ai-runtime.browser.js` gains no code, only the two names in the entry's export list (and a module reordering, since they are now direct entry re-exports). Every other provider's `runtime.browser.ts` was audited against its `runtime.ts` — deepseek, openai, openrouter and xai are in parity modulo the `.browser` specifier suffix. ollama was the only omission. **openrouter** — `runtime.browser.ts` was a byte copy of `runtime.ts` naming only RELATIVE specifiers, so both already resolved the same module graph and the declaration split was nominal: two `.d.ts` files kept equal by hand. It now re-exports its node peer, in the shape #717 established for `providers/llamacpp-server`. Rebuilt `ai-runtime.browser.js` is byte-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lgxtp7mQECdh7F2UT9CVwN
…d74u5n-export-guard-depth test(exports): widen both export guards one directory down, and fix what they find
`duplicateBrowserEntryViolations` exempts an identical browser/node pair whose
specifiers are all BARE, on the ground that the split happens one layer down.
That is a claim about the TARGET, and nothing resolved it — so a shim, its
`browser` block, its bundle and its declarations could all exist while
resolving browser consumers to the NODE build, with every rule in this file
silent.
That shape is in the tree today. Of the nine `packages/workglow` shims,
deepseek / ollama / openai / openrouter / xai do split, but
`@workglow/anthropic/ai`, `@workglow/google-gemini/ai`,
`@workglow/huggingface-inference/ai` and `@workglow/huggingface-transformers/ai`
expose only `{types, import}` — no `browser` condition at all. `@workglow/mlx`
has none either, so the reviewer's scenario (a new shim shipping a `browser`
condition that resolves browser consumers to the node build) is reachable now,
not hypothetical.
`inertBareShimViolations` asks the question the exemption assumes the answer
to. Its probe is deliberately NOT `"browser" in exports`: `providers/chrome-ai`
and `providers/tf-mediapipe` declare a `browser` block that names the NODE
target — honest there, since each ships one bundle — and a presence test would
call that a split. `subpathSplit` compares the declared implementations against
`nodeImportTarget` instead, so a split exists only when a browser consumer
lands somewhere a node consumer does not. ONE splitting specifier keeps the
shim; a pair naming no bare specifier belongs to the sibling rule; and an
`unknown` target is REPORTED rather than skipped, for the same reason an
underivable dist stem is — a target the rule could not resolve is where an
inert shim would hide.
The pair scan is hoisted to a module-level `entryPairs` since two tests now
read it, and its count assertion is loosened to a lower bound: the exact number
moves whenever a shim or provider entry lands, and the surviving shims are the
negative case this rule must keep passing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJRf3YFa8DjmjsZvXz8xDT
`@workglow/anthropic/ai`, `@workglow/google-gemini/ai`,
`@workglow/huggingface-inference/ai` and `@workglow/huggingface-transformers/ai`
expose only `{types, import}` — no `browser` condition at all — so the meta-
package's browser shims for them, their `browser` blocks, their bundles and
their declarations were four artifacts byte-identical to the node ones, and the
`types` repoint was a no-op.
Resolution-neutral, verified by hand: today a browser consumer of
`workglow/anthropic` resolves `dist/anthropic.browser.*` →
`export * from "@workglow/anthropic/ai"` → (no browser block) →
`providers/anthropic/dist/ai.*`. After removal it resolves `dist/anthropic.*` →
the same bare specifier → the same module, one hop earlier.
The four subpaths now carry the two-key shape `./tf-mediapipe` and
`./chrome-ai` already use. No build-script change is needed —
`packages/workglow/build.ts` globs `src/*.ts` — and nothing else imports the
deleted files, including `src/{common,browser,node}.ts`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJRf3YFa8DjmjsZvXz8xDT
…oviders `providers/llamacpp-server` and `providers/stable-diffusion-server` have no platform-specific source — `src/ai/` contains no `.browser.ts` — and their `src/ai.browser.ts` was `export * from "./ai"`. A relative specifier is resolved once by the file's own path, so the browser bundle was the same module graph and the split was nominal: two extra bundles and two extra declaration files kept equal only by construction. Both `.browser.ts` entry files, both `"browser"` blocks and the `watch-browser` / `build-browser` scripts are gone, and the two aggregators now match `providers/anthropic` verbatim. Verified guard by guard: `browserSplitViolations` reaches its empty-block branch only when `findBrowserBlock` returns something and then continues at the source-entry probe, so removing BOTH halves keeps it green; `buildEntryViolations` now sees only `./dist/ai.js` and `./dist/ai-runtime.js`, both named by `build-code`; and `ExportBarrelParity`'s pair count is unchanged, since those four were already filtered out by `isSiblingReExport`. That helper's doc named these two packages as the shape it wants; `providers/openrouter/src/ai/runtime.browser.ts` is the one live instance left, so it now names that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJRf3YFa8DjmjsZvXz8xDT
The check computed only `node.surface \ browser.surface` while its title claimed symmetry, so a name the BROWSER barrel exports and the node one does not was invisible — the same defect mirrored, and the one that hurts a browser consumer, who compiles against a symbol the node build cannot supply. Re-running the guard's own parser with the direction reversed found exactly one real case: `providers/ollama/src/ai/common/Ollama_JobRunFns.browser.ts` re-exporting `getClient`, `getModelName` and `loadOllamaSDK` from `./Ollama_Client.browser`. That line is DEAD, not intentional, so it is deleted rather than pinned: `runtime.browser.ts` already does `export * from "./common/Ollama_Client.browser"`, so all three names reach `@workglow/ollama/ai-runtime` regardless; `ai/index.browser.ts` never re-exports the run-fns module, so `@workglow/ollama/ai` never saw them; and nothing imports them from that path (only `OLLAMA_RUN_FNS` is imported from it). Pinning would have installed a permanent exemption for a redundant line, against this file's own stated philosophy. `INTENTIONAL_BROWSER_ONLY` therefore ships empty and documented, exactly as `ALLOWED_MISMATCHES` does, with the deleted line recorded and a warning not to add an entry to silence drift. The staleness test loops both maps. Three inline fixtures exercise the comparison directly, since no pair in the tree differs either way. The first is the go-red proof: the old one-direction check returned `[]` for exactly that input. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJRf3YFa8DjmjsZvXz8xDT
Every browser-split guard in the repo reads TEXT: `ExportTypesPairing` walks `exports` maps, `ExportBarrelParity` diffs `export` statements. An `export *` dropped from BOTH halves of a pair keeps barrel parity, keeps the manifest self-consistent, builds clean, and surfaces only when a downstream browser app upgrades and gets `TS2305`. Nothing in CI resolved a provider subpath under `customConditions: ["browser"]` — `examples/web/tsconfig.json` sets the condition but references no affected provider. `packages/test/src/browser-conditions/browserConditionResolution.types.ts` is a type-only fixture that never runs; the assertion is that it RESOLVES and COMPILES. Positives name a symbol each split subpath must still export; the negatives are what make them mean anything — `@ts-expect-error` on the node-only `_testOnly`, so a `customConditions` that silently stopped applying turns every one into `TS2578` rather than passing against the node declarations. Verified by deleting `customConditions` from the program: seven `TS2578`. It compiles as its own program (`tsconfig.browser-conditions.json`, `noEmit`), never as part of `packages/test`'s `build-types`, which runs under node conditions where every negative control would fail the build — `packages/test/tsconfig.json` excludes the directory for that reason. `workglow` joins `packages/test`'s devDependencies because the fixture also checks the meta-package's five splitting shims, the two-hop case no other program covers. The subpath list is not hand-maintained: a guard derives the expected set from the `providers/*` and `packages/workglow` manifests via `subpathSplit` — every subpath that really splits, no exemption list — so a new provider shipping a browser split fails until the fixture names it. A second guard pins both halves of the wiring, since a script nothing calls and a workflow step naming a script that does not exist each disable the check silently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJRf3YFa8DjmjsZvXz8xDT
test(exports): close the gaps the export guards left — resolve shim targets, compare both directions, compile the browser condition
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.
The real count: 25 branches, 9 manifests
Not ~24
./aientries in providers — 25 condition branches across 9 workspace manifests, and they are not all inproviders/*:providers/*(8 packages ×./ai+./ai-runtime, minus cactus's already-correct./ai)packages/workglow(9 provider re-export shims +./worker)Affected providers:
openai,ollama,xai,deepseek,openrouter,llamacpp-server,stable-diffusion-server,cactus.Shape in
providers/*: the"browser"block loads./dist/ai.browser.jsbut types it with./dist/ai.d.ts.packages/workglow's nine shims are the same shape; its./workeris the inverse — the default branch loadsworker-node.jsbut types it withworker-browser.d.ts.providers/anthropic,google-gemini,huggingface-inference,huggingface-transformers,mlx,node-llama-cpphave nobrowsercondition at all and are unaffected.tf-mediapipe,chrome-ai,cloudflarehave abrowserblock that points at the same files as the default — redundant, but internally consistent, so untouched.Phase 1 classification — do the surfaces actually differ?
I compared the resolved export surface (not the text) of each emitted
X.d.tsagainstX.browser.d.tsusing the TypeScript checker — the emitted files are one-lineexport * from …stubs, so a textual diff is meaningless.Type-surface difference (live bug) — 7 branches:
openai/ai_testOnly,registerOpenAiImageValidator,OpenAI_ModelSearch_Streamollama/ai_testOnlyollama/ai-runtimecreateOllamaTextGenerationStream,createOllamaStructuredGenerationStreamxai/ai_testOnly,Xai_ModelSearch_Streamdeepseek/aiassertNotTruncatedByReasoning,resolveMaxTokens,DEEPSEEK_DEFAULT_REASONING_ALLOWANCE,DeepSeekToolChoiceNotHonoredError,assertToolChoiceHonored,isForcingToolChoice,_testOnly,DeepSeek_ModelSearch_Streamopenrouter/ai_testOnly,OPENROUTER_RUN_FN_SPECS,fetchOpenRouterModels,mapOpenRouterModels,OpenRouterRawModel,OPENROUTER_FALLBACK_MODELS, …)cactus/ai-runtimegetCactusModelCacheInfoSame surface (wrong pointer, currently harmless) — 18 branches:
openai/ai-runtime,xai/ai-runtime,deepseek/ai-runtime,openrouter/ai-runtime, bothllamacpp-serverentries, bothstable-diffusion-serverentries (theirai.tsandai.browser.tsare byte-identical), and all 10packages/workglowbranches (its shims are byte-identical pass-throughs — the browser/node split really happens one layer down, in the provider's own exports map). No branch is browser-only or has a browser-superset surface: node ⊇ browser everywhere.The difference is real at runtime, not just in the declarations. Importing both openai bundles and diffing
Object.keys:Phase 1 — build and consumers
Per-condition declarations are already emitted; no build change is needed. Each package's
tsconfig.jsonusesinclude: ["src/**/*"]withemitDeclarationOnly, sotsgoproduces a.d.tsfor every source file —dist/ai.browser.d.tsanddist/worker-node.d.tsall exist after a clean build. The browser.jsbundles come from an existing per-packagebuild-browserscript (wired intobuild-js/build-package), which I initially missed by reading onlybuild-code. Verified by listingdist/afterbun run build:packages: all eight providers andpackages/workglowemit both.jsand.d.tsfor both targets. This is a manifest-only fix.Nothing in-repo depends on the mismatch.
customConditions: ["browser"]appears in exactly one place,examples/web/tsconfig.json, and that app does not reference any affected provider. Everything else (roottsconfig.json,tsconfig.typecheck.json, vitest) resolves under the default conditions, which are unchanged.packages/testimports_testOnlyfrom these providers under the default condition — unaffected.Phase 2 — decision
Point each condition's
typesat its own emitted declaration. The evidence rules out the cheaper options: 7 branches have genuinely different export surfaces, so "browser and node share a type surface" is factually false and documenting it would be wrong; and they should differ (the browser entries deliberately omit node-only test hooks and model-search code), so collapsing them onto one shared surface would be a product regression, not a de-drift. The fix costs nothing to build because the per-target declarations are already emitted, and I applied it uniformly to all 25 branches — including the 18 that agree today — because "identical right now" is exactly the state the seven divergent ones were in before they drifted. No package needed different treatment.Phase 3 — the guard
packages/test/src/test/util/ExportTypesPairing.test.tswalks every condition branch (recursively, so nested conditions are covered) of every{packages,providers,examples}/*/package.jsonand asserts eachtypesequals its siblingimport/require/defaultwith.js→.d.ts. Three assertions: the pairing invariant; a non-vacuity check (>50 branches found, so a broken scan can't pass silently); and a staleness check on the allowlist.ALLOWED_MISMATCHESis empty and documented as such — no legitimate exception exists today.A short paragraph was added to
docs/technical/19-build-system.md's "Conditional Exports" section stating the invariant and naming the guard.Consumer-visible type surface change — yes
For the 7 divergent branches, browser builds now see the browser type surface. Downstream code importing a node-only symbol under the
browsercondition will start failing to compile. That is the point of the fix: those symbols were alreadyundefinedat runtime in a browser bundle, so the old behavior was a silent runtime failure dressed up as a clean compile. The other 18 branches are a no-op for consumers.Verified
Everything below was run in this worktree and observed to pass:
bun install— clean.bun run build:types— 41/41 tasks successful.bun run build:packages— 81/81 tasks successful.npx vitest run packages/test/src/test/util/ExportTypesPairing.test.ts— 3/3 passed.providers/openai's./aibrowsertypesback to./dist/ai.d.ts→ the test failed with the exact branch named (providers/openai/package.json exports["./ai"] [browser]: types="./dist/ai.d.ts" but implementation is "./dist/ai.browser.js"); restored, test passes again.{ OPENAI, _testOnly } from "@workglow/openai/ai", compiled twice withmoduleResolution: bundler. After the fix — undercustomConditions: ["browser"]:TS2305: Module '"@workglow/openai/ai"' has no exported member '_testOnly'; under default conditions: exit 0. Before the fix, the browser-condition compile exited 0 — i.e. it silently accepted a symbol that does not exist in the browser bundle.npx vitest run packages/test/src/test/util/— 45 files, 669 passed / 6 skipped._testOnlyat runtime (OpenAI_ReasoningTemperature,ProviderUsageNormalization,CrossProviderRefusals) — 50/50 passed, confirming runtime resolution is untouched (typesis a TypeScript-only condition).npx eslinton the new test — clean.prettier --checkon the new test and all changed manifests — clean;docs/technical/19-build-system.mdreports a pre-existing prettier warning that is present onmainbefore my edit (unrelated table/JSON formatting), so I left it rather than adding reformatting churn.git statuschecked after every command; no stray manifest rewrites (bun run use-sourcewas never invoked).Not verified: I did not run the full repo test suite (only the
utilsection plus the three provider tests above), and I did not build a downstream browser app against the corrected types — the compile-condition probe is the evidence for that path.🤖 Generated with Claude Code
Generated by Claude Code