a73x

c2d9f95d

docs: the module-collapse implementation plan

a73x   2026-09-01 17:29

Commit message
docs: the module-collapse implementation plan

Landed 2026-08-30; committed after the fact so the plans directory holds
the whole sequence the log narrates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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.