# Console v2 — Architecture

This document is the high-level pointer doc. It exists to close the Law 8 ("Theory") doc-debt the audit flagged on the Console v2 surface. ADRs in `docs/adr/` are the authoritative sources for specific decisions; this file points at them.

## 1. System diagram

```
┌──────────────────────────────────────────────────────────────────────────────────┐
│  Mac Studio (always-on, 100.105.59.118) — LaunchAgent runs on boot               │
│                                                                                  │
│   launcher.sh → uvicorn recoil.api.main:app --port 8431                          │
│                                                                                  │
│   ┌────────────────────────────────────────────────────────────────────┐        │
│   │  recoil/api/                                                        │        │
│   │  ├─ workspace_state.py       (SQLite WAL @ ~/.recoil/v2_state.db)   │        │
│   │  ├─ engine_routes.py         (project / episode / scene / beat /    │        │
│   │  │                            take read routes)                     │        │
│   │  ├─ adapters/projects.py     (project + synthesized episodes)       │        │
│   │  ├─ adapters/beats.py        (beats + synthesized scenes)           │        │
│   │  ├─ adapters/events.py       (JSONL receipts → SSE)                 │        │
│   │  ├─ adapters/memory.py       (engine-memory reader)                 │        │
│   │  ├─ mutation_routes.py       (take + proposal mutations)            │        │
│   │  ├─ proposal_dispatch.py     (typed severity_map + ProposalKind     │        │
│   │  │                            registry; exec=None for all kinds)    │        │
│   │  ├─ system_status.py         (GET /api/system-status — chrome truth)│        │
│   │  ├─ sanctioned_fallbacks.py  (REGISTRY + emit_fallback + counters)  │        │
│   │  ├─ chat.py                  (SSE stub frames; real LLM deferred)   │        │
│   │  └─ slash_dispatch.py        (BUS-event whitelist; exec deferred)   │        │
│   │                                                                     │        │
│   │  STATE: SQLite WAL @ ~/.recoil/v2_workspace_state.db                │        │
│   │  STATE: in-process EventBus ring buffer (BUS)                       │        │
│   │  STATE: in-process FALLBACK_COUNTERS (Law 1, capped, named)         │        │
│   └──────┬──────────────────────────────────────────────────────────────┘        │
│          │                                                                       │
└──────────┼───────────────────────────────────────────────────────────────────────┘
           │ HTTP / SSE over Tailscale
           │
   ┌───────┴──────────────────────────┐         ┌─────────────────────────┐
   │  Desktop (@recoil/desktop)        │         │  Mobile (@recoil/mobile)│
   │  React + Vite + pnpm              │         │  React + Vite           │
   │                                   │         │                         │
   │  App.tsx                          │         │  App.tsx                │
   │   ├─ useSystemStatus  → chrome    │         │   (light-touch surface; │
   │   │   props (truth)               │         │    inherits contracts   │
   │   ├─ useEventStream → SSE state   │         │    from desktop)        │
   │   ├─ useBreadcrumb (focused)      │         │                         │
   │   ├─ <SessionRestoreModal/> (Q5)  │         │                         │
   │   ├─ Titlebar + StatusBar +       │         │                         │
   │   │   StatusPopover + BottomBay   │         │                         │
   │   │   (all chrome reads from      │         │                         │
   │   │    useSystemStatus)           │         │                         │
   │   ├─ HierarchyNavigator           │         │                         │
   │   ├─ ArtifactStage (breadcrumb)   │         │                         │
   │   ├─ TakeInspector (caller-aware) │         │                         │
   │   ├─ ProposalCard (lifecycle)     │         │                         │
   │   ├─ EventsDrawer                 │         │                         │
   │   └─ CommandPalette ⌘K            │         │                         │
   │                                   │         │                         │
   │  Adapters (one of two):           │         │                         │
   │   - @recoil/fixtures (dev)        │         │                         │
   │   - @recoil/http-adapter (prod)   │         │                         │
   │  IDENTICAL function signatures.   │         │                         │
   └───────────────────────────────────┘         └─────────────────────────┘

   ┌────────────────────────────────────────────────────────────────┐
   │  scripts/verify_laws/                                          │
   │   ├─ check_fallback_registration.mjs            (TS gate)      │
   │   ├─ check_fallback_registration_python.py      (Python gate)  │
   │   ├─ check_no_cross_package_imports.mjs                        │
   │   └─ … 5 other checks                                          │
   └────────────────────────────────────────────────────────────────┘
```

ADR-0007 (LaunchAgent always-on) covers the Studio process lifecycle. ADR-0006 (EventBus contract Day 1, SSE Phase 19) covers the SSE wire shape. ADR-0009 (codegen Pydantic to TypeScript) covers the contract-flow from Pydantic models to zod schemas. ADR-0013 (chrome as status truth) covers the system-status endpoint and its consumers.

## 2. State-home table

| State | Canonical home | Cache(s) | Notes |
|---|---|---|---|
| Engine entities (Project / Episode / Scene / Beat / Take / Lineage) | Disk JSON files under `projects/<slug>/state/` | None on the API side — every request walks disk; LRU cache deferred to CP-N+ | Episode + Scene are SYNTHESIZED today (ADR-0011). The underlying disk shape is unchanged. |
| Workspace UI state (column widths, parked tabs, viewport positions, tweaks) | `~/.recoil/v2_workspace_state.db` (SQLite WAL, OUTSIDE Dropbox per ADR-0004) | None | Q5 modal lives client-side; on parse failure user picks discard-or-report; chosen action POSTs to existing workspace-state route. |
| Sanctioned fallback counters | `recoil.api.sanctioned_fallbacks._COUNTERS` (in-process dict, named per Law 1, never persisted) | Itself is the cache; reset on uvicorn restart by design | ADR-0012 declares the canonical home; `/api/system-status` reads it for the chrome status pill. |
| Selection (focused project / beat / take) | React `App.tsx` state (ephemeral) | None | Used by `useBreadcrumb` for ChromeTop + ArtifactStage. |
| SSE connection state | `useEventStream` hook return value | None | Derived from `EventSource.readyState`; chrome status pill derives ConnectionState from this + system-status HTTP. |
| Slash-command whitelist | `recoil.api.slash_dispatch._WHITELIST` (in-process tuple, immutable post-import) | Itself | BUS-only today; real subprocess execution deferred (`.out-of-scope/jt-priority-slash-commands.md`). |
| Proposal lifecycle | `recoil.api.proposal_dispatch._PROPOSALS` (in-process dict) | Itself; not persisted | All 8 ProposalKinds have `exec: None` today (`.out-of-scope/proposal-execution-glue.md`). |
| Persisted contracts version | `SCHEMA_VERSION = 1 as const` in `packages/contracts/src/manual.ts` | Compile-time literal | ADR-0010. Any persisted shape mismatch raises `UnknownSchemaVersionError`. |

## 3. Blast-radius table

| Module | Deletion impact |
|---|---|
| `recoil/api/sanctioned_fallbacks.py` | Every `emit_fallback()` call site loses its target; ~7 silent recovery paths revert to invisibility. The Python registry IS the substrate; deletion is fatal to Laws 4 + Tenet 6. ADR-0012. **Earning its keep — DEEP.** |
| `recoil/api/system_status.py` | Chrome reverts to fixture-frozen literals; the disconnect-but-green-pill failure mode reappears. ADR-0013. **Earning its keep.** |
| `<SessionRestoreModal>` | `workspace_state_parse_failure_default` still fires telemetry but silently substitutes defaults; bytes the user sees change without consent. The modal IS the Law-4 prong-3 enforcement. **Earning its keep.** |
| `scripts/verify_laws/check_fallback_registration_python.py` | The Python registry becomes voluntary — drift is caught only at exec time, never at PR time. Parallel to the existing TS gate. **Earning its keep.** |
| Synthetic episode + scene synthesis (`adapters/projects.py`, `adapters/beats.py`) | HierarchyNavigator goes back to empty trees; PHASE_18_SMOKE Item 2 unblocks-and-re-blocks. Deletion forces the engine-side `scene_id` backfill (which `.out-of-scope/hierarchy-enumeration-engine-side.md` says is correct long-term). ADR-0011. **Earning its keep until the engine-side fix lands.** |
| `EventBus` (singleton, ring buffer) | Every SSE consumer (events drawer, status pill, mobile activity tab) goes silent. ADR-0006. **Earning its keep — DEEP.** |
| `packages/contracts/` (zod schemas) | Every consumer's compile-time validation evaporates; runtime parse failures reappear at every wire boundary. ADR-0005, ADR-0009, ADR-0010. **Earning its keep — DEEP.** |
| `@recoil/fixtures` adapter | Dev mode loses its real-shape stub; story tests + UI dev loop break. ADR-0005. **Earning its keep.** |
| `@recoil/http-adapter` | Prod mode loses its HTTP transport; identical signature to fixtures means the swap is one config flag. **Earning its keep.** |
| `LaunchAgent (com.recoil.console-v2.plist)` | Studio API stops auto-starting on boot; manual start required. ADR-0007. **Earning its keep.** |
| `useSystemStatus` hook | Chrome props go null; every status surface renders empty or default. **Earning its keep — paired with system_status.py.** |
| `useEventStream` hook | All SSE event consumption stops; events drawer goes empty. **Earning its keep — DEEP.** |
| `proposal_dispatch.py` | Proposal lifecycle (originate/review/approve/reject) breaks; ProposalCard goes inert. Today's `_KIND_INFO` table holds the typed registry; deletion would force re-deriving the 8 ProposalKinds elsewhere. **Earning its keep.** |
| `chat.py` (stub today) | Chat surface goes inert; command palette can't dispatch through it. Stub IS the wire shape; the eventual real LLM integration swaps the body, not the contract. **Earning its keep at substrate level.** |
| `slash_dispatch.py` | ⌘K command palette can't fire any slash command; events drawer never sees slash-dispatch frames. **Earning its keep at substrate level.** |
