# Shared contracts package — one wire-type and vocabulary source for collector, controller, web

audience: AI coding agents first.

- Status: Draft
- Source: architecture-review-20260813-102418.html candidates C3 (remaining part), C4, C5, C12, C18
- Scope: packages/, collector/src/incidents/, collector/src/actions.ts, collector/src/server.ts, collector/src/token.ts, controller/src/store.ts, controller/src/token.ts, apps/web/src/lib/
- Delivery scope: ends after reviewed design + implementation plan documents are written. MUST NOT launch, MUST NOT implement (owner instruction 2026-08-13: "do not continue to execution yet")
- Last updated: 2026-08-13

## 1. Outcome

- ONE workspace package `@overdeck/incident-contract` (pattern: existing `@overdeck/activity-contract` in packages/activity-contract) owns the incident wire types, the dispatch state vocabulary, the dispatch error codes, the offload verbs, and the token-file helper.
- All three processes import it: collector (`file:../packages/incident-contract`), web (`workspace:*`), controller (`file:../packages/incident-contract`).
- Three hand-maintained mirrors collapse: apps/web/src/lib/incident-types.ts becomes re-exports or is deleted; the `IncidentsProvider` interface in collector/src/incidents/provider.ts is typed by the contract instead of `Parameters<IncidentService[...]>` gymnastics; controller's `TransitionVerb` extends the contract's offload-verb union.
- Two behaviors are byte-identical after the change: the wire shape and the token file handling.

## 2. Decisions

- D1: New package packages/incident-contract, name `@overdeck/incident-contract`, `"private": true`, `"type": "module"`, `"exports": { ".": "./src/index.ts" }`, `"scripts": { "typecheck": "tsc --noEmit" }`. Mirrors packages/activity-contract exactly (workspace globs already include `packages/*`).
- D2: Direction of dependence: contract imports NOTHING from collector/controller/web. web MUST NOT import collector; web imports the contract. collector imports the contract. controller imports the contract. No other process may import the contract package except these three.
- D3: The wire types move from the COLLECTOR producer shapes — they are canonical. The contract's `incidents.ts` module is sourced from collector/src/incidents/incident-service.ts (`IncidentPriority`, `IncidentState`, `IncidentScope`, `IncidentCoverage`, `IncidentDispatch` :34, `IncidentActivity`, `Incident`, `ListIncidentsQuery` :80, `ListIncidentsResult` :96, `FileIncidentRequest` :102, `FileIncidentResponse`, `IncidentTypeOption`, `IncidentCliOption`, `IncidentOptionsResponse`). The web mirror (incident-types.ts) is the one corrected, not the source: it must gain `FileIncidentRequest.wrapperModel?: string` (incident-service.ts:114) and adopt the canonical names (`ListIncidentsQuery`, `ListIncidentsResult`). Collector producer names, members, and values MUST NOT change in this delivery; the wire bytes stay identical.
- D4: Dispatch state vocabulary single-sourced in the contract: `IncidentTerminalState = "resolved" | "needs-attention" | "rate-limited" | "timed-out" | "engine-down" | "interrupted" | "invalid-result"` and `IncidentStatusState = "running" | IncidentTerminalState` move verbatim from collector/src/incidents/status-writer.ts:6-14 into the contract as `vocabulary.ts`. The contract adds `DispatchMetadataState = IncidentStatusState | "starting"`; the service's `DISPATCH_STATE_BY_METADATA` (collector/src/incidents/incident-service.ts:211) becomes `Record<DispatchMetadataState, IncidentState>`. "starting" is written by the launcher (incident-service.ts:722/:725/:1008) and stays a member; never add an untyped key.
- D5: Dispatch error codes single-sourced: the contract exports `DISPATCH_ERROR_CODES` (the incident-route literals from collector/src/server.ts: `"idempotency-conflict"`, `"resolution-conflict"`, `"brief-assembly-failed"`, `"incidents-store-unavailable"`, `"invalid-incident-dispatch"`) and the derived `DispatchErrorCode` type. server.ts MUST respond using these constants. The shared auth code `"unauthorized"` and `"harness-unreachable"` MAY move into a parallel `AUTH_ERROR_CODES` const in the same contract; anything else stays local.
- D6: Offload verbs: the contract exports `OFFLOAD_ACTION_VERBS` (the eight literals from collector/src/actions.ts:53-62: `box-drain`, `box-restore`, `host-quarantine`, `host-unquarantine`, `admission-reconcile`, `job-retry`, `ci-reconcile`, `recall-spill`) and `OffloadActionVerb`. collector imports the list verbatim; controller's `TransitionVerb` (controller/src/store.ts:75-84) becomes `OffloadActionVerb | "delivery-feature-reconcile"` — the controller keeps its ninth verb as an explicit extension, never silently diverging.
- D7: Token helper: the contract exports `loadOrCreateToken(tokenPath: string): string` — the identical logic of collector/src/token.ts and controller/src/token.ts (randomBytes(32).hex, dir mode 0o700, file mode 0o600, throw on empty with message `token file ${path} exists but is empty` and a distinguishing error class). Both processes call it with their own `tokenFile()` path and keep their own fatal-error wrapper (CollectorFatalError / ControllerFatalError) around it.
- D8: The contract MUST NOT depend on zod or any framework; it is type-only plus the small token function. zod stays in collector.
- D9: `IncidentsProvider` (provider.ts:34-50) is retyped: every method signature is declared with contract types (e.g. `listIncidents(query: ListIncidentsQuery): Promise<ListIncidentsResult>`); `createIncidentService` return shapes must satisfy them structurally — no new adapter code is authored. The one-method slice `IncidentsProviderLike` (collector/src/actions.ts:99-101) is deliberate locality, NOT a mirror; keep it.
- D10: Doc structure (owner decision 2026-08-13): this delivery is one of three specs — shared-contracts, module-deepenings (2026-08-13-arch-module-deepenings-design.md), domain-glossary (2026-08-13-arch-domain-glossary-design.md). Grouping chosen over umbrella and over per-cluster pairs. This spec lands first: controller/web consumption clusters depend on it.

## 3. Architecture

### 3.1 The package — packages/incident-contract

Files:

- package.json, tsconfig.json — clone of packages/activity-contract.
- src/index.ts — barrel re-exporting the modules below (single entry, like activity-contract).
- src/incidents.ts — D3 wire types, verbatim.
- src/vocabulary.ts — D4 state unions.
- src/errors.ts — D5 error codes.
- src/verbs.ts — D6 offload verbs.
- src/token.ts — D7 token helper.

### 3.2 Consumers

- collector: package.json dependency; src/incidents/provider.ts imports wire types + vocabulary; src/incidents/status-writer.ts re-exports or imports the unions from the contract (its own types MUST NOT be re-declared); src/incidents/incident-service.ts imports the metadata map types; src/actions.ts imports OFFLOAD_ACTION_VERBS; src/server.ts imports DISPATCH_ERROR_CODES; src/token.ts deletes its logic and re-exports the contract helper; src/incidents/incident-types.ts does not exist — do not create one.
- web: package.json `workspace:*` dependency; src/lib/incident-types.ts becomes `export * from "@overdeck/incident-contract"` (deletion test: file must be re-exports only, or deleted and imports updated); src/lib/collector-client.ts, collector-queries.ts, page-mappers.ts, overview-mappers.ts, panel-data.ts, components/incidents/* import from the contract. collector-types.ts is a SEPARATE protocol mirror (collector/src/schema.ts + state.ts, per its header comment) — NOT absorbed by this contract; out of scope (see §7).
- controller: package.json dependency; src/store.ts `TransitionVerb` becomes the extension union; src/token.ts swaps to the contract helper; controller/src/verbs.ts does not exist — do not create a second copy.

### 3.3 Seams

- Seam 1: `@overdeck/incident-contract` ↔ collector — the incident wire contract (read routes + dispatch POST).
- Seam 2: `@overdeck/incident-contract` ↔ web — same contract, consumer side.
- Seam 3: `@overdeck/incident-contract` ↔ controller — offload verbs + token helper.
- Deleted seam: web's hand-maintained mirror files. Deleted seam: collector's Parameters<> interface derivation.

## 4. Behavior

### 4.1 Wire shapes — same names, same members, same values

- The wire shape is the contract; nothing about what web renders or collector serves changes. The deletion test is the proof (D3).

### 4.2 Vocabulary — one union, typed keys

- `DISPATCH_STATE_BY_METADATA` keys MUST be compile-time-checked against `DispatchMetadataState`; a future runner state MUST be added to the union in the contract first, in one place, and is then visible to the service map and the web renderer.

### 4.3 Verbs — one list, one extension

- The eight canonical verbs are consumed, not copied. `delivery-feature-reconcile` remains controller-only and is declared as an extension (D6).

### 4.4 Token — one implementation, two call sites

- Same bytes on disk, same permissions, same error message. The two fatal-error wrappers differ by process and stay local (D7).

### 4.5 Error codes — constants, not literals

- server.ts responds with contract constants; web consumers (if any read the body) MUST NOT string-compare against literals they define locally.

## 5. Error handling

- TokenFileError from the contract propagates as today's TOKEN_EMPTY through each process's fatal wrapper; message text unchanged (D7).
- Error codes: values identical to today's literals; no consumer of the body changes.
- No new error paths are introduced; the contract exports types and one pure function only.

## 6. Testing

- Package: `pnpm --filter @overdeck/incident-contract typecheck`.
- collector: `bun test` in collector/ and `tsc --noEmit` — provider.ts, actions.ts, token.ts suites stay green (collector/src/actions.test.ts exists; server route behavior is covered by collector/src/incidents/routes.test.ts).
- controller: `bun test` in controller/ and `tsc --noEmit` — store.test.ts, transitions.test.ts stay green.
- web: `pnpm --filter web build` and `pnpm --filter web typecheck` — collector-client.test.ts, collector-queries.test.ts, overview-mappers.test.ts, panel-freshness.test.ts stay green.
- Deletion tests: (1) apps/web/src/lib/incident-types.ts contains no declarations, only re-exports; (2) `grep -rn "OFFLOAD_ACTION_VERBS" collector/src/actions.ts` shows an import, no declaration; (3) `grep -rn "Parameters<IncidentService" collector/src/ | wc -l` = 0; (4) `grep -n "IncidentTerminalState\|IncidentStatusState" collector/src/incidents/status-writer.ts` shows imports only, no `export type`; (5) `grep -c "DISPATCH_ERROR_CODES" collector/src/server.ts` ≥ 1; (6) `grep -n "randomBytes" collector/src/token.ts controller/src/token.ts` = 0.
- No factory/harness changes: the mandatory harness suite (modules/harness/factory/tests/) is untouched by this spec — run it only if the plan's implementer touches modules/harness/** (they MUST NOT).

## 7. Out of scope

- Kanboard client, mutation store, status projection internals (already deepened at origin/main; context only).
- The five already-landed candidates (C1 runner collapse, C2 lock lifetime, C6 launcher trim, C14 kanboard typed door, C20 bootstrap probe) — document, never re-specify.
- apps/web/src/lib/collector-types.ts — a second, separate protocol mirror (collector/src/schema.ts + state.ts, the activity/state protocol, not incidents); a future contract may absorb it; this delivery MUST NOT.
- Any wire-shape or behavior change; any UI work (components touched only by import path updates — od-ui-dev skill still governs edits under apps/web/src/**).
- Fleet, gptbridge, systray, controller store split, actions gateway table, web data path — these are spec 2026-08-13-arch-module-deepenings-design.md.

## 8. Architecture Decisions

- ADR-0001 (created by 2026-08-13-arch-domain-glossary-design.md): shared contracts package — D1, D2, D8 of this spec.
- ADR-0002: single dispatch state vocabulary — D4; the "starting" mystery is resolved by making it an explicit union member or removing it.
- ADR-0003: offload verbs single-sourced with a declared extension point — D6.
- Not an ADR: the interface retyping (D9) is a mechanical consequence of D3, not a decision.
