a73x

600666bc

docs: a module is an owned component, and the tables name one

a73x   2026-08-30 13:19

Commit message
docs: a module is an owned component, and the tables name one

The folder table names the row and the child files it re-exports; the layer
table is the four real ranks. Every dead module spelling in the prose is now
either a file path or a live seam (`client.layout.Tree`, `client.hosts.load`),
and one sentence says which of the two a name is.

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.