# 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/contracts` package built like `@boocode/server` (`declaration:true`, `exports` map with `types`+`default` per subpath). All three apps depend on it via `workspace:*`. Zod-backed contracts (ws-frames, provider-config) live there as the single schema (`z.infer` for 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.ts` via exports has no in-repo precedent); a third Zod-version-pin site; and it does not, by itself, address the `apps/coder/web` SPA copy or the web `types.ts`-vs-`ws-frames.ts` dual `WsFrame` representation. 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 `AgentSessionConfig` drift, (2) add structural type-parity tests for the currently-unguarded contracts (`AgentSessionConfig`, `MessageMetadata`, `WorktreeRiskReport`, `ProviderOverride`/`CoderProvidersFile`, the interface `WsFrame` union), (3) decide the fate of the `apps/coder/web` SPA copy. Pairs with the `cross-app-contract-parity` coding 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 `names` arrays (the exact gap that let `AgentSessionConfig` drift).
- **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-code` fails 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 `AgentSessionConfig` mismatch and bring the unguarded contracts under parity coverage (or the new `cross-app-contract-parity` standard), and explicitly decide whether `apps/coder/web` is 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 into **`apps/web` first** (the unproven consumer), migrate **only the ws-frames Zod schema**, and verify that `tsc`, Vite dev (HMR), and `vite build` all resolve the built `.d.ts`/`.js` through 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 in `contracts` to the workspace version, invert the build order (contracts first) and update the Docker build, resolve the web `types.ts`-vs-`ws-frames.ts` dual `WsFrame` representation, and handle `apps/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/server` consumed by `apps/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 live `AgentSessionConfig` drift (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/coder` consumes `@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.json` has no `@boocode/*` dependency; no `@boocode` import anywhere in `apps/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:** `AgentSessionConfig` differs between coder (all optional) and web (required/nullable) and is absent from the parity test `names` array — 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 a `dist/*.d.ts` via 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 build` explicitly; added build-order/Docker preconditions.

### V6: Is the interface `WsFrame` union a simple plain-type move?
- **Strategy:** Challenge the Assumptions
- **Investigation:** Web `types.ts` `WsFrame` is missing `session_renamed` and is already partially superseded by the Zod-inferred type from `ws-frames.ts`.
- **Result:** Confirmed (more complex than a move).
- **Impact:** Added "resolve the web dual-`WsFrame` representation" 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.md` and 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, dual `WsFrame`, latent Zod v4) — discovery may not be complete (the `AgentSessionConfig` miss 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. Full `packages/types` is 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 with `types`+`default` per 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/contracts` would use, working today for a NodeNext consumer (coder). `apps/server` has `declaration: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 built `node_modules` package (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-61` vs `apps/web/src/api/types.ts:310-315`; not in `apps/coder/src/services/__tests__/provider-types-parity.test.ts` `names`
- **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: bundler` and `node16`/`nodenext` both resolve `package.json` `exports` and match the `types` condition; `node10`/`classic` ignore `exports`. This is why a built `@boocode/contracts` is 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 a `z.discriminatedUnion` yields 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>>` under `vitest --typecheck` asserts 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. guarding `AgentSessionConfig`).
- **Evidence status:** corroborated (Vitest docs + Total TypeScript).
