Files
boocode/openspec/changes/contracts-ssot/proposal.md
indifferentketchup 649ce71eff feat: single-source cross-app wire contracts in @boocode/contracts (v2.7.13)
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>
2026-06-02 21:24:08 +00:00

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-framesWsFrameSchema (Zod runtime), KNOWN_FRAME_TYPES, WsFrame (z.infer from the schema).
  • ./provider-snapshotProviderSnapshotEntry, ProviderModel, ProviderMode, ThinkingOption, AgentCommand, ProviderSnapshotStatus (plain TS; the coder's provider-types.ts re-exports them so internal importers are unchanged).
  • ./provider-configProviderOverrideSchema, CoderProvidersFileSchema, ProviderConfigPatchSchema and their z.infer types.
  • ./message-metadataMessageMetadata, ErrorReason, AgentSessionConfig.
  • ./worktree-riskWorktreeRiskReport (unified from three copies that differed only in name; the coder called it RiskReport).

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-frames exports 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, AgentSessionConfig copies
  • WorktreeRiskReport / RiskReport duplicates (3 copies)
  • apps/coder/web dead pending_change_added/pending_change_updated reducer arms and associated WS plumbing (DiffPane prop, Session.tsx listener)

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/UserFrame discriminated 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.json build script
  • Dockerfile — new COPY 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.