c2d9f95d
docs: the module-collapse implementation plan
a73x 2026-09-01 17:29
Commit message
docs/superpowers/plans/2026-08-30-module-collapse.md
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,442 @@ | |||
| 1 | # Module-Table Collapse Implementation Plan | ||
| 2 | |||
| 3 | > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. | ||
| 4 | |||
| 5 | **Goal:** Collapse mux's 41-entry build module table to ~15: six domain modules whose child files are relative imports, shared leaves stay tabled. | ||
| 6 | |||
| 7 | **Architecture:** A module in the table = an independently owned component; a file consumed by one domain only becomes a relative `@import("file.zig")` child of that domain's root, reachable from outside (when needed) via `pub const` re-exports on the root. Zig's one-file-one-module rule then blocks cross-domain file grabs at compile time. One file moves (`predict.zig` → `src/tui/`), making "prediction is an overlay" structural: `term` cannot import `wall`. | ||
| 8 | |||
| 9 | **Tech Stack:** Zig 0.15.2 (vendored: `deps/zig/zig`; system zig will NOT build this). Gates: `make check` per task, `make ci` + `make xversion` at the end. | ||
| 10 | |||
| 11 | **Spec:** This plan is its own spec. The agreed end state: | ||
| 12 | |||
| 13 | | Module | Root | Children (leave the table, become relative imports) | | ||
| 14 | |---|---|---| | ||
| 15 | | `term` | `src/engine/term.zig` (new) | `protocol` `engine` `delta` `replica` | | ||
| 16 | | `daemon` | `src/server/server.zig` | `quic_server` `cmd` `shellint` `upgrade` (+ `server_agent` `server_sessions`, already children) | | ||
| 17 | | `client` | `src/client/client.zig` | `client_core` `hosts` `handoff` `layout` `keymap` `askpass` | | ||
| 18 | | `wall` | `src/tui/wallview.zig` | `interact` `paint` `select` `predict` (+ `wall_*`, already children) | | ||
| 19 | | `agent` | `src/cli/muxa.zig` | — | | ||
| 20 | | `mux` | `src/cli/mux.zig` | `main.zig` `mux_main.zig` `webhub_main.zig` `webhub.zig` | | ||
| 21 | |||
| 22 | Shared leaves stay tabled (≥2 domains consume them): `quic` `proxy` `xdg` `sockpath` `spawn` `pty` `cliflags` `testtmp`. Test fixtures stay tabled: `script` `rawmode` `delaypipe` `render` `ptyclient` `wsclient`. | ||
| 23 | |||
| 24 | Final production import lists (this IS the layer graph): | ||
| 25 | - `term` → nothing (plus the `ghostty-vt` dep edge, wired outside the table) | ||
| 26 | - `daemon` → `term` `pty` `quic` `proxy` `xdg` `sockpath` | ||
| 27 | - `client` → `term` `quic` `xdg` `sockpath` | ||
| 28 | - `wall` → `term` `client` `spawn` `proxy` (hosts/handoff/layout/askpass/keymap/core are reached as `@import("client").X`; proxy for ignoreSigpipe — ruled in final review, the header had omitted what Task 1's own row spec included) | ||
| 29 | - `agent` → `term` `quic` `xdg` `sockpath` `cliflags` | ||
| 30 | - `mux` → `term` `daemon` `client` `wall` `agent` `quic` `proxy` `xdg` `sockpath` `spawn` `cliflags` | ||
| 31 | |||
| 32 | ## Global Constraints | ||
| 33 | |||
| 34 | - Build ONLY with `deps/zig/zig` (the Makefile already points at it). Never system zig. | ||
| 35 | - `make check` green before every commit; capture `$?` before piping (`make check; echo rc=$?`). | ||
| 36 | - NO directory reorganization. One `git mv`: `src/engine/predict.zig` → `src/tui/predict.zig`. Every other file stays put. | ||
| 37 | - `docscheck.budget` keys are basenames — the predict move needs no budget edit. New files (`term.zig`) need a budget line; if `zig build check` reports a mismatch after comment edits, run `deps/zig/zig build doc-report 2>&1 | tail -20` and set the line to the reported figure. Lowering is free; raising an existing file's figure is forbidden without user sign-off. | ||
| 38 | - Comments say *why*, not *how*. When editing import blocks, keep existing comments attached to their imports. `zig build check` verifies comment symbol refs resolve. | ||
| 39 | - `test_order` in build.zig must cover every `mod_table` row exactly once (comptime-enforced) — every row you delete from the table must also leave `test_order`. | ||
| 40 | - The layer check is comptime: imports point strictly downward. When a row gains an import, its `.layer` must exceed the dep's. Renumber only in the touched rows; final renumbering happens in Task 6. | ||
| 41 | - Commit per task, message style matches the repo (`refactor: <narrative subject>`). | ||
| 42 | - A unit test that writes to stdout wedges `zig build test` silently — if `make check` hangs with no output for minutes, suspect that first. | ||
| 43 | |||
| 44 | --- | ||
| 45 | |||
| 46 | ### Task 0: Branch | ||
| 47 | |||
| 48 | - [ ] **Step 0.1:** `git -C /home/xanderle/code/rad/mux checkout -b modules` (from main, which must be clean apart from RETRO.md). | ||
| 49 | |||
| 50 | --- | ||
| 51 | |||
| 52 | ### Task 1: Wall collapse | ||
| 53 | |||
| 54 | `interact`, `paint`, `select` become child files of the `wallview` module; `predict.zig` moves to `src/tui/` and becomes a child too. Rename the module `wallview` → `wall`. | ||
| 55 | |||
| 56 | **Files:** | ||
| 57 | - Move: `src/engine/predict.zig` → `src/tui/predict.zig` (git mv) | ||
| 58 | - Modify: `src/tui/wallview.zig`, `src/tui/interact.zig`, `src/tui/wall_picker.zig`, `src/tui/wall_layout.zig`, `src/tui/wall_pump.zig`, `src/tui/wall_test_pump.zig`, `src/tui/wall_test_wall.zig`, `src/tui/wall_test_picker.zig`, `src/cli/mux_main.zig` (imports `wallview` by name), `build.zig` | ||
| 59 | - Also check: `grep -rn '@import("wallview")' src test` and rewrite every hit to `@import("wall")`. | ||
| 60 | |||
| 61 | **Interfaces:** | ||
| 62 | - Consumes: current table rows `interact`, `paint`, `select`, `predict`, `wallview` (build.zig:220–294 region). | ||
| 63 | - Produces: table row `.{ .name = "wall", .path = "src/tui/wallview.zig", ... }`; no re-exports needed (nothing outside tui consumes interact/paint/select/predict). | ||
| 64 | |||
| 65 | - [ ] **Step 1.1: Move predict** | ||
| 66 | |||
| 67 | ```bash | ||
| 68 | git mv src/engine/predict.zig src/tui/predict.zig | ||
| 69 | ``` | ||
| 70 | |||
| 71 | - [ ] **Step 1.2: Rewrite name-imports to relative imports inside src/tui/** | ||
| 72 | |||
| 73 | Every tui file importing a now-child sibling switches to the `.zig` relative form. Exact current sites (re-grep before editing; line numbers drift): | ||
| 74 | |||
| 75 | ```bash | ||
| 76 | grep -rn '@import("interact")\|@import("paint")\|@import("select")\|@import("predict")' src/tui/ | ||
| 77 | ``` | ||
| 78 | |||
| 79 | In each hit, change `@import("interact")` → `@import("interact.zig")`, `@import("paint")` → `@import("paint.zig")`, `@import("select")` → `@import("select.zig")`, `@import("predict")` → `@import("predict.zig")`. Sites as of writing: wallview.zig:29,30,35; interact.zig:26,30,32; wall_picker.zig:12; wall_layout.zig:8; wall_pump.zig:10; wall_test_pump.zig:5; wall_test_wall.zig:5; wall_test_picker.zig:5. | ||
| 80 | |||
| 81 | - [ ] **Step 1.3: Chain the new children's tests from the root** | ||
| 82 | |||
| 83 | `src/tui/wallview.zig` ends with a `test { ... }` block (currently ~line 2416) listing `wall_test_*`. Add the four new children so their `test` blocks stay in the suite: | ||
| 84 | |||
| 85 | ```zig | ||
| 86 | test { | ||
| 87 | _ = @import("interact.zig"); | ||
| 88 | _ = @import("paint.zig"); | ||
| 89 | _ = @import("select.zig"); | ||
| 90 | _ = @import("predict.zig"); | ||
| 91 | _ = @import("wall_test_harness.zig"); | ||
| 92 | // ... existing lines stay ... | ||
| 93 | } | ||
| 94 | ``` | ||
| 95 | |||
| 96 | - [ ] **Step 1.4: Edit the module table** | ||
| 97 | |||
| 98 | In `build.zig`: | ||
| 99 | 1. Delete rows `interact` (line ~248), `paint` (~220), `select` (~231), `predict` (~170). | ||
| 100 | 2. Rename the `wallview` row to `wall` and merge in the deleted rows' import needs (interact consumed `replica`, `client_core`, `keymap`; paint consumed `engine`, `protocol`). New row: | ||
| 101 | |||
| 102 | ```zig | ||
| 103 | .{ .name = "wall", .path = "src/tui/wallview.zig", .layer = 4, .link_libc = true, .imports = &.{ "protocol", "client", "hosts", "handoff", "proxy", "engine", "replica", "client_core", "keymap", "layout", "askpass", "spawn" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | ||
| 104 | ``` | ||
| 105 | |||
| 106 | 3. Remove `interact`, `paint`, `select`, `predict` from `test_order` (~line 816); rename `wallview` → `wall` there. | ||
| 107 | 4. `grep -n 'idxOf("wallview")\|idxOf("interact")\|idxOf("paint")\|idxOf("select")\|idxOf("predict")\|"wallview"' build.zig` — rewrite every named-handle reference to the surviving names (`idxOf("wall")`); delete wiring that only served deleted rows. | ||
| 108 | 5. `src/cli/mux_main.zig` row (`client_main`, ~307): change `"wallview"` → `"wall"` in its imports; change the source line `@import("wallview")` → `@import("wall")` in `src/cli/mux_main.zig`. | ||
| 109 | |||
| 110 | - [ ] **Step 1.5: Gate** | ||
| 111 | |||
| 112 | ```bash | ||
| 113 | make check; echo rc=$? | ||
| 114 | ``` | ||
| 115 | Expected: `rc=0`. If docscheck complains about `predict.zig`'s folder flags, re-read the error — budget keys are basenames, so a failure here is something else; fix what it names. | ||
| 116 | |||
| 117 | - [ ] **Step 1.6: Commit** (BEFORE the mutation probe — a probe against an uncommitted tree cannot be reverted safely; the baseline must be committed first) | ||
| 118 | |||
| 119 | ```bash | ||
| 120 | git add -A && git commit -m "refactor: the wall owns its interaction loop, painter, selector and overlay as child files" | ||
| 121 | ``` | ||
| 122 | |||
| 123 | - [ ] **Step 1.7: Watch the new guard fail once (mutation check, never committed)** | ||
| 124 | |||
| 125 | The collapse's safety claim: cross-domain file grabs are compiler-blocked. Prove it fires: | ||
| 126 | |||
| 127 | ```bash | ||
| 128 | echo 'const sneak = @import("../client/hosts.zig");' >> src/tui/interact.zig | ||
| 129 | make check 2>&1 | grep -i 'file exists in modules' ; echo probe=$? | ||
| 130 | git checkout -- src/tui/interact.zig | ||
| 131 | git status --porcelain src/tui/interact.zig | ||
| 132 | ``` | ||
| 133 | Expected: the grep finds Zig's file-in-multiple-modules error (probe=0), and the final status prints nothing (mutation gone). If probe=1, the guard did NOT fire — stop and investigate before proceeding; do not continue on an unproven safety claim. | ||
| 134 | |||
| 135 | --- | ||
| 136 | |||
| 137 | ### Task 2: Daemon collapse | ||
| 138 | |||
| 139 | `cmd`, `shellint`, `upgrade`, `quic_server` become children of the `server` module; rename it `daemon`. `pty` stays tabled (the `ptyclient` fixture consumes it). The root re-exports `quic_server` and `upgrade` for `src/cli/main.zig`. | ||
| 140 | |||
| 141 | **Files:** | ||
| 142 | - Modify: `src/server/server.zig`, `src/server/server_test_quic.zig`, `server_test_agent.zig`, `server_test_harness.zig`, `server_test_session.zig`, `server_test_upgrade.zig`, `server_test_await.zig`, `src/cli/main.zig`, `build.zig` | ||
| 143 | - Also re-grep: `grep -rn '@import("server")\|@import("cmd")\|@import("shellint")\|@import("upgrade")\|@import("quic_server")' src test` | ||
| 144 | |||
| 145 | **Interfaces:** | ||
| 146 | - Produces: table row `daemon` rooted at `src/server/server.zig`; re-exports on the root: | ||
| 147 | |||
| 148 | ```zig | ||
| 149 | // Re-exported for the daemon's own main (src/cli/main.zig) — the only | ||
| 150 | // consumer outside this folder; nobody else may know these exist. | ||
| 151 | pub const quic_server = @import("quic_server.zig"); | ||
| 152 | pub const upgrade = @import("upgrade.zig"); | ||
| 153 | ``` | ||
| 154 | |||
| 155 | - [ ] **Step 2.1: Rewrite src/server/ name-imports to relative** | ||
| 156 | |||
| 157 | Sites as of writing: server.zig:16 (`cmd`→`cmd.zig`, keep the local alias `cmdmod`), server.zig:17 (`shellint`), server.zig:23 (`quic_server`), server.zig:25 (`upgrade`); server_test_quic.zig:5, server_test_agent.zig:4, server_test_harness.zig:6, server_test_session.zig:5 (all `quic_server`); server_test_upgrade.zig:3 (`upgrade`); server_test_await.zig:3 (`shellint`). Same transformation as Task 1: `@import("X")` → `@import("X.zig")`. | ||
| 158 | |||
| 159 | - [ ] **Step 2.2: Add the two re-exports to server.zig** (code block above, near the existing top-of-file imports) **and chain the children's tests** into server.zig's closing `test {}` block (~line 3600): | ||
| 160 | |||
| 161 | ```zig | ||
| 162 | _ = @import("cmd.zig"); | ||
| 163 | _ = @import("shellint.zig"); | ||
| 164 | _ = @import("upgrade.zig"); | ||
| 165 | _ = @import("quic_server.zig"); | ||
| 166 | ``` | ||
| 167 | |||
| 168 | - [ ] **Step 2.3: Point main.zig at the re-exports** | ||
| 169 | |||
| 170 | `src/cli/main.zig`: line ~12 `const quic_server = @import("quic_server");` → `const quic_server = @import("daemon").quic_server;`; line ~18 `const upgrade = @import("upgrade");` → `const upgrade = @import("daemon").upgrade;`; and its `@import("server")` → `@import("daemon")`. | ||
| 171 | |||
| 172 | - [ ] **Step 2.4: Edit the module table** | ||
| 173 | |||
| 174 | 1. Delete rows `cmd` (~193), `shellint` (~211), `upgrade` (~204), `quic_server` (~165); remove the four from `test_order`. | ||
| 175 | 2. Rename row `server` → `daemon`; its children's deps fold in (cmd: `engine`+`protocol` — already present; shellint: `xdg` — present; upgrade: `protocol` — present; quic_server: `quic` — present). Drop `cmd`, `shellint`, `upgrade`, `quic_server` from its imports: | ||
| 176 | |||
| 177 | ```zig | ||
| 178 | .{ .name = "daemon", .path = "src/server/server.zig", .layer = 2, .link_libc = true, .imports = &.{ "engine", "pty", "protocol", "delta", "sockpath", "quic", "xdg", "proxy" }, .test_imports = &.{ "replica", "testtmp" }, .quic_tests = true }, | ||
| 179 | ``` | ||
| 180 | |||
| 181 | 3. `daemon_main` row (~279): imports drop `quic_server`, `upgrade`; `"server"` → `"daemon"`. | ||
| 182 | 4. `grep -n '"server"\|idxOf("server")\|idxOf("quic_server")\|idxOf("upgrade")\|idxOf("cmd")\|idxOf("shellint")' build.zig` — fix every handle. Note build.zig:594's test-sibling group table names `src/server/server.zig` by path — unchanged. | ||
| 183 | |||
| 184 | - [ ] **Step 2.5: Gate and commit** | ||
| 185 | |||
| 186 | ```bash | ||
| 187 | make check; echo rc=$? | ||
| 188 | git add -A && git commit -m "refactor: the daemon owns its command surface, shell integration, upgrade and QUIC arm as child files" | ||
| 189 | ``` | ||
| 190 | |||
| 191 | --- | ||
| 192 | |||
| 193 | ### Task 3: Client collapse | ||
| 194 | |||
| 195 | `client_core`, `hosts`, `handoff`, `layout`, `keymap`, `askpass` become children of `client`; the root re-exports all six (every one has an outside consumer). The wasm canary and `wasm_core.zig` switch to relative imports; build.zig's wasm wiring drops the two dead twins. | ||
| 196 | |||
| 197 | **Files:** | ||
| 198 | - Modify: `src/client/client.zig`, `src/client/wasm_core.zig`, `src/client/client_core_wasm_check.zig`, `src/tui/wallview.zig`, `wall_picker.zig`, `wall_layout.zig`, `wall_host.zig`, `wall_pump.zig`, `interact.zig`, `wall_test_layout.zig`, `wall_test_harness.zig`, `wall_test_host.zig`, `wall_test_pump.zig`, `wall_test_picker.zig`, `src/cli/mux.zig`, `src/cli/main.zig`, `src/cli/mux_main.zig`, `src/cli/webhub_main.zig`, `build.zig` | ||
| 199 | |||
| 200 | **Interfaces:** | ||
| 201 | - Produces on `src/client/client.zig`: | ||
| 202 | |||
| 203 | ```zig | ||
| 204 | // The client link's public seams: the wall, the hub and the mains reach | ||
| 205 | // these as client.X — the table stays one row, the files stay children. | ||
| 206 | pub const hosts = @import("hosts.zig"); | ||
| 207 | pub const handoff = @import("handoff.zig"); | ||
| 208 | pub const layout = @import("layout.zig"); | ||
| 209 | pub const keymap = @import("keymap.zig"); | ||
| 210 | pub const askpass = @import("askpass.zig"); | ||
| 211 | pub const core = @import("client_core.zig"); | ||
| 212 | ``` | ||
| 213 | |||
| 214 | (`core`, not `client_core` — `client.client_core` stutters; every outside consumer spells `@import("client").core`.) | ||
| 215 | |||
| 216 | - [ ] **Step 3.1: Inside src/client/: name → relative** | ||
| 217 | |||
| 218 | client.zig:20 `keymap`, :24 `handoff`, :25 `askpass`, :26 `hosts` → `.zig` forms. wasm_core.zig:23 `keymap`, :25 `client_core` → `.zig` forms. client_core_wasm_check.zig:1 `client_core` → `client_core.zig`. Re-grep first: | ||
| 219 | |||
| 220 | ```bash | ||
| 221 | grep -rn '@import("client_core")\|@import("hosts")\|@import("handoff")\|@import("layout")\|@import("keymap")\|@import("askpass")' src | ||
| 222 | ``` | ||
| 223 | |||
| 224 | - [ ] **Step 3.2: Outside consumers → re-exports** | ||
| 225 | |||
| 226 | For every hit OUTSIDE `src/client/` from the same grep, rewrite (adding `const client = @import("client");` to the file's import block if absent — wall files: it's the module the wall already imports): | ||
| 227 | - `@import("hosts")` → `@import("client").hosts` | ||
| 228 | - `@import("handoff")` → `@import("client").handoff` | ||
| 229 | - `@import("layout")` → `@import("client").layout` | ||
| 230 | - `@import("askpass")` → `@import("client").askpass` | ||
| 231 | - `@import("keymap")` → `@import("client").keymap` (interact.zig:35 imports `.detach_key` — becomes `@import("client").keymap.detach_key`) | ||
| 232 | - `@import("client_core")` → `@import("client").core` | ||
| 233 | |||
| 234 | Sites as of writing: wallview.zig:23,24,25,36; wall_picker.zig:10,11,13; wall_layout.zig:7,9; wall_host.zig:9,10; wall_pump.zig:9; wall_test_layout.zig:4,5; wall_test_harness.zig:7; wall_test_host.zig:5; wall_test_pump.zig:7; wall_test_picker.zig:6; interact.zig:27,31,35; mux.zig:17; main.zig:16; mux_main.zig:22,25; webhub_main.zig:16. | ||
| 235 | |||
| 236 | - [ ] **Step 3.3: client.zig root additions** | ||
| 237 | |||
| 238 | Add the re-export block (Interfaces above) and chain child tests into client.zig's `test {}` block (grep `test {` in client.zig; create the block at file end if none): | ||
| 239 | |||
| 240 | ```zig | ||
| 241 | _ = @import("client_core.zig"); | ||
| 242 | _ = @import("hosts.zig"); | ||
| 243 | _ = @import("handoff.zig"); | ||
| 244 | _ = @import("layout.zig"); | ||
| 245 | _ = @import("keymap.zig"); | ||
| 246 | _ = @import("askpass.zig"); | ||
| 247 | ``` | ||
| 248 | |||
| 249 | - [ ] **Step 3.4: Edit the module table** | ||
| 250 | |||
| 251 | 1. Delete rows `client_core` (~161), `hosts` (~199), `handoff` (~180), `askpass` (~185), `layout` (~224), `keymap` (~131); remove all six from `test_order`. | ||
| 252 | 2. `client` row (~272): drop `keymap`, `handoff`, `hosts`, `askpass` from imports (now children); keep `protocol`, `replica`, `quic`, `xdg`, `sockpath`; keep `test_imports = &.{"testtmp"}` (hosts/handoff/askpass tests used testtmp — it must stay reachable in the test twin). | ||
| 253 | 3. Rows that imported the deleted names: `wall` (drop `hosts` `handoff` `client_core` `keymap` `layout` `askpass` — it has `client`), `daemon_main` (drop `handoff`), `client_main` (drop `handoff` `hosts`), `hub_main` (drop `hosts`), `mux` (drop `askpass`), `agent_main`/`interact` — interact row is gone; agent_main never had them. Re-grep the table: `grep -n '"hosts"\|"handoff"\|"layout"\|"keymap"\|"client_core"\|"askpass"' build.zig`. | ||
| 254 | 4. wasm wiring (~line 1001–1030): the table-driven twin loop now only covers `protocol` `engine` `replica` (still tabled with `.wasm`). Hand edits: delete `const client_core_wasm_mod = ...` handle; `wasm_core_mod` keeps `engine`/`protocol`/`replica` addImports, DROP the `client_core` and `keymap` addImports (now relative children of wasm_core's own module). The canary: `client_core_wasm_check_mod.addImport("client_core", ...)` → replace with `client_core_wasm_check_mod.addImport("protocol", wasm_mods[comptime idxOf("protocol")].?);` (client_core.zig, now its relative child, imports `protocol` by name). | ||
| 255 | |||
| 256 | - [ ] **Step 3.5: Gate and commit** | ||
| 257 | |||
| 258 | ```bash | ||
| 259 | make check; echo rc=$? | ||
| 260 | git add -A && git commit -m "refactor: the client link owns its core, hosts file, handoff, layout, keymap and askpass as child files behind pub seams" | ||
| 261 | ``` | ||
| 262 | |||
| 263 | --- | ||
| 264 | |||
| 265 | ### Task 4: Executable collapse | ||
| 266 | |||
| 267 | The four mains and `webhub` become children of the `mux` dispatcher module. `agent` (muxa) stays its own module — `cliflags` therefore stays a tabled leaf. | ||
| 268 | |||
| 269 | **Files:** | ||
| 270 | - Modify: `src/cli/mux.zig`, `src/cli/webhub_main.zig`, `build.zig` | ||
| 271 | - Rename module `agent_main` → `agent` (grep `@import("agent_main")` — only mux.zig:14). | ||
| 272 | |||
| 273 | **Interfaces:** | ||
| 274 | - Consumes: `@import("client").askpass` (already rewritten in Task 3). | ||
| 275 | - Produces: one row `mux` whose children are the mains; `agent` row unchanged but renamed. | ||
| 276 | |||
| 277 | - [ ] **Step 4.1: mux.zig dispatch → relative children** | ||
| 278 | |||
| 279 | mux.zig:13–16: `@import("daemon_main")` → `@import("main.zig")`, `@import("agent_main")` → `@import("agent")` (module — stays a name import), `@import("hub_main")` → `@import("webhub_main.zig")`, `@import("client_main")` → `@import("mux_main.zig")`. | ||
| 280 | |||
| 281 | - [ ] **Step 4.2: webhub becomes a child too** | ||
| 282 | |||
| 283 | webhub_main.zig:15: `@import("webhub")` → `@import("../client/webhub.zig")`. (Legal: relative imports may traverse up; webhub.zig joins the mux module. Its own `@import("protocol")`/`@import("client")` resolve against the mux row's imports.) | ||
| 284 | |||
| 285 | - [ ] **Step 4.3: Edit the module table** | ||
| 286 | |||
| 287 | 1. Delete rows `daemon_main` (~279), `client_main` (~307), `hub_main` (~301), `webhub` (~284); remove from `test_order`; rename `agent_main` → `agent` (row ~260). | ||
| 288 | 2. New `mux` row = union of the deleted rows' imports minus dead names: | ||
| 289 | |||
| 290 | ```zig | ||
| 291 | .{ .name = "mux", .path = "src/cli/mux.zig", .layer = 6, .link_libc = true, .imports = &.{ "daemon", "client", "wall", "agent", "protocol", "proxy", "quic", "xdg", "spawn", "sockpath", "cliflags" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | ||
| 292 | ``` | ||
| 293 | |||
| 294 | 3. The anonymous imports (~line 1053): `hub_main_mod.addAnonymousImport("index.html", ...)` etc. — the handle `hub_main_mod` is gone; attach all three to `mux_mod` instead (webhub_main.zig is now a mux child; `@embedFile` resolves against its module). The comment about sequencing the wasm build before the hub's stays true — keep it. | ||
| 295 | 4. Fix named handles: `grep -n 'idxOf("daemon_main")\|idxOf("client_main")\|idxOf("hub_main")\|idxOf("agent_main")\|idxOf("webhub")\|hub_main_mod\|daemon_main\|client_main' build.zig`. The `mux` exe wiring (~line 914–960) already builds from `idxOf("mux")` — unchanged. | ||
| 296 | |||
| 297 | - [ ] **Step 4.4: Chain the mains' tests from mux.zig** | ||
| 298 | |||
| 299 | Grep each of main.zig / mux_main.zig / webhub_main.zig / webhub.zig for `test `. Add to (or create at the end of) mux.zig: | ||
| 300 | |||
| 301 | ```zig | ||
| 302 | test { | ||
| 303 | _ = @import("main.zig"); | ||
| 304 | _ = @import("mux_main.zig"); | ||
| 305 | _ = @import("webhub_main.zig"); | ||
| 306 | _ = @import("../client/webhub.zig"); | ||
| 307 | } | ||
| 308 | ``` | ||
| 309 | |||
| 310 | - [ ] **Step 4.5: Gate and commit** | ||
| 311 | |||
| 312 | ```bash | ||
| 313 | make check; echo rc=$? | ||
| 314 | git add -A && git commit -m "refactor: the one binary's mains and the hub page are children of the dispatcher" | ||
| 315 | ``` | ||
| 316 | |||
| 317 | --- | ||
| 318 | |||
| 319 | ### Task 5: Term collapse | ||
| 320 | |||
| 321 | The big sed: `protocol`, `engine`, `delta`, `replica` fold under a new root `src/engine/term.zig`. Every consumer spells `@import("term").X`; the four files import each other relatively. ghostty-vt moves to the term module (native and wasm). | ||
| 322 | |||
| 323 | **Files:** | ||
| 324 | - Create: `src/engine/term.zig` | ||
| 325 | - Modify: every file the greps below hit (~25 files), `build.zig`, `docscheck.budget` | ||
| 326 | |||
| 327 | **Interfaces:** | ||
| 328 | - Produces `src/engine/term.zig`: | ||
| 329 | |||
| 330 | ```zig | ||
| 331 | //! The terminal component: the wire contract, the authoritative engine, | ||
| 332 | //! the delta minting that feeds replicas, and the one replay core. One | ||
| 333 | //! table row — the four files are one owner, and only this root is a seam. | ||
| 334 | pub const protocol = @import("protocol.zig"); | ||
| 335 | pub const engine = @import("engine.zig"); | ||
| 336 | pub const delta = @import("delta.zig"); | ||
| 337 | pub const replica = @import("replica.zig"); | ||
| 338 | |||
| 339 | test { | ||
| 340 | _ = protocol; | ||
| 341 | _ = engine; | ||
| 342 | _ = delta; | ||
| 343 | _ = replica; | ||
| 344 | } | ||
| 345 | ``` | ||
| 346 | |||
| 347 | - [ ] **Step 5.1: Inside src/engine/: name → relative** | ||
| 348 | |||
| 349 | delta.zig imports `engine`+`protocol`; replica.zig imports `engine`+`protocol`; check engine.zig and protocol.zig too: | ||
| 350 | |||
| 351 | ```bash | ||
| 352 | grep -rn '@import("engine")\|@import("protocol")\|@import("delta")\|@import("replica")' src/engine/ | ||
| 353 | ``` | ||
| 354 | Rewrite each to the `.zig` relative form. | ||
| 355 | |||
| 356 | - [ ] **Step 5.2: Everywhere else: name → term.X** | ||
| 357 | |||
| 358 | ```bash | ||
| 359 | grep -rln '@import("protocol")\|@import("engine")\|@import("delta")\|@import("replica")' src test | ||
| 360 | ``` | ||
| 361 | In every hit (~25 files, none under src/engine/ after 5.1), apply: | ||
| 362 | - `@import("protocol")` → `@import("term").protocol` | ||
| 363 | - `@import("engine")` → `@import("term").engine` | ||
| 364 | - `@import("delta")` → `@import("term").delta` | ||
| 365 | - `@import("replica")` → `@import("term").replica` | ||
| 366 | |||
| 367 | This is sed-able per file; verify with a final grep that zero name-form sites remain outside src/engine/. Keep local alias names as they are (`const protocol = @import("term").protocol;` — call sites don't change). | ||
| 368 | |||
| 369 | - [ ] **Step 5.3: Edit the module table** | ||
| 370 | |||
| 371 | 1. Add the row (and to `test_order`): | ||
| 372 | |||
| 373 | ```zig | ||
| 374 | .{ .name = "term", .path = "src/engine/term.zig", .layer = 0, .wasm = true }, | ||
| 375 | ``` | ||
| 376 | |||
| 377 | 2. Delete rows `protocol`, `engine`, `delta`, `replica`; remove from `test_order`. | ||
| 378 | 3. Every row importing any of the four: replace with a single `"term"` (dedup!). As of Task 4's end that is: `daemon` (had engine, protocol, delta), `client` (protocol, replica), `wall` (protocol, engine, replica), `agent` (protocol), `mux` (protocol), `render` (engine), `wsclient` (engine, replica, protocol), `paint`/`cmd`/etc. are already children. `daemon`'s `test_imports` drops `replica` (term is a production import now). Re-grep: `grep -n '"protocol"\|"engine"\|"delta"\|"replica"' build.zig` until only comments remain. | ||
| 379 | 4. ghostty native (~line 930): `engine_mod.addImport("ghostty-vt", ...)` — the handle comes from `idxOf("engine")`; change to `idxOf("term")` / `term_mod`. The import name `"ghostty-vt"` is consumed by engine.zig, now a term child — attaching it to the term module is exactly right. | ||
| 380 | 5. wasm (~line 1001+): the `.wasm` twin loop now instantiates only `term`. Replace the explicit engine/protocol/replica handles: `term_wasm_mod = wasm_mods[comptime idxOf("term")].?`; ghostty wasm import attaches to `term_wasm_mod`; `wasm_core_mod` drops addImports `engine`/`protocol`/`replica`/`client_core`/`keymap` (the latter two died in Task 3) and gains `wasm_core_mod.addImport("term", term_wasm_mod);`. The canary swaps its `protocol` addImport for `term`: `client_core_wasm_check_mod.addImport("term", term_wasm_mod);` (client_core.zig now spells `@import("term").protocol`). | ||
| 381 | 6. src/client/wasm_core.zig source: its `@import("engine")` etc. were already rewritten to `term.X` by Step 5.2 — confirm. | ||
| 382 | 7. The wasm-closure comptime check (~line 346) still holds (term has no imports); leave it. | ||
| 383 | |||
| 384 | - [ ] **Step 5.4: Budget line for the new file** | ||
| 385 | |||
| 386 | `zig build check` will demand a `term.zig` figure. Run `deps/zig/zig build doc-report 2>&1 | grep term` and add the exact reported line to `docscheck.budget` (alphabetical position matching the file's ordering convention — read the neighboring lines). | ||
| 387 | |||
| 388 | - [ ] **Step 5.5: Gate and commit** | ||
| 389 | |||
| 390 | ```bash | ||
| 391 | make check; echo rc=$? | ||
| 392 | git add -A && git commit -m "refactor: one term component — wire contract, engine, delta and replay core behind a single seam" | ||
| 393 | ``` | ||
| 394 | |||
| 395 | --- | ||
| 396 | |||
| 397 | ### Task 6: Layers, folder rules, docs | ||
| 398 | |||
| 399 | Renumber layers to the real ranks, audit the folder rules against the shrunken table, update CLAUDE.md and decisions.md. | ||
| 400 | |||
| 401 | **Files:** | ||
| 402 | - Modify: `build.zig`, `CLAUDE.md`, `docs/decisions.md` | ||
| 403 | |||
| 404 | - [ ] **Step 6.1: Renumber `.layer`** | ||
| 405 | |||
| 406 | Final ranks — leaves 0, domains by depth: | ||
| 407 | - 0: `term` `quic` `proxy` `xdg` `sockpath` `pty` `cliflags` `testtmp` `spawn` `script` `rawmode` `delaypipe` | ||
| 408 | - 1: `daemon` `client` `agent` `render` `ptyclient` `wsclient` | ||
| 409 | - 2: `wall` `webhub` (webhub survived Task 4 as a row: Zig forbids relative imports outside the module root's dirname, so src/client/webhub.zig cannot be a child of the src/cli/ dispatcher without a file move; ruled kept-as-row) | ||
| 410 | - 3: `mux` | ||
| 411 | |||
| 412 | (`spawn` at 0 assumes its row still lists no imports — verify before renumbering.) | ||
| 413 | |||
| 414 | Adjust every row; the comptime check (imports strictly downward) is the verifier — `make check` fails loudly on any wrong rank. Also update the comment at the comptime block (~line 329) that says "32-row, 76-edge table" to the new true counts (count them: `grep -c '.name =' build.zig` minus non-table hits). | ||
| 415 | |||
| 416 | - [ ] **Step 6.2: Folder-rules audit** | ||
| 417 | |||
| 418 | `checkFolderRules` (build.zig:402) iterates table rows — it still compiles unchanged, but verify the three domain rules still have teeth by reading what edges remain: rule 3 (client imports no tui) now guards the `client` row; rule 2 (server imports no client/tui) guards `daemon`; rule 1 guards `term`+`client`. The rules 4–6 source bans are path-keyed and untouched — but rule 4's file set: `predict.zig` moved OUT of src/engine/ into src/tui/, where the VT-byte ban does not apply. Check `folder_exemptions` (should stay empty) and the `source_bans` file lists for any literal `predict` path; fix if named. Run the mutation probe from Task 1 Step 1.6 once more if any rule text was edited. | ||
| 419 | |||
| 420 | - [ ] **Step 6.3: CLAUDE.md** | ||
| 421 | |||
| 422 | Rewrite the Layout section: the folder table (module column now names roots + child files), the layer table (new 0–3 ranks above), and the sentence "Layers are enforced in the same module table". Update the module names in prose (`server`→`daemon` module, `wallview`→`wall`; file paths unchanged — say so). The reading-guide file sizes are unchanged (no content moved except predict) — leave them. Update the line "grep `.layer =` for the graph" only if still true (it is). | ||
| 423 | |||
| 424 | - [ ] **Step 6.4: decisions.md entry** | ||
| 425 | |||
| 426 | Append (matching the file's dated-entry format — read the last entry for shape) a decision: module = owned component, not compilable file; table 41→~15; children are relative imports; cross-domain grabs now die on Zig's one-file-one-module error where they used to die on a missing table edge; predict lives with the wall so term structurally cannot see it. | ||
| 427 | |||
| 428 | - [ ] **Step 6.5: Gate and commit** | ||
| 429 | |||
| 430 | ```bash | ||
| 431 | make check; echo rc=$? | ||
| 432 | git add -A && git commit -m "refactor: layers renumbered to the six-component graph; docs follow" | ||
| 433 | ``` | ||
| 434 | |||
| 435 | --- | ||
| 436 | |||
| 437 | ### Task 7: Delivery gates | ||
| 438 | |||
| 439 | - [ ] **Step 7.1:** `make ci; echo rc=$?` — expected rc=0 (check + e2e + agent + throughput). E2e runs real binaries; nothing in this refactor touches the wire or behavior, so any red here is a genuine wiring mistake — debug, don't waive. | ||
| 440 | - [ ] **Step 7.2:** `XVER_OLD_WORKTREE=../mux-xver-old make xversion-build xversion; echo rc=$?` — expected rc=0 (12/12). The old worktree must exist at `../mux-xver-old`; a stale prefix reads as a failed gate — rebuild first, per the target's own recipe. | ||
| 441 | - [ ] **Step 7.3:** Review the branch story: `git log --oneline main..HEAD` — 7-ish commits, each a component. Squash fixups if any accumulated (`git rebase -i` is unavailable; use `git commit --fixup` + `GIT_SEQUENCE_EDITOR=true git rebase --autosquash main` only if needed). | ||
| 442 | - [ ] **Step 7.4:** STOP. Do not merge to main, push, or install — report completion with the gate outputs and wait for the user. | ||