Move 13 shipped openspec change docs under openspec/changes/archived/. Add docs/features/git-diff-panel, docs/plans/post-review-backlog, and docs/research/cross-app-contract-ssot.md (the research behind the @boocode/contracts SSOT work). Update BOOCHAT.md, BOOCODER.md, and boocode_roadmap.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
28 KiB
Research: Long-term fix for BooCode's hand-synced cross-app type contracts
How to eliminate the duplicated, hand-synced TypeScript wire contracts shared across apps/server ↔ apps/web ↔ apps/coder (a single source of truth), and which approach to choose. Evidence mode: strict (default — sourced claims only; the recommendation rests on corroborated or codebase evidence, never reasoning alone).
Summary
The textbook fix is real and well-understood: put each shared type in one place that all three apps import, instead of keeping two or three hand-copied versions in sync. The strongest version of that is a small shared package (e.g. @boocode/contracts) where each contract is defined once — and for the contracts that are already validation schemas, define the schema once and derive the type from it so the validator and the type literally cannot disagree. This is the mainstream industry pattern and it is technically possible here: the project already does exactly this between two of its apps (apps/coder imports a built @boocode/server package).
But "do it now" did not survive scrutiny. The team already looked at this and deliberately chose not to build the shared package at its current (solo/small) scale, judging it "not worth the Docker/build-order risk," and that decision is the most recent word on the matter. The web app — the hardest consumer — has never imported a workspace package, so the path is unproven for it specifically. And the real duplication is messier than a clean "move five types" job: there is a live, undetected mismatch in one contract today, a whole extra copy of types in a second web app nobody counted, and a Zod-version tension lurking. So the honest answer is staged, not a single big-bang migration.
Recommended: do the cheap, high-value cleanup now (close the actual drift gaps — including the one that's already broken — and extend the existing guard-rails); treat the full shared-package unification as a deliberate, de-risked investment gated behind a small proof-of-concept, not a foregone conclusion. Whether to spend that larger effort now is a judgment call about your scale that only you can make.
- Confidence: Medium
Research Results
The end-state the industry converges on is a shared package, ideally schema-first. Across independent sources, the portable way to share types across a pnpm monorepo that mixes a Node app (NodeNext resolution) and a browser app (Vite/Bundler resolution) is a workspace package whose package.json exports map points at compiled dist/ with a types condition listed before default (A11, A12, A14, A15). For contracts that are also runtime validators, the schema-first pattern — define a Zod schema once and derive the static type via z.infer — makes type/validator drift structurally impossible, because the type and the validator are the same definition; z.discriminatedUnion infers a correct narrowing union, which is exactly the shape of a WS-frame contract (A18, A19). This is the dominant production pattern (T3/tRPC-style packages/contracts) (A19).
This is technically feasible in BooCode — the "blocker" is narrower than it looks. The codebase already proves the built-package path: apps/coder consumes a compiled @boocode/server via workspace:*, and apps/server ships a package.json exports map with types+default conditions per subpath (including ./ws-frames) (A3). The TS6307 error cited in the code as "structurally blocking" cross-import (A5) applies to importing another app's raw source file — not a properly built node_modules package. And contrary to a claim in the codebase inventory, moduleResolution: "Bundler" (what apps/web uses) does honor exports maps and the types condition per the TypeScript primary docs (A11) — so a built @boocode/contracts is, in principle, consumable by all three apps.
But the codebase — the authoritative record of current intent — pushes back on doing it now. The most recent explicit decision (CHANGELOG, v2.5.12) records that a shared package was "considered and declined (drift is already prevented; not worth the Docker/build-order risk at solo scale)" (A2). The prior design note (DEFERRED-WORK.md §3) recommended the lightweight options (Zod-inferred, or a parity test) "unless planning a broader shared-types initiative," with full packages/types "justified when a third consumer appears or WS frame duplication becomes painful" (A1). Critically, that "third consumer" trigger is not cleanly met: each duplicated contract still has exactly two hand-synced consumers — apps/coder already consumes the server's frames as a built package, not as a third hand-copy [validation V1].
The real duplication surface is wider and messier than a clean five-type list. Codebase evidence found, beyond the six enumerated contracts: a live, uncaught drift — AgentSessionConfig is model?/modeId?/thinkingOptionId? (all optional) on the coder side but model: string; modeId: string | null; … (required/nullable) on the web side, and it is not in the parity test's coverage (A7) [validation V3, confirmed]; a fourth duplication site, apps/coder/web/src/api/types.ts, a fallback SPA with its own WsFrame/Message/ToolCall copies and no parity guard (A8) [V4, confirmed]; and the web's hand-written WsFrame union in types.ts is already partially superseded by the Zod-inferred type and is missing at least one frame type (session_renamed) (A10) [V6]. The web app has also never consumed a workspace package (apps/web/package.json has no @boocode/* dependency), so the build-tooling path is unproven for the hardest consumer (A9) [V2].
If unification is deferred, the lightweight guard-rails have documented prior art. A structural type-parity test (expectTypeOf().toEqualTypeOf() / Expect<Equal<>>, run under vitest --typecheck) catches add/remove/rename drift between two copies with no new toolchain (A20) — this is the same family as the project's existing parity tests, and the natural way to close the AgentSessionConfig-style gaps. Generated-copy approaches (OpenAPI/Orval/Zod codegen) exist but trade the duplication for a schema artifact and a codegen pipeline (A23).
One source conflict worth flagging: Zod v4's browser bundle size is reported inconsistently — ~5.36 KB gzipped (official) vs ~14 KB (independent), likely core-only vs realistic-usage measurement (A21, single-source on each figure). It does not bear on the recommendation (server/web already pin Zod v3.23.8), but a contracts package shipping Zod schemas to the browser would make this a real number to measure, and would add a third Zod-version-sync site against the latent v3→v4 pressure from the claude-agent-sdk (A21) [V8].
Options to Consider
O1: Shared compiled workspace package — @boocode/contracts (full unification)
- What it is: A new
packages/contractspackage built like@boocode/server(declaration:true,exportsmap withtypes+defaultper subpath). All three apps depend on it viaworkspace:*. Zod-backed contracts (ws-frames, provider-config) live there as the single schema (z.inferfor types); plain-type contracts move there as plain TS. Hand-synced copies and parity tests are deleted. - Trade-offs: Eliminates drift by construction — the strongest guarantee. Costs: a new workspace package + inverted build order (contracts must build before server/web/coder; today the root script builds web→server) and a Docker-build change; an unproven web-consumer path (composite +
noEmit+ Bundler reading a built.d.tsvia exports has no in-repo precedent); a third Zod-version-pin site; and it does not, by itself, address theapps/coder/webSPA copy or the webtypes.ts-vs-ws-frames.tsdualWsFramerepresentation. Explicitly declined at v2.5.12 for solo-scale cost. - Rests on: (A1, A2, A3, A4, A11, A18, A19) corroborated; web-consumer feasibility (A11) is primary-source but unexercised here.
- Evidence status: corroborated as an end-state; "proven for web" is refuted as currently unproven (V2).
O2: Schema-first SSOT for the Zod-backed contracts only (partial unification)
- What it is: A narrower O1 — share only the already-Zod contracts (the WS-frame schema, the provider-config schema) as single Zod definitions in a small shared package; leave the plain-type contracts under parity tests. Targets the highest-pain, lowest-friction subset (ws-frames is already byte-identical-maintained Zod in two files).
- Trade-offs: Captures most of the drift-elimination value (the WS frames are the most painful duplicate) while touching fewer types and deferring the plain-type migration. Still needs the shared-package build wiring and the web-consumer proof; still leaves plain-type contracts duplicated (but guarded).
- Rests on: (A3, A6, A11, A18) corroborated; (A19) pattern.
- Evidence status: corroborated; the same unproven-web-path caveat applies (V2, V5).
O3: Extend the status quo — parity tests + the new coding standard (close the gaps)
- What it is: Keep hand-synced copies; (1) fix the live
AgentSessionConfigdrift, (2) add structural type-parity tests for the currently-unguarded contracts (AgentSessionConfig,MessageMetadata,WorktreeRiskReport,ProviderOverride/CoderProvidersFile, the interfaceWsFrameunion), (3) decide the fate of theapps/coder/webSPA copy. Pairs with thecross-app-contract-paritycoding standard already written. - Trade-offs: Lowest cost and risk; matches the recorded v2.5.12 decision; closes real, demonstrated holes (including one already broken). Does not eliminate duplication — it makes drift loud rather than impossible, and the guard-rail is only as good as remembering to add new types to the
namesarrays (the exact gap that letAgentSessionConfigdrift). - Rests on: (A1, A2, A7, A20) corroborated; (A7) is a confirmed live defect.
- Evidence status: corroborated; matches the authoritative codebase decision.
O4: Codegen from a canonical schema (generated copies)
- What it is: A canonical schema (Zod source, or OpenAPI) generates the per-app type files; CI regenerates and
git diff --exit-codefails on staleness. - Trade-offs: Strong drift guarantee without a runtime shared import; but adds a schema artifact + codegen pipeline, and the guarantee degrades to "CI must run the generator." Heaviest setup for the least fit here — most BooCode contracts are not described by an OpenAPI spec, and the server is TypeScript (so a TS-source SSOT, i.e. O1/O2, dominates codegen).
- Rests on: (A23) corroborated as a pattern.
- Evidence status: corroborated, but weakly fit to this codebase.
Recommendation
-
Recommendation: No single big-bang winner survives scrutiny — take the staged path. (1) Now, regardless of the larger decision (O3): close the demonstrated gaps — fix the live
AgentSessionConfigmismatch and bring the unguarded contracts under parity coverage (or the newcross-app-contract-paritystandard), and explicitly decide whetherapps/coder/webis retired or guarded. This is low-risk, matches the recorded decision, and fixes something that is already broken. (2) The long-term unification you asked for (O2 → O1) is the right end-state but is a deliberate investment, not a proven drop-in. Gate it behind a tracer-bullet proof-of-concept: stand up a minimal@boocode/contracts, wire it intoapps/webfirst (the unproven consumer), migrate only the ws-frames Zod schema, and verify thattsc, Vite dev (HMR), andvite buildall resolve the built.d.ts/.jsthrough the exports map. If green → proceed to migrate the rest and delete the copies/tests (O1). If it hits the composite/noEmit/Vite-resolution wall → fall back to the extended O3. Preconditions for O1/O2: pin Zod incontractsto the workspace version, invert the build order (contracts first) and update the Docker build, resolve the webtypes.ts-vs-ws-frames.tsdualWsFramerepresentation, and handleapps/coder/web. -
Evidence basis: The end-state (shared package, schema-first for Zod contracts) rests on corroborated web evidence (TypeScript primary docs A11; multiple independent monorepo/Zod sources A12–A19) plus codebase precedent (
@boocode/serverconsumed byapps/coder, A3). The decision not to treat full unification as proven/justified-now rests on codebase evidence, which is authoritative on current state: the explicit v2.5.12 decline (A2), the unmet "third consumer" trigger (A1, V1), the absent web workspace-package precedent (A9, V2), and the liveAgentSessionConfigdrift (A7, V3 — confirmed by direct inspection). The immediate O3 action rests on the confirmed defect (A7) and the recorded decision (A1, A2). No part of the recommendation rests on reasoning alone. The one judgment that is genuinely yours: whether the WS-frame byte-sync pain is now "painful enough" to spend the O1/O2 investment despite the solo-scale cost the team previously weighed — the evidence frames that trade-off but cannot settle it for you.
Validation
V1: Is the "third consumer" deferral trigger met?
- Strategy: Challenge the Evidence
- Investigation: Read
DEFERRED-WORK.md§3 trigger and enumerated actual consumers;apps/coderconsumes@boocode/server(built package), not a third hand-copy. - Result: Refuted (trigger not met — still two hand-synced consumers per contract).
- Impact: Removed "the trigger is now met" from the rationale; the case for O1 now rests on WS-frame pain judged on its own merit, not a satisfied precondition.
V2: Is the built-package path proven for the web consumer?
- Strategy: Challenge the Evidence
- Investigation:
apps/web/package.jsonhas no@boocode/*dependency; no@boocodeimport anywhere inapps/web/src. Coder→server (NodeNext) is proven; web (Bundler + composite +noEmit) is not. - Result: Partially Refuted (NodeNext path proven; web path unproven).
- Impact: Reframed O1/O2 as gated behind a tracer-bullet probe; removed the "proven for web too" claim.
V3: Is the parity-test safety net complete?
- Strategy: Challenge the Evidence
- Investigation:
AgentSessionConfigdiffers between coder (all optional) and web (required/nullable) and is absent from the parity testnamesarray — directly verified. - Result: Confirmed (a live, uncaught drift exists today).
- Impact: Promoted "fix this now" into the recommendation's immediate action; weakens the "drift is already prevented" basis of the v2.5.12 decision.
V4: Is the duplication scope fully enumerated?
- Strategy: Challenge the Evidence
- Investigation:
apps/coder/web/src/api/types.ts(12 exported types, no parity test) is a fourth duplication site not in the inventory. - Result: Confirmed (uncounted site).
- Impact: Added "decide the fate of
apps/coder/web" as an explicit scope item / precondition.
V5: Does the web tsconfig flag combo break package consumption?
- Strategy: Challenge the Fix
- Investigation:
composite:true+noEmit:true+allowImportingTsExtensions:true+Bundler reading adist/*.d.tsvia exports is feasible but unexercised; Vite must resolve the JS at runtime, not just tsc the types. - Result: Partially Refuted (feasible, under-specified).
- Impact: Made the probe verify
tsc+ Vite dev +vite buildexplicitly; added build-order/Docker preconditions.
V6: Is the interface WsFrame union a simple plain-type move?
- Strategy: Challenge the Assumptions
- Investigation: Web
types.tsWsFrameis missingsession_renamedand is already partially superseded by the Zod-inferred type fromws-frames.ts. - Result: Confirmed (more complex than a move).
- Impact: Added "resolve the web dual-
WsFramerepresentation" as an O1 precondition.
V7: Is the prior decision stale, or authoritative?
- Strategy: Challenge the Evidence-Gathering Integrity
- Investigation: CHANGELOG v2.5.12 explicitly declined a shared package — more recent than
DEFERRED-WORK.mdand consistent with it (option C was chosen). - Result: Partially Refuted (the "pending decision awaiting triggers" framing was wrong; it was an explicit rejection).
- Impact: Recommendation now states O1 needs new justification over a recorded "declined," not just "triggers met."
V8: Any hidden version/coupling cost?
- Strategy: Challenge the Fix
- Investigation: Server/web pin Zod
^3.23.8; claude-agent-sdk exerts latent v4 pressure; a contracts package adds a third Zod-version-sync site. - Result: Confirmed (manageable, underspecified).
- Impact: Added "pin Zod in contracts to the workspace version" as a precondition.
Adjustments Made
The original recommendation ("build @boocode/contracts now; the path is proven") did not survive and was rewritten into the staged / no-single-winner form above: an evidence-backed immediate action (O3 gap-closing, including the confirmed AgentSessionConfig defect) plus full unification (O2→O1) reframed as a deliberate, probe-gated investment with explicit preconditions.
Confidence Assessment
- Confidence: Medium
- Remaining Risks: The web build-tooling path (composite +
noEmit+ Bundler consuming a built workspace package) is unproven in-repo — a failed probe is the main scope risk. The "is WS-frame pain worth the investment now" decision is a solo-scale judgment the evidence cannot settle. Web bundle-size of shipping Zod schemas to the browser is unmeasured (A21 conflict). Scope is wider than first enumerated (coder/web SPA, dualWsFrame, latent Zod v4) — discovery may not be complete (theAgentSessionConfigmiss is evidence the inventory can lag reality).
Sources
| ID | Source | Link / location | Retrieved | Trust class | Summary (one line) | Evidence status |
|---|---|---|---|---|---|---|
| A1 | DEFERRED-WORK §3 — prior options A/B/C | docs/DEFERRED-WORK.md:160-225 |
n/a | codebase | Weighed Zod-inferred / shared packages/types / parity test; "A or C unless broader initiative; full package when 3rd consumer or WS pain" |
recommendation-bearing |
| A2 | CHANGELOG v2.5.12 decline note | CHANGELOG.md (~:99) |
n/a | codebase | Shared package "considered and declined… not worth the Docker/build-order risk at solo scale" | recommendation-bearing |
| A3 | @boocode/server built-package precedent |
apps/server/package.json (exports), apps/coder/package.json (workspace:*), apps/coder/src/index.ts:19 |
n/a | codebase | Coder consumes a compiled server package via exports map + NodeNext; build server first | recommendation-bearing |
| A4 | Web build constraints | apps/web/tsconfig.app.json |
n/a | codebase | composite:true, moduleResolution:"Bundler", noEmit:true, allowImportingTsExtensions:true, include:["src"] |
recommendation-bearing |
| A5 | TS6307 cross-import block (raw source) | apps/coder/src/services/__tests__/provider-types-parity.test.ts:11-19 |
n/a | codebase | Web-side import of coder's source file blocked by TS6307 on the composite project | corroborated by A11 |
| A6 | WS-frame Zod byte-identical pair | apps/server/src/types/ws-frames.ts ↔ apps/web/src/api/ws-frames.ts (test …/ws-frames.test.ts) |
n/a | codebase | The most-painful duplicate; already Zod; byte-parity test | single source (codebase) |
| A7 | Live AgentSessionConfig drift |
apps/coder/src/services/provider-types.ts:56 vs apps/web/src/api/types.ts:310; absent from parity names |
n/a | codebase | Optional (coder) vs required/nullable (web), uncaught | recommendation-bearing |
| A8 | Uncounted 4th duplication site | apps/coder/web/src/api/types.ts |
n/a | codebase | Fallback SPA, 12 exported types incl. own WsFrame/Message, no parity test |
single source (codebase) |
| A9 | Web has no workspace-package dep | apps/web/package.json |
n/a | codebase | No @boocode/* dependency — built-package consumption unexercised for web |
recommendation-bearing |
| A10 | Web dual WsFrame representation |
apps/web/src/api/types.ts:560-622 vs ws-frames.ts |
n/a | codebase | Interface union partially superseded by the Zod type; missing session_renamed |
single source (codebase) |
| A11 | TypeScript Modules Reference | https://www.typescriptlang.org/docs/handbook/modules/reference.html | 2026-06-02 | web | bundler AND node16/nodenext honor exports+types condition; node10/classic don't |
recommendation-bearing (primary) |
| A12 | Live Types in a TS Monorepo (C. McDonnell) | https://colinhacks.com/essays/live-types-typescript-monorepo | 2026-06-02 | web | Five sharing strategies; custom export conditions for source-pointing dev | corroborated by A11, A14 |
| A13 | TypeScript Project References (docs) | https://www.typescriptlang.org/docs/handbook/project-references.html | 2026-06-02 | web | composite/declaration/declarationMap; tsc -b build ordering |
corroborated by A14 |
| A14 | Minimal TS monorepo lib (manzt gist) | https://gist.github.com/manzt/222c8e8f4ed35e74514eb756e4ba09bc | 2026-06-02 | web | publishConfig flip; source-pointing exports; nodenext authoring |
corroborated by A12, A15 |
| A15 | Sharing types React/NestJS pnpm (lico) | https://dev.to/lico/step-by-step-guide-sharing-types-and-values-between-react-esm-and-nestjs-cjs-in-a-pnpm-monorepo-2o2j | 2026-06-02 | web | Dual ESM/CJS exports map; verbatimModuleSyntax needs import type |
corroborated by A11 |
| A16 | TS6307 issue / tsup repro | https://github.com/microsoft/TypeScript/issues/27887 ; https://github.com/egoist/tsup/issues/1364 | 2026-06-02 | web | TS6307 on composite transitive re-exports; surfaces via bundler tools too | corroborated by A5 |
| A17 | Vite TS Monorepo RFC / vite-tsconfig-paths | https://github.com/vitejs/vite-ts-monorepo-rfc ; https://www.npmjs.com/package/vite-tsconfig-paths | 2026-06-02 | web | Vite source-consumption gap; paths-alias workaround for HMR | single-source on the RFC stats |
| A18 | Zod v4 — z.infer / z.discriminatedUnion |
https://zod.dev/v4 ; https://zod.dev/api | 2026-06-02 | web | One schema yields a narrowing discriminated-union type; validator+type can't drift | recommendation-bearing |
| A19 | Shared-Zod monorepo pattern (T3/tRPC) | https://calmops.com/programming/web/type-safe-fullstack-trpc-zod-monorepo/ ; https://www.ruthvikdev.com/blog/3-shared-zod-schemas | 2026-06-02 | web | packages/contracts Zod shared by server+client; version-pin caveat |
corroborated |
| A20 | Structural type-parity testing | https://vitest.dev/guide/testing-types ; https://www.totaltypescript.com/how-to-test-your-types | 2026-06-02 | web | expectTypeOf().toEqualTypeOf() / Expect<Equal<>> catches drift in CI, no new tooling |
recommendation-bearing |
| A21 | Zod v4 size/perf + alternatives | https://zod.dev/v4 ; https://pockit.tools/blog/zod-valibot-arktype-comparison-2026/ ; https://www.pkgpulse.com/blog/zod-vs-typebox-2026 | 2026-06-02 | web | Zod v4 ~5.36 KB (official) vs ~14 KB (independent); Valibot smaller, ArkType faster | single-source per figure; conflict flagged |
| A22 | Standard Schema | https://standardschema.dev/schema | 2026-06-02 | web | Common interface across Zod/Valibot/ArkType; reduces library lock-in | corroborated by A21 |
| A23 | OpenAPI/Zod codegen (generated copies) | https://openapi-ts.dev/ ; https://www.npmjs.com/package/ts-to-zod | 2026-06-02 | web | Generate type/validator copies from one source; guarantee = CI must regenerate | corroborated |
A1: DEFERRED-WORK §3 — prior options and recommendation — recommendation-bearing
- Link / location:
docs/DEFERRED-WORK.md:160-225 - Retrieved: n/a (codebase)
- Trust class: codebase (trusted current-state anchor)
- Summary: The project's own prior analysis of this exact question. Lays out option A (Zod + inferred types), B (shared
packages/types), C (status-quo parity test), with a trade-off table, and recommends "Start with A or C unless planning a broader shared-types initiative. Fullpackages/typesis justified when a third consumer appears or WS frame duplication becomes painful again." - Evidence status: authoritative on intent; corroborated by A2.
A2: CHANGELOG v2.5.12 decline note — recommendation-bearing
- Link / location:
CHANGELOG.md(~line 99, v2.5.12 provider-lifecycle entry) - Retrieved: n/a (codebase)
- Trust class: codebase
- Summary: The most recent explicit decision: "a shared package was considered and declined (drift is already prevented; not worth the Docker/build-order risk at solo scale)." Records that option C was chosen.
- Evidence status: authoritative; the "drift is already prevented" premise is weakened by A7.
A3: @boocode/server built-package precedent — recommendation-bearing
- Link / location:
apps/server/package.json(exports map withtypes+defaultper subpath, incl../ws-frames),apps/coder/package.json("@boocode/server": "workspace:*"),apps/coder/src/index.ts:19 - Retrieved: n/a (codebase)
- Trust class: codebase
- Summary: Demonstrates the exact built-package mechanism a
@boocode/contractswould use, working today for a NodeNext consumer (coder).apps/serverhasdeclaration:true; build order is server-first. - Evidence status: proves the NodeNext path; does not prove the Bundler/web path (A9, V2).
A4: Web build constraints — recommendation-bearing
- Link / location:
apps/web/tsconfig.app.json - Retrieved: n/a (codebase)
- Trust class: codebase
- Summary:
composite:true,moduleResolution:"Bundler",noEmit:true,allowImportingTsExtensions:true,include:["src"]. These permit consuming a builtnode_modulespackage (Bundler honors exports, A11) but the combination is unexercised against a workspace package here. - Evidence status: feasibility is primary-source (A11) but unexercised (V2, V5).
A7: Live AgentSessionConfig drift — recommendation-bearing
- Link / location:
apps/coder/src/services/provider-types.ts:56-61vsapps/web/src/api/types.ts:310-315; not inapps/coder/src/services/__tests__/provider-types-parity.test.tsnames - Retrieved: n/a (codebase)
- Trust class: codebase
- Summary: Coder:
model?/modeId?/thinkingOptionId?(optional). Web:model: string; modeId: string | null; thinkingOptionId: string | null(required/nullable). Structurally incompatible and uncaught by any parity test — a live defect. - Evidence status: confirmed by direct inspection (V3); the concrete immediate action.
A11: TypeScript Modules Reference — recommendation-bearing (primary)
- Link / location: https://www.typescriptlang.org/docs/handbook/modules/reference.html
- Retrieved: 2026-06-02
- Trust class: web (primary source — TypeScript team)
- Summary:
moduleResolution: bundlerandnode16/nodenextboth resolvepackage.jsonexportsand match thetypescondition;node10/classicignoreexports. This is why a built@boocode/contractsis consumable by the Vite (Bundler) web app — and corrects the codebase-inventory claim that "Bundler ignores exports." - Evidence status: primary source; corroborated by A12, A14.
A18: Zod v4 — z.infer / z.discriminatedUnion — recommendation-bearing
- Link / location: https://zod.dev/v4 ; https://zod.dev/api
- Retrieved: 2026-06-02
- Trust class: web
- Summary:
z.infer<typeof Schema>over az.discriminatedUnionyields a correct narrowing TypeScript discriminated union; the validator and the static type are one definition, so they cannot drift. This is the schema-first SSOT mechanism for the WS-frame and provider-config contracts (which are already Zod). - Evidence status: corroborated by A19; directly applicable since A6 shows ws-frames is already Zod.
A20: Structural type-parity testing — recommendation-bearing
- Link / location: https://vitest.dev/guide/testing-types ; https://www.totaltypescript.com/how-to-test-your-types
- Retrieved: 2026-06-02
- Trust class: web
- Summary:
expectTypeOf().toEqualTypeOf()/Expect<Equal<A,B>>undervitest --typecheckasserts structural identity (not just assignability) between two copies and fails CI on drift, with no new tooling beyond what the repo already runs. The natural mechanism for O3's gap-closing (e.g. guardingAgentSessionConfig). - Evidence status: corroborated (Vitest docs + Total TypeScript).