600666bc
docs: a module is an owned component, and the tables name one
a73x 2026-08-30 13:19
Commit message
CLAUDE.md
| Old | New | ||
|---|---|---|---|
| @@ -45,15 +45,20 @@ costs ~800k tokens; every token stays in context and is re-billed each turn. | |||
| 45 | ## Layout | 45 | ## Layout |
| 46 | 46 | ||
| 47 | `src/` is grouped by the thing that owns the code, so an app that wants the | 47 | `src/` is grouped by the thing that owns the code, so an app that wants the |
| 48 | engine and a client can link those folders and paint its own way: | 48 | engine and a client can link those folders and paint its own way. A module is |
| 49 | a COMPONENT, not a file: a table row names the root, and the rest of the | ||
| 50 | folder is child files that root re-exports (`term.replica`, `client.hosts`). | ||
| 51 | A file belongs to exactly one module, so a cross-domain grab is a compiler | ||
| 52 | error about a file in two modules, not a missing table edge. Prose here names | ||
| 53 | a symbol by its FILE stem (`wall_pump.askOn`) — a file, not a module. | ||
| 49 | 54 | ||
| 50 | | Folder | Modules | | 55 | | Folder | Row — its child files | |
| 51 | |---|---| | 56 | |---|---| |
| 52 | | `src/engine/` | `protocol` `engine` `delta` `replica` `predict` | | 57 | | `src/engine/` | `term`(`term.zig`) — `protocol` `engine` `delta` `replica` | |
| 53 | | `src/server/` | `server`(+`_agent` `_sessions` `_test_*`) `pty` `quic_server` `cmd` `shellint` `upgrade` | | 58 | | `src/server/` | `daemon`(`server.zig`) — `server_agent` `server_sessions` `cmd` `shellint` `quic_server` `upgrade` `server_test_*` · `pty` | |
| 54 | | `src/client/` | `client` `client_core` `hosts` `handoff` `layout` `webhub` `keymap` `wasm_core` | | 59 | | `src/client/` | `client` — `client_core` `hosts` `handoff` `layout` `keymap` `askpass` · `webhub` · `wasm_core` `client_core_wasm_check` (wasm roots the build wires outside the table) | |
| 55 | | `src/tui/` | `wallview`(+`wall_host` `wall_picker` `wall_pump` `wall_layout` `wall_test_*`) `interact` `paint` `select` | | 60 | | `src/tui/` | `wall`(`wallview.zig`) — `interact` `paint` `select` `predict` `wall_host` `wall_picker` `wall_pump` `wall_layout` `wall_test_*` | |
| 56 | | `src/cli/` | `mux`(dispatch) `main`(daemon) `mux_main`(client) `muxa`(agent) `webhub_main`(hub) `flags` `spawn` | | 61 | | `src/cli/` | `mux`(dispatch) — `main`(daemon) `mux_main`(client) `webhub_main`(hub) · `agent`(`muxa.zig`) · `cliflags`(`flags.zig`) · `spawn` | |
| 57 | | `src/` | `xdg` `sockpath` `proxy` `quic` `testtmp` — what both sides link | | 62 | | `src/` | `xdg` `sockpath` `proxy` `quic` `testtmp` — what both sides link | |
| 58 | 63 | ||
| 59 | `build.zig`'s `checkFolderRules` enforces it: engine and client name no tui, | 64 | `build.zig`'s `checkFolderRules` enforces it: engine and client name no tui, |
| @@ -69,40 +74,41 @@ markers are `flags.zig`, `main.zig` and `server_test_session.zig`, and two | |||
| 69 | of the three cover prose and a fixture rather than a shell the product runs. | 74 | of the three cover prose and a fixture rather than a shell the product runs. |
| 70 | `folder_exemptions` is empty and kept so; rule 4's three remaining debts are | 75 | `folder_exemptions` is empty and kept so; rule 4's three remaining debts are |
| 71 | the markers in `engine.zig`, `protocol.zig` and `keymap.zig`, each of which | 76 | the markers in `engine.zig`, `protocol.zig` and `keymap.zig`, each of which |
| 72 | produces VT bytes by contract. `spawn` lives under `src/cli/` because it | 77 | produces VT bytes by contract. `predict.zig` sits under `src/tui/` with the |
| 73 | asks the OS whether it has a terminal, which rule 4 forbids a client module. | 78 | rest of the wall, so `term` structurally cannot see the overlay. `spawn` |
| 79 | lives under `src/cli/` because it asks the OS whether it has a terminal, | ||
| 80 | which rule 4 forbids a client module. | ||
| 74 | 81 | ||
| 75 | Layers are enforced in the same module table (grep `.layer =` for the graph). | 82 | Layers are enforced in the same module table (grep `.layer =` for the graph). |
| 76 | 83 | ||
| 77 | | Layer | Modules | | 84 | | Layer | Modules | |
| 78 | |---|---| | 85 | |---|---| |
| 79 | | 0 | `protocol` `engine` `pty` `quic` `keymap` `xdg` `sockpath` `proxy` `cliflags` `testtmp` | | 86 | | 0 | `term` `pty` `quic` `proxy` `xdg` `sockpath` `cliflags` `spawn` `testtmp` + `script` `rawmode` `delaypipe` | |
| 80 | | 1 | `client_core` `quic_server` `predict` `spawn`(cli) `handoff` `delta` `cmd` `hosts` `shellint` `replica` `paint` `layout` `select` `upgrade` | | 87 | | 1 | `daemon` `client` `agent` + `render` `ptyclient` `wsclient` | |
| 81 | | 2 | `server` `agent_main` `interact` | | 88 | | 2 | `wall` `webhub` | |
| 82 | | 3 | `client` `daemon_main` | | 89 | | 3 | `mux` — the dispatcher, and the only executable | |
| 83 | | 4 | `webhub` `wallview` | | ||
| 84 | | 5 | `client_main` `hub_main` | | ||
| 85 | | 6 | `mux` — the dispatcher, and the only executable | | ||
| 86 | 90 | ||
| 87 | ONE binary, `mux`, and the first word picks a mode: `mux d` the daemon, | 91 | ONE binary, `mux`, and the first word picks a mode: `mux d` the daemon, |
| 88 | `mux a` the agent surface (JSON verbs), `mux web` the browser hub, and no | 92 | `mux a` the agent surface (JSON verbs), `mux web` the browser hub, and no |
| 89 | letter at all the client. There is NO alias: a word that is not a mode is a | 93 | letter at all the client. There is NO alias: a word that is not a mode is a |
| 90 | transport, so `mux run` names a host called "run" (pinned in `mux.zig`'s | 94 | transport, so `mux run` names a host called "run" (pinned in `mux.zig`'s |
| 91 | `modeOf`). Module names in the table are | 95 | `modeOf`). The daemon's entrypoint, the client's and the hub's are child |
| 92 | `daemon_main` `agent_main` `hub_main` `client_main`; the files keep their old | 96 | files of the `mux` row and keep their old paths (`src/cli/main.zig` is the |
| 93 | paths (`src/cli/main.zig` is the daemon). Test fixtures in `test/`: | 97 | daemon); `agent` stays a row of its own so muxa's suite can fail on its |
| 98 | own. Test fixtures in `test/`: | ||
| 94 | `ptyclient` (real client on a real pty), `wsclient` (browser stand-in), | 99 | `ptyclient` (real client on a real pty), `wsclient` (browser stand-in), |
| 95 | `rawmode`, `delaypipe`, `render` — those stay separate binaries. | 100 | `rawmode`, `delaypipe`, `render` — those stay separate binaries. |
| 96 | 101 | ||
| 97 | ## Invariants — do not break, they are load-bearing | 102 | ## Invariants — do not break, they are load-bearing |
| 98 | 103 | ||
| 99 | - **Transport is dumb.** `proxy.zig` and the QUIC modules carry opaque bytes and | 104 | - **Transport is dumb.** `proxy.zig` and the QUIC modules carry opaque bytes and |
| 100 | know nothing of frames. Keep `protocol` out of `proxy.zig`'s imports. | 105 | know nothing of frames. Keep the wire contract — `term` — out of |
| 106 | `proxy.zig`'s imports. | ||
| 101 | - **Prediction is an overlay.** `predict.zig` output never enters the replica. | 107 | - **Prediction is an overlay.** `predict.zig` output never enters the replica. |
| 102 | - **One replay core.** CLI, wasm, and test fixtures all go through `replica.zig`. | 108 | - **One replay core.** CLI, wasm, and test fixtures all go through `replica.zig`. |
| 103 | Do not hand-roll a second applier. | 109 | Do not hand-roll a second applier. |
| 104 | - **Latest wins.** The grid follows the most recently active client. | 110 | - **Latest wins.** The grid follows the most recently active client. |
| 105 | - **Every tile claims its rect.** The layout tree (`layout.Tree`) is | 111 | - **Every tile claims its rect.** The layout tree (`client.layout.Tree`) is |
| 106 | client-local, owned by the keyboard thread under `paint_mu`; `relayout` | 112 | client-local, owned by the keyboard thread under `paint_mu`; `relayout` |
| 107 | is the single flatten point that turns it into tile rects. Attach sends | 113 | is the single flatten point that turns it into tile rects. Attach sends |
| 108 | the rect, relayout resends it. Focus is client-local and sends nothing | 114 | the rect, relayout resends it. Focus is client-local and sends nothing |
| @@ -112,7 +118,7 @@ paths (`src/cli/main.zig` is the daemon). Test fixtures in `test/`: | |||
| 112 | zoomed on the tile it just attached to, and a wall of one tile whose rect | 118 | zoomed on the tile it just attached to, and a wall of one tile whose rect |
| 113 | is the whole terminal only when there is no terminal at all. A chord-born | 119 | is the whole terminal only when there is no terminal at all. A chord-born |
| 114 | tile inherits the focused tile's target and its `-A`. | 120 | tile inherits the focused tile's target and its `-A`. |
| 115 | - **Resize is gain-only.** `layout.Tree.resize` never shrinks a pane; the | 121 | - **Resize is gain-only.** `client.layout.Tree.resize` never shrinks a pane; the |
| 116 | shrink keys grow a neighbor at the focus's expense in `wall_layout.doResize`. | 122 | shrink keys grow a neighbor at the focus's expense in `wall_layout.doResize`. |
| 117 | - **Rails are painted from `relayout` and tiles cannot reach them.** Every | 123 | - **Rails are painted from `relayout` and tiles cannot reach them.** Every |
| 118 | tile clear is span-bounded ECH, never line-wide, so a tile cannot erase | 124 | tile clear is span-bounded ECH, never line-wide, so a tile cannot erase |
| @@ -141,7 +147,7 @@ paths (`src/cli/main.zig` is the daemon). Test fixtures in `test/`: | |||
| 141 | ride out a blip rather than vanishing. `--via` and `mux a` record no host. | 147 | ride out a blip rather than vanishing. `--via` and `mux a` record no host. |
| 142 | Nothing re-creates a session: a daemon restart heals to what that daemon | 148 | Nothing re-creates a session: a daemon restart heals to what that daemon |
| 143 | now has, which for a fresh daemon is session `0`. | 149 | now has, which for a fresh daemon is session `0`. |
| 144 | `hosts.load` is strict — a bad line refuses `mux` with rc 2 and prints | 150 | `client.hosts.load` is strict — a bad line refuses `mux` with rc 2 and prints |
| 145 | it — while `mux hosts` lists and `mux hosts rm` edits VERBATIM, so the | 151 | it — while `mux hosts` lists and `mux hosts rm` edits VERBATIM, so the |
| 146 | repair tool can read what the loader will not. The BROWSER hub is the | 152 | repair tool can read what the loader will not. The BROWSER hub is the |
| 147 | same wall: `mux web [HOST ...]` records its argv into the file and then | 153 | same wall: `mux web [HOST ...]` records its argv into the file and then |
| @@ -225,7 +231,7 @@ paths (`src/cli/main.zig` is the daemon). Test fixtures in `test/`: | |||
| 225 | so the child's first DebugAllocator stack trace calls `process_vm_readv` | 231 | so the child's first DebugAllocator stack trace calls `process_vm_readv` |
| 226 | on the parent and hits `unreachable // own pid is always valid` | 232 | on the parent and hits `unreachable // own pid is always valid` |
| 227 | (decisions.md, 2026-08-28). | 233 | (decisions.md, 2026-08-28). |
| 228 | - **Remote is still ssh, and still by name.** `handoff` spells | 234 | - **Remote is still ssh, and still by name.** `client.handoff` spells |
| 229 | `ssh HOST 'mux d endpoint'` / `'mux d endpoint --start'`, so a remote | 235 | `ssh HOST 'mux d endpoint'` / `'mux d endpoint --start'`, so a remote |
| 230 | running an installation of v0.0.1-15 or older reads `[unreachable]` until | 236 | running an installation of v0.0.1-15 or older reads `[unreachable]` until |
| 231 | it is upgraded — ≤15 knows neither the mode word nor the flag, and answers | 237 | it is upgraded — ≤15 knows neither the mode word nor the flag, and answers |
| @@ -236,7 +242,7 @@ paths (`src/cli/main.zig` is the daemon). Test fixtures in `test/`: | |||
| 236 | `mux d stop` then `mux d start -d` — as is 16 to the next release, whose | 242 | `mux d stop` then `mux d start -d` — as is 16 to the next release, whose |
| 237 | daemon execs its candidate as `d run` and finds no such verb. That ssh's | 243 | daemon execs its candidate as `d run` and finds no such verb. That ssh's |
| 238 | STDERR is a pipe mux reads, never a writer on somebody's screen: its last | 244 | STDERR is a pipe mux reads, never a writer on somebody's screen: its last |
| 239 | line is the dial's `handoff.Reason` — quoted by the picker row | 245 | line is the dial's `client.handoff.Reason` — quoted by the picker row |
| 240 | (`unreachable: <reason>`) and by the entry dial's failure line — and only | 246 | (`unreachable: <reason>`) and by the entry dial's failure line — and only |
| 241 | the entry dial (`HandoffTarget.narrate`) relays the bytes onward, to its | 247 | the entry dial (`HandoffTarget.narrate`) relays the bytes onward, to its |
| 242 | own fd 2, and only until the wall takes the screen. | 248 | own fd 2, and only until the wall takes the screen. |
| @@ -253,7 +259,7 @@ paths (`src/cli/main.zig` is the daemon). Test fixtures in `test/`: | |||
| 253 | every wall left on a TERMINAL (`shared.is_tty`) — a piped `mux` is a wall | 259 | every wall left on a TERMINAL (`shared.is_tty`) — a piped `mux` is a wall |
| 254 | of one and neither saves nor restores. Restore is verbatim; the aspect | 260 | of one and neither saves nor restores. Restore is verbatim; the aspect |
| 255 | rule applies only when no saved tree exists. Healing is per-leaf against | 261 | rule applies only when no saved tree exists. Healing is per-leaf against |
| 256 | the hosts' live lists. `hosts.load` stays strict (a host line is authored | 262 | the hosts' live lists. `client.hosts.load` stays strict (a host line is authored |
| 257 | intent) while the sidecar is lenient — every failure degrades silently to | 263 | intent) while the sidecar is lenient — every failure degrades silently to |
| 258 | the default cut. The asymmetry is deliberate: a bad host line is a user's | 264 | the default cut. The asymmetry is deliberate: a bad host line is a user's |
| 259 | mistake to fix; a stale layout is a convenience to forget. | 265 | mistake to fix; a stale layout is a convenience to forget. |
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -7587,3 +7587,31 @@ body-capable request that declared no length now closes its connection. | |||
| 7587 | `confirm` shows in the clear, `none` is a notice with no input line that | 7587 | `confirm` shows in the clear, `none` is a notice with no input line that |
| 7588 | closes when ssh kills its helper, anything else is starred. Both ends fold | 7588 | closes when ssh kills its helper, anything else is starred. Both ends fold |
| 7589 | control bytes — a keyboard-interactive prompt is the SERVER's wording. | 7589 | control bytes — a keyboard-interactive prompt is the SERVER's wording. |
| 7590 | |||
| 7591 | ## 2026-08-30 — a module is an owned component, not a compilable file | ||
| 7592 | |||
| 7593 | - **The table halves: 41 rows to 21.** Every file that could compile on its | ||
| 7594 | own had become a row, so the graph named files and the layers counted hops | ||
| 7595 | between them. A row is a COMPONENT now — `term`, `daemon`, `client`, | ||
| 7596 | `wall`, `agent`, `mux` — and the rest of each folder is child files its | ||
| 7597 | root re-exports (`term.replica`, `client.hosts`). The strata fell from | ||
| 7598 | seven to four, four being how deep the components are. | ||
| 7599 | - **The compiler took the enforcement over.** A file belongs to exactly one | ||
| 7600 | module, so a cross-domain grab now fails as "file exists in multiple | ||
| 7601 | modules" while the build graph is built. It used to fail as a missing edge | ||
| 7602 | in a table an author could simply have added the edge to. | ||
| 7603 | - **`predict.zig` moved to `src/tui/`.** The overlay is the wall's, and out | ||
| 7604 | of `src/engine/` the `term` component structurally cannot see it — before, | ||
| 7605 | "prediction never enters the replica" rested on prose and a table edge. | ||
| 7606 | - **`webhub` stayed a row.** A module's root directory is the dirname of its | ||
| 7607 | root file, so `@import("../client/webhub.zig")` from `src/cli/` is "import | ||
| 7608 | of file outside module path": making the hub a child of the dispatcher | ||
| 7609 | needs a file move, which this was not. | ||
| 7610 | - **`daemon_main` briefly reached layer 4 and then stopped existing.** The | ||
| 7611 | daemon's entrypoint is a child file of `mux`; that row lived inside the | ||
| 7612 | branch and never shipped. | ||
| 7613 | - **Test order is banded by what a suite TOUCHES**: no fd, then a pty or a | ||
| 7614 | fixture binary, then the suites that bind a unix socket, with the | ||
| 7615 | seconds-long one last inside its band (`pty` 5s, `daemon` 40s, Debug, | ||
| 7616 | everything else in milliseconds). A wedged step prints nothing, so what | ||
| 7617 | runs before it is the only legible catch. | ||