# Mahana Devices — Dependency Map
*What is coupled to what, with file:line citations. 2026-04-22.*

---

## Mermaid — the real topology

```mermaid
flowchart TB
  subgraph ELECTRON["Electron UI (optional — ~3,500L of real coupling)"]
    notch["notch/notch-window.ts:762L"]
    island["island/island-window.ts:659L"]
    editor["editor-window/editor-window.ts:462L"]
    videoRoom["video-room/video-room-window.ts:185L"]
    mainWin["window.ts<br/>(TCC mic, Touch ID)"]
    sovGate["services/sovereign-gate.ts<br/>(systemPreferences)"]
    ipcLayer["ipc/*.ts<br/>(~20 handlers)"]
  end

  subgraph BRIDGE["bridge/ — the decoupling layer"]
    elBridge["ElectronBridgeImpl<br/>electron-bridge.ts:73-140"]
    daemonBridge["DaemonBridgeImpl<br/>electron-bridge.ts:144-250+"]
    tryWin["tryGetBrowserWindow()"]
  end

  subgraph DAEMON["Daemon routes + services (runs in both modes)"]
    voiceR["automation/routes/voice*.ts<br/>17,002L"]
    videoR["automation/routes/video*.ts<br/>1,436L"]
    browserR["automation/routes/browser*.ts<br/>8,164L"]
    terminalR["automation/routes/terminal*.ts<br/>4,305L"]
    voiceS["services/voice*.ts + voice/<br/>~7,000L"]
    videoS["services/video/<br/>mediasoup-service.ts"]
    browserS["services/browser/<br/>electron-backend.ts + puppeteer-backend.ts"]
    ptyD["pty-daemon/*.ts<br/>1,237L<br/>(HTTP+WS :9879)"]
  end

  subgraph STANDALONE["Already-standalone repos (zero Electron)"]
    substrate["mahana-substrate<br/>4,983L Node"]
    brain["mahana-brain<br/>Node TS"]
    sdk["mahana-agent-sdk<br/>@mahana/agent-sdk"]
    unified["mahana-unified<br/>Next+Capacitor iOS"]
    capture["mahana-capture<br/>Rust sidecar"]
    mcpMini["mahana-mini-mcp<br/>CF Worker"]
    translator["packages/translator-proxy<br/>CF Worker"]
    canvas["mahana-canvas<br/>Express :9880"]
    dream["mahana-dream-studio<br/>apps/ + services/"]
    elevenlabs["elevenlabs-voice-app<br/>Next.js"]
    terminalSwift["mahana-terminal<br/>Swift + SwiftTerm"]
    film["mahana-film<br/>Vite"]
    videoEd["mahana-video-editor-v1<br/>Next"]
  end

  subgraph EXT["External services"]
    supabase[("Supabase gyz")]
    cfWorkers["CF Workers<br/>api.mahana-mini.cc"]
    cfRealtime["CF Realtime<br/>APP_ID + DO"]
    vercel["Vercel<br/>(web apps)"]
    providers["AI providers<br/>DeepInfra/Fireworks/<br/>Together/Nebius/xAI"]
  end

  %% Electron-routes coupling (graceful)
  voiceR -. "tryGetBrowserWindow (43 refs, degrade to 503)" .-> tryWin
  videoR -. "createVideoRoomWindow (1 call site)" .-> videoRoom
  browserR -. "tryGetBrowserWindow (36 refs, or fall to puppeteer)" .-> tryWin
  terminalR -. "0 direct imports — via ipc/terminal.ipc" .-> ipcLayer

  tryWin --> elBridge
  tryWin --> daemonBridge

  %% Service coupling
  browserR --> browserS
  videoR --> videoS
  voiceR --> voiceS

  %% Daemon → external (no Electron in the path)
  voiceR --> supabase
  videoR --> supabase
  browserR --> supabase
  terminalR --> supabase
  voiceR --> providers
  voiceR --> cfRealtime
  videoR --> cfRealtime

  %% Substrate is peer, not parent
  substrate <--> supabase
  substrate <--> cfWorkers

  %% iOS + CF + Capture register with substrate
  unified -. "auth via Supabase+CF" .-> supabase
  capture -. "POST /substrate/register :9877" .-> substrate
  mcpMini --> cfWorkers
  translator --> cfWorkers
  canvas -. "WebSocket :9880 (hot reload)" .-> dream
  elevenlabs --> vercel

  %% Terminal native
  terminalSwift -. "HTTP 127.0.0.1:9878 DaemonBridge" .-> DAEMON
```

---

## Narrative

### 1. Systems that accidentally share the Electron-main event bus

**None, as of 2026-04-22.** The `bridge/electron-bridge.ts` abstraction was shipped before this audit. It exposes:

- `ElectronBridgeImpl` (`electron-bridge.ts:73-140`) — wraps real Electron APIs
- `DaemonBridgeImpl` (`electron-bridge.ts:144+`) — `EventEmitter`-backed, zero Electron dependency
- `tryGetBrowserWindow(): typeof BrowserWindow | null` — daemon mode returns `null`, routes check and return 503 gracefully

Every voice/video/browser/terminal route that references `BrowserWindow` or `webContents` does so through this bridge. **The routes cannot crash the daemon by trying to reach into Electron state that isn't there.**

Verified: `grep -l "from 'electron'"` across all 372 routes returns **zero matches** for voice/video/browser/terminal production files (only `*.test.ts` fixtures). The bridge is the only import surface.

### 2. Systems that import `app/` or `main/` namespaces when they don't need to

**Three legitimate hard-Electron files** (and they're correctly Electron-only; they shouldn't change):

1. `src/main/video-room/video-room-window.ts:15` — `import { BrowserWindow, screen, app } from 'electron'`
   - This is the *optional* wrapper that opens `localhost:5204` in a dedicated window for on-TV use. In a daemon/CLI build, you replace the call site (`video-room-open.ts:19 createVideoRoomWindow()`) with `open http://localhost:5204` (macOS) or `xdg-open` (Linux). **Estimated work: 30 min** — just wrap in an `if (bridge.mode === 'electron')` check and add a `child_process.exec('open …')` else-branch.

2. `src/main/window.ts:5` — `import { BrowserWindow, session, app, systemPreferences } from 'electron'`
   - Lines 65-70: `systemPreferences.askForMediaAccess('microphone')`. This is the **TCC mic grant path** — only needed when running as a signed .app on macOS. In daemon mode, TCC is handled by the launchd plist or by the calling app (e.g. `mahana-terminal` Swift app requests its own TCC permissions for mic).

3. `src/main/services/sovereign-gate.ts:22-24` — Touch ID via `systemPreferences.promptTouchID()`
   - Already gated by `getSystemPreferences()` returning null in non-Electron contexts. In daemon mode, sovereign gate falls back to a different auth path. **Already correctly abstracted.**

**No accidental coupling found.** The three Electron-only files genuinely need Electron for the feature they implement (windowed UI / mac TCC grants / Touch ID). A daemon build just… doesn't load them.

### 3. Already-clean boundaries (name them as gifts)

These subsystems are **already architecturally ready to ship outside Electron, today**:

| Gift | Why it matters |
|---|---|
| **`bridge/electron-bridge.ts`** | The single abstraction that makes daemon mode work. Every device route passes through it. |
| **`browser-do.ts:118-131` dual-backend selector** | Set `MAHANA_BROWSER_BACKEND=puppeteer` and browser ops work without any Electron window. |
| **`pty-daemon/*.ts` (1,237L)** | Separate HTTP+WS server on `:9879`. Already standalone — the Electron side is just a WS client. |
| **`headless.ts:67 --daemon --terminal` flags** | Three modes: headless pure API, `--terminal` with lightweight PTY, `--daemon` full daemon. All tested. |
| **`mahana-terminal/` (Swift)** | Native macOS 14+ terminal app, zero Electron, speaks to daemon via HTTP. Alternative front-end for the Terminal subsystem. |
| **`mahana-unified/` (Capacitor iOS)** | Live iOS surface, zero Electron, wraps web apps via Capacitor webview. Alternative front-end for every subsystem. |
| **`mahana-capture/` (Rust sidecar)** | Sub-10ms CoreAudio/ScreenCaptureKit. Registers with substrate at boot. Peer to daemon, not child. |
| **`mahana-substrate/` (Node, :9877)** | Already runs as `cc.mahana.substrate` launchd service. Zero Electron. This is the proof that the fleet can live without Electron. |
| **`mahana-dream-studio/` + `mahana-canvas/`** | All dream/video-room apps run on web ports (5204-5206). Already standalone. |
| **`packages/translator-proxy/` (CF Worker)** | New package (appeared in today's git status). Deploys to Cloudflare independently. |
| **`mahana-mini-mcp/` (CF Worker)** | Already on `api.mahana-mini.cc`. Deploys via `wrangler`. |

These eleven pieces form **the skeleton of what Mahana Devices is, when you pull the Electron monolith apart.** The work isn't decoupling — it's **naming the bundle, packaging the bundle, and making first-run feel right.**

---

## The four distribution targets implied by the dependency graph

The graph above has exactly **four shippable leaves** (plus web apps that go to Vercel unchanged):

1. **Mahana Daemon** = `src/main/` in headless mode + `pty-daemon/` + `mahana-substrate/`. DMG or curl-sh. Runs as launchd/systemd service.
2. **Mahana Capture** = `mahana-capture/` Rust binary. DMG or curl-sh. Runs as launchd service on macOS, systemd on Linux.
3. **Mahana Terminal** = `mahana-terminal/` Swift app. macOS .app or DMG. Alternative front-end.
4. **Mahana Mobile** = `mahana-unified/` → iOS IPA via Fastlane (already shipping via TestFlight).

Plus CF Workers (`mahana-mini-mcp`, `packages/translator-proxy`) that are edge-deployed, not shipped.

Plus Vercel web apps (`web/apps/*`) that deploy independently.

**"Mahana Devices" is the product name for the bundle of these four shippables + the web surface.**

---

## Coupling risk: where the three-way-drift threat lives

Per the project's `three-way-drift.md` rule (spec/impl/verify must move together), the **most drift-prone seam** is:

```
bridge/electron-bridge.ts  (spec)
  ↕
routes/voice*.ts + services/voice/*  (impl: 43 tryGetBrowserWindow refs)
  ↕
no verification layer that asserts "daemon mode actually degrades gracefully"
```

There is **no current CI test** that runs the daemon in `--daemon` mode and hits every voice/video/browser route to verify 503 degradation. A new route using `tryGetBrowserWindow()` correctly today can be silently broken by a future route that assumes the window exists.

**Recommendation for NEXT-PHASE**: add a `scripts/smoke-daemon-mode.sh` that starts the daemon with `--daemon`, curls every device route, and asserts either `200` or `503 + daemon-mode-fallback` — never `500` / crash. This is the verify layer for the decoupling work already done.
