Move all hand-synced cross-app wire contracts into one built workspace package, @boocode/contracts, consumed by server/web/coder/coder-web via workspace:* + a per-subpath exports map. The ws-frames and provider-config Zod schemas are schema-first (z.infer); MessageMetadata, ErrorReason, AgentSessionConfig, the provider snapshot types, and WorktreeRiskReport are each single-sourced. Deletes the byte-identical copies and their parity tests, fixes a live AgentSessionConfig drift (coder dead copy removed, unified to the web required/nullable shape), removes the dead pending_change WS arms in the fallback SPA, and inverts the build order (contracts builds first) across root build, Dockerfile, and the coder deploy docs. Reverses the shared-package decision declined in v2.5.12. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.1 KiB
@boocode/contracts — cross-app wire contract SSOT
Status: shipped (2026-06-02)
Eliminate BooCode's hand-synced, duplicated cross-app TypeScript wire contracts by
creating one workspace package, @boocode/contracts, that every consumer imports.
Each contract is defined exactly once; the two Zod-backed contracts (ws-frames,
provider-config) use z.infer so validator and type derive from the same definition
and cannot drift independently.
Why
BooCode maintained hand-synced copies of cross-app contracts across up to four
locations (apps/server, apps/web, apps/coder, apps/coder/web), verified only
by byte-parity tests — provider-types-parity.test.ts and the ws-frames byte-parity
assertion in ws-frames.test.ts. A live drift had appeared: AgentSessionConfig
existed in two incompatible shapes — apps/coder held an all-optional dead copy
(zero live importers) while apps/web held the real required/nullable shape — making
the parity-test regime insufficient for type-only contracts.
v2.5.12-provider-lifecycle-phase4 had explicitly deferred a shared types package as
"not worth the Docker/build-order risk at solo scale"; the observed drift made the
investment worth taking.
What shipped
Package: packages/contracts (@boocode/contracts), declaration:true, zod
pinned ^3.23.8, per-subpath exports map with types-then-default conditions.
packages/* added to pnpm-workspace.yaml; pnpm-lock.yaml regenerated.
Six contracts single-sourced:
./ws-frames—WsFrameSchema(Zod runtime),KNOWN_FRAME_TYPES,WsFrame(z.inferfrom the schema)../provider-snapshot—ProviderSnapshotEntry,ProviderModel,ProviderMode,ThinkingOption,AgentCommand,ProviderSnapshotStatus(plain TS; the coder'sprovider-types.tsre-exports them so internal importers are unchanged)../provider-config—ProviderOverrideSchema,CoderProvidersFileSchema,ProviderConfigPatchSchemaand theirz.infertypes../message-metadata—MessageMetadata,ErrorReason,AgentSessionConfig../worktree-risk—WorktreeRiskReport(unified from three copies that differed only in name; the coder called itRiskReport).
Four consumers import via workspace:* through the exports map: apps/server,
apps/web, apps/coder, and the fallback SPA apps/coder/web. No tsconfig project
references; built dist only.
Deleted:
apps/server/src/types/ws-frames.ts(server./ws-framesexports subpath dropped)apps/web/src/api/ws-frames.ts- Provider-snapshot mirror block in web (
apps/web/src/api/types.ts) - Provider-config 17-line hand-mirror in web
apps/coder/src/services/__tests__/provider-types-parity.test.ts(6 parity tests)- ws-frames byte-parity assertion (server test suite)
- All duplicate
MessageMetadata,ErrorReason,AgentSessionConfigcopies WorktreeRiskReport/RiskReportduplicates (3 copies)apps/coder/webdeadpending_change_added/pending_change_updatedreducer arms and associated WS plumbing (DiffPaneprop,Session.tsxlistener)
Preserved:
- KNOWN_FRAME_TYPES drift test — moved into the package (11/11)
- Broker fail-closed tests — kept in
apps/server, importing from the package (4/4) - Web strict
SessionFrame/UserFramediscriminated union — web-local, untouched
Key decisions
F1 (ws-frames): repoint all 8 server/coder importers + 2 web validators to the
package; drop the server ./ws-frames re-export subpath — one path per contract, no
shim.
F2 (web strict union): the package exports the runtime schema only (WsFrameSchema,
KNOWN_FRAME_TYPES, loose WsFrame). The web's rich discriminated union stays
web-local — it references entity types (Message, ToolCall, etc.) that are
intentionally web-local and not cross-app duplicated. Zero entity-type scope expansion.
AgentSessionConfig drift: unified to the web required/nullable shape; the coder's all-optional copy confirmed dead (zero live importers) and deleted.
apps/coder/web (fallback SPA): its hand-copied 9-arm WsFrame union replaced by
the canonical import; dead pending_change_* arms removed (no publisher exists for
these frames anywhere in the codebase — they were HTTP-delivered, not WS); field
conflicts reconciled per-field (tool_result.error boolean→string, tokens_used
number→number|null, snapshot.messages cast).
Two ErrorReason concepts (intentional, not duplication):
message-metadata's ErrorReason is the DB-persisted 3-value set; the ws-frames
frame-level reason is the wire 5-value set. Different value sets, different
semantics, confirmed during audit.
Build-order inversion
Contracts builds before all consumers in:
- Root
package.jsonbuild script Dockerfile— newCOPY packages/*block + contracts build step before web/server- Coder deploy command — updated in all 7 doc sites (CLAUDE.md, apps/coder/CLAUDE.md, BOOCODER.md, docs/ARCHITECTURE.md, docs/project-discovery.md, README.md, docs/coder-backends.md)
Test counts at ship
Server 543 / coder 293 / contracts 11. Clean docker compose build --no-cache boocode
green. Human smoke verified 2026-06-02.