Files
boocode/openspec/changes/archived/contracts-ssot/proposal.md
indifferentketchup a734615480 docs: archive shipped openspec changes, refresh roadmap + DEFERRED-WORK
Move openspec/changes/{contracts-ssot,orchestrator} → archived/ (both shipped,
v2.7.13 and v2.7.17). Mark the roadmap's "Write/edit robustness" and "Claude
provider SDK" milestones as shipped (fuzzy-match.ts + checkpoints.ts; the
claude-sdk backend is live via CLAUDE_SDK_BACKEND in .env.host) and add a
v2.7.12–v2.7.17 shipped summary. Flag DEFERRED-WORK.md as superseded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:30:01 +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.