Files
boocode/docs/research/cross-app-contract-ssot.md
indifferentketchup 2a05d2f9fe docs: archive shipped openspec batches; add feature/plan/research notes
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>
2026-06-02 21:20:33 +00:00

28 KiB
Raw Permalink Blame History

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/serverapps/webapps/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 filenot 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 driftAgentSessionConfig 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 A12A19) 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.tsapps/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).