86beb855
Remove the layer and folder-edge enforcement; keep the source bans
a73x 2026-08-31 15:12
Commit message
CLAUDE.md
| Old | New | ||
|---|---|---|---|
| @@ -61,32 +61,27 @@ a symbol by its FILE stem (`wall_pump.askOn`) — a file, not a module. | |||
| 61 | | `src/cli/` | `mux`(dispatch) — `main`(daemon) `mux_main`(client) `webhub_main`(hub) · `agent`(`muxa.zig`) · `cliflags`(`flags.zig`) · `spawn` | | 61 | | `src/cli/` | `mux`(dispatch) — `main`(daemon) `mux_main`(client) `webhub_main`(hub) · `agent`(`muxa.zig`) · `cliflags`(`flags.zig`) · `spawn` | |
| 62 | | `src/` | `xdg` `sockpath` `proxy` `quic` `testtmp` — what both sides link | | 62 | | `src/` | `xdg` `sockpath` `proxy` `quic` `testtmp` — what both sides link | |
| 63 | 63 | ||
| 64 | `build.zig`'s `checkFolderRules` enforces it: engine and client name no tui, | 64 | The grouping itself is a convention now, not a gate: the table wires the |
| 65 | server or cli module; server names no client and no terminal; tui imports | 65 | build and says who owns what, and an import edit is an ordinary diff. What |
| 66 | client and never the reverse; and no line outside a `test` block under | 66 | `build.zig` still ENFORCES is `checkSourceBans`, which reads the PRODUCTION |
| 67 | `src/engine/` or `src/client/` spells `termios`, `isatty`, `tcgetattr`, | 67 | lines of every file under `src/` for bytes no import graph can catch. Rule 4: |
| 68 | `tcsetattr` or an escape byte without a `// folder rule 4 exemption:` line | 68 | no line outside a `test` block under `src/engine/` or `src/client/` spells |
| 69 | saying why — matched against a lower-cased line, so `\X1B[` is the same | 69 | `termios`, `isatty`, `tcgetattr`, `tcsetattr` or an escape byte without a |
| 70 | needle as `\x1b[`. Rule 5 is the product-wide one: no file under `src/` at | 70 | `// folder rule 4 exemption:` line saying why — matched against a lower-cased |
| 71 | all may spell `"/bin/sh"` or `"-c"`, because every program mux runs is | 71 | line, so `\X1B[` is the same needle as `\x1b[`. Rule 5 is the product-wide |
| 72 | exec'd as argv and no shell of ours parses a line we built; its three | 72 | one: no file under `src/` at all may spell `"/bin/sh"` or `"-c"`, because |
| 73 | markers are `flags.zig`, `main.zig` and `server_test_session.zig`, and two | 73 | every program mux runs is exec'd as argv and no shell of ours parses a line |
| 74 | of the three cover prose and a fixture rather than a shell the product runs. | 74 | we built; its three markers are `flags.zig`, `main.zig` and |
| 75 | `folder_exemptions` is empty and kept so; rule 4's three remaining debts are | 75 | `server_test_session.zig`, and two of the three cover prose and a fixture |
| 76 | the markers in `engine.zig`, `protocol.zig` and `keymap.zig`, each of which | 76 | rather than a shell the product runs. Rule 6 is the one `posix.fork` site, |
| 77 | produces VT bytes by contract. `predict.zig` sits under `src/tui/` with the | 77 | and it names its file in `except` rather than in the file. The three keep |
| 78 | rest of the wall, so `term` structurally cannot see the overlay. `spawn` | 78 | their numbers because the in-file `folder rule N exemption:` lines cite them |
| 79 | lives under `src/cli/` because it asks the OS whether it has a terminal, | 79 | by number. Rule 4's three remaining debts are the markers in `engine.zig`, |
| 80 | which rule 4 forbids a client module. | 80 | `protocol.zig` and `keymap.zig`, each of which produces VT bytes by contract. |
| 81 | 81 | `predict.zig` sits under `src/tui/` with the rest of the wall, so the overlay | |
| 82 | Layers are enforced in the same module table (grep `.layer =` for the graph). | 82 | lives beside the code that paints it and nowhere `term` would find it. |
| 83 | 83 | `spawn` lives under `src/cli/` because it asks the OS whether it has a | |
| 84 | | Layer | Modules | | 84 | terminal, which rule 4 forbids a client module. |
| 85 | |---|---| | ||
| 86 | | 0 | `term` `pty` `quic` `proxy` `xdg` `sockpath` `cliflags` `spawn` `testtmp` + `script` `rawmode` `delaypipe` | | ||
| 87 | | 1 | `daemon` `client` `agent` + `render` `ptyclient` `wsclient` | | ||
| 88 | | 2 | `wall` `webhub` | | ||
| 89 | | 3 | `mux` — the dispatcher, and the only executable | | ||
| 90 | 85 | ||
| 91 | ONE binary, `mux`, and the first word picks a mode: `mux d` the daemon, | 86 | ONE binary, `mux`, and the first word picks a mode: `mux d` the daemon, |
| 92 | `mux a` the agent surface (JSON verbs), `mux web` the browser hub, and no | 87 | `mux a` the agent surface (JSON verbs), `mux web` the browser hub, and no |
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -74,7 +74,7 @@ fn linkQuic(b: *std.Build, c: *std.Build.Step.Compile, deps: anytype) void { | |||
| 74 | 74 | ||
| 75 | /// Fail at build-graph-construction time with one clean line. The hygiene | 75 | /// Fail at build-graph-construction time with one clean line. The hygiene |
| 76 | /// invariants below are not opinions a gate step samples: a violation has | 76 | /// invariants below are not opinions a gate step samples: a violation has |
| 77 | /// to stop every build, the way the comptime layer checks do, so the graph | 77 | /// to stop every build, the way the comptime table checks do, so the graph |
| 78 | /// simply refuses to be built. | 78 | /// simply refuses to be built. |
| 79 | fn fatal(comptime fmt: []const u8, args: anytype) noreturn { | 79 | fn fatal(comptime fmt: []const u8, args: anytype) noreturn { |
| 80 | std.debug.print("build.zig: " ++ fmt ++ "\n", args); | 80 | std.debug.print("build.zig: " ++ fmt ++ "\n", args); |
| @@ -86,24 +86,21 @@ fn fatal(comptime fmt: []const u8, args: anytype) noreturn { | |||
| 86 | /// table-module import can exist except through this loop — violations | 86 | /// table-module import can exist except through this loop — violations |
| 87 | /// are impossible, not detected. (Dependency edges — ghostty — and | 87 | /// are impossible, not detected. (Dependency edges — ghostty — and |
| 88 | /// build_options are explicitly outside the table's jurisdiction and | 88 | /// build_options are explicitly outside the table's jurisdiction and |
| 89 | /// stay wired by hand.) Layers are the topological strata of the production | 89 | /// stay wired by hand.) A row is one owned COMPONENT rather than one file |
| 90 | /// graph, computed 2026-08-14 and re-stratified 2026-08-30, when the table | 90 | /// (re-grouped 2026-08-30): the root names the module and re-exports its |
| 91 | /// became one row per owned COMPONENT rather than one per file: the strata | 91 | /// child files, so a second row claiming one of those files is a compile |
| 92 | /// are four deep because the components are, and a row's children no longer | 92 | /// error about a file in two modules. |
| 93 | /// buy it a layer. FROZEN either way — a new import that would flatten or | ||
| 94 | /// invert a stratum fails at comptime, and re-stratifying requires editing | ||
| 95 | /// this table, which is the point. | ||
| 96 | const ModSpec = struct { | 93 | const ModSpec = struct { |
| 97 | name: []const u8, | 94 | name: []const u8, |
| 98 | path: []const u8, | 95 | path: []const u8, |
| 99 | layer: u8, | 96 | /// Production imports: what every instance of this row may @import, the |
| 100 | /// Production imports: must point at a strictly lower layer. | 97 | /// exes built from it included. |
| 101 | imports: []const []const u8 = &.{}, | 98 | imports: []const []const u8 = &.{}, |
| 102 | /// Test-only imports (the testtmp pattern): granted ONLY to the row's | 99 | /// Test-only imports (the testtmp pattern): granted ONLY to the row's |
| 103 | /// test twin — the second module instance `addTest` compiles — so the | 100 | /// test twin — the second module instance `addTest` compiles — so the |
| 104 | /// production instance every exe and every importer sees has no such | 101 | /// production instance every exe and every importer sees has no such |
| 105 | /// module available. "Test scaffolding never ships" is therefore a | 102 | /// module available. "Test scaffolding never ships" is therefore a |
| 106 | /// compile error, not a convention. Excluded from the strata computation. | 103 | /// compile error, not a convention. |
| 107 | test_imports: []const []const u8 = &.{}, | 104 | test_imports: []const []const u8 = &.{}, |
| 108 | link_libc: bool = false, | 105 | link_libc: bool = false, |
| 109 | /// Also instantiated against wasm32 (the browser core's twins). | 106 | /// Also instantiated against wasm32 (the browser core's twins). |
| @@ -113,7 +110,7 @@ const ModSpec = struct { | |||
| 113 | }; | 110 | }; |
| 114 | 111 | ||
| 115 | const mod_table = [_]ModSpec{ | 112 | const mod_table = [_]ModSpec{ |
| 116 | // ---- layer 0: imports nothing internal in production ---- | 113 | // ---- the leaves: they import nothing internal in production ---- |
| 117 | // The terminal component, one row over four files: the wire contract, | 114 | // The terminal component, one row over four files: the wire contract, |
| 118 | // the authoritative engine, the daemon-side delta minting and the one | 115 | // the authoritative engine, the daemon-side delta minting and the one |
| 119 | // replay core, reached as `term.protocol`, `.engine`, `.delta`, | 116 | // replay core, reached as `term.protocol`, `.engine`, `.delta`, |
| @@ -124,56 +121,56 @@ const mod_table = [_]ModSpec{ | |||
| 124 | // internal, which is what lets the tracker and the replay core be | 121 | // internal, which is what lets the tracker and the replay core be |
| 125 | // driven by a test holding an engine and no socket; and the whole | 122 | // driven by a test holding an engine and no socket; and the whole |
| 126 | // component is platform-free, so `mux_core.wasm` compiles it. | 123 | // component is platform-free, so `mux_core.wasm` compiles it. |
| 127 | .{ .name = "term", .path = "src/engine/term.zig", .layer = 0, .wasm = true }, | 124 | .{ .name = "term", .path = "src/engine/term.zig", .wasm = true }, |
| 128 | .{ .name = "pty", .path = "src/server/pty.zig", .layer = 0, .link_libc = true }, | 125 | .{ .name = "pty", .path = "src/server/pty.zig", .link_libc = true }, |
| 129 | // The QUIC vocabulary both ends share: the one @cImport of the vendored | 126 | // The QUIC vocabulary both ends share: the one @cImport of the vendored |
| 130 | // stack, the key, the wire constants, the egress ring. It has to be ONE | 127 | // stack, the key, the wire constants, the egress ring. It has to be ONE |
| 131 | // module — two @cImport blocks over the same headers are two distinct | 128 | // module — two @cImport blocks over the same headers are two distinct |
| 132 | // type universes, and `ngtcp2_vec`s cross between listener and client. | 129 | // type universes, and `ngtcp2_vec`s cross between listener and client. |
| 133 | .{ .name = "quic", .path = "src/quic.zig", .layer = 0, .link_libc = true, .quic_tests = true }, | 130 | .{ .name = "quic", .path = "src/quic.zig", .link_libc = true, .quic_tests = true }, |
| 134 | // Test-only: short temp paths for the tests that bind unix sockets. | 131 | // Test-only: short temp paths for the tests that bind unix sockets. |
| 135 | // Imported by every module that has such a test, which is why it is a | 132 | // Imported by every module that has such a test, which is why it is a |
| 136 | // module rather than three copies. | 133 | // module rather than three copies. |
| 137 | .{ .name = "testtmp", .path = "src/testtmp.zig", .layer = 0 }, | 134 | .{ .name = "testtmp", .path = "src/testtmp.zig" }, |
| 138 | // What the two scripted fixtures share: the escape table and the exit | 135 | // What the two scripted fixtures share: the escape table and the exit |
| 139 | // codes. One copy, so ptyclient and wsclient cannot disagree about | 136 | // codes. One copy, so ptyclient and wsclient cannot disagree about |
| 140 | // what a scenario's heredoc sent. | 137 | // what a scenario's heredoc sent. |
| 141 | .{ .name = "script", .path = "test/script.zig", .layer = 0 }, | 138 | .{ .name = "script", .path = "test/script.zig" }, |
| 142 | // Test helpers, built as real binaries because that is how the suite | 139 | // Test helpers, built as real binaries because that is how the suite |
| 143 | // uses them: rawmode is a deterministic stand-in for an editor (nvim's | 140 | // uses them: rawmode is a deterministic stand-in for an editor (nvim's |
| 144 | // redraw timing is its own business and it is not installed everywhere), | 141 | // redraw timing is its own business and it is not installed everywhere), |
| 145 | // and delaypipe makes a slow round trip out of a shell pipeline instead | 142 | // and delaypipe makes a slow round trip out of a shell pipeline instead |
| 146 | // of out of netem and root. | 143 | // of out of netem and root. |
| 147 | .{ .name = "rawmode", .path = "test/rawmode.zig", .layer = 0 }, | 144 | .{ .name = "rawmode", .path = "test/rawmode.zig" }, |
| 148 | .{ .name = "delaypipe", .path = "test/delaypipe.zig", .layer = 0 }, | 145 | .{ .name = "delaypipe", .path = "test/delaypipe.zig" }, |
| 149 | // XDG-derived paths (key file, daemon log), shared by both binaries. | 146 | // XDG-derived paths (key file, daemon log), shared by both binaries. |
| 150 | .{ .name = "xdg", .path = "src/xdg.zig", .layer = 0, .link_libc = true, .test_imports = &.{"testtmp"} }, | 147 | .{ .name = "xdg", .path = "src/xdg.zig", .link_libc = true, .test_imports = &.{"testtmp"} }, |
| 151 | // The socket path's identity and the right to bind it: the stale-socket | 148 | // The socket path's identity and the right to bind it: the stale-socket |
| 152 | // claim and the dev+ino record teardown compares against. A leaf — it | 149 | // claim and the dev+ino record teardown compares against. A leaf — it |
| 153 | // takes a path and nothing else, and knows no Server exists. | 150 | // takes a path and nothing else, and knows no Server exists. |
| 154 | .{ .name = "sockpath", .path = "src/sockpath.zig", .layer = 0, .test_imports = &.{"testtmp"} }, | 151 | .{ .name = "sockpath", .path = "src/sockpath.zig", .test_imports = &.{"testtmp"} }, |
| 155 | // No imports that teach it anything, deliberately: the proxy is a byte | 152 | // No imports that teach it anything, deliberately: the proxy is a byte |
| 156 | // pump that knows nothing about the protocol it carries. `testtmp` is | 153 | // pump that knows nothing about the protocol it carries. `testtmp` is |
| 157 | // the one exception and does not weaken that — it hands its tests a | 154 | // the one exception and does not weaken that — it hands its tests a |
| 158 | // short directory to put a socket in and knows nothing about the bytes. | 155 | // short directory to put a socket in and knows nothing about the bytes. |
| 159 | .{ .name = "proxy", .path = "src/proxy.zig", .layer = 0, .link_libc = true, .test_imports = &.{"testtmp"} }, | 156 | .{ .name = "proxy", .path = "src/proxy.zig", .link_libc = true, .test_imports = &.{"testtmp"} }, |
| 160 | // Reflection over a caller's options struct, so it imports nothing: the | 157 | // Reflection over a caller's options struct, so it imports nothing: the |
| 161 | // struct is the flag table and the parser learns it at comptime. | 158 | // struct is the flag table and the parser learns it at comptime. |
| 162 | .{ .name = "cliflags", .path = "src/cli/flags.zig", .layer = 0 }, | 159 | .{ .name = "cliflags", .path = "src/cli/flags.zig" }, |
| 163 | // This image, as a path something can exec. Under src/cli/ because it | 160 | // This image, as a path something can exec. Under src/cli/ because it |
| 164 | // asks the OS about the process it is in — a question no headless | 161 | // asks the OS about the process it is in — a question no headless |
| 165 | // client may spell. | 162 | // client may spell. |
| 166 | .{ .name = "spawn", .path = "src/cli/spawn.zig", .layer = 0, .link_libc = true }, | 163 | .{ .name = "spawn", .path = "src/cli/spawn.zig", .link_libc = true }, |
| 167 | // ---- layer 1: single-hop over the leaves ---- | 164 | // ---- single-hop over the leaves ---- |
| 168 | // Replays a captured client stdout stream and prints the final grid in | 165 | // Replays a captured client stdout stream and prints the final grid in |
| 169 | // `mux d dump`'s formats — the client half of the M11 render-vs-dump | 166 | // `mux d dump`'s formats — the client half of the M11 render-vs-dump |
| 170 | // convergence check. Imports term so both sides of the diff go through | 167 | // convergence check. Imports term so both sides of the diff go through |
| 171 | // the same ghostty-vt and the same formatter. | 168 | // the same ghostty-vt and the same formatter. |
| 172 | .{ .name = "render", .path = "test/render.zig", .layer = 1, .imports = &.{"term"} }, | 169 | .{ .name = "render", .path = "test/render.zig", .imports = &.{"term"} }, |
| 173 | // The pty-driving e2e fixture: real client on a pty slave, scripted | 170 | // The pty-driving e2e fixture: real client on a pty slave, scripted |
| 174 | // from stdin (M12). Imports pty so the product's own module is the one | 171 | // from stdin (M12). Imports pty so the product's own module is the one |
| 175 | // under it. | 172 | // under it. |
| 176 | .{ .name = "ptyclient", .path = "test/ptyclient.zig", .layer = 1, .link_libc = true, .imports = &.{ "pty", "script" } }, | 173 | .{ .name = "ptyclient", .path = "test/ptyclient.zig", .link_libc = true, .imports = &.{ "pty", "script" } }, |
| 177 | // The command state machine, shell integration, the upgrade vocabulary | 174 | // The command state machine, shell integration, the upgrade vocabulary |
| 178 | // and the QUIC listener are CHILD FILES of this root rather than modules | 175 | // and the QUIC listener are CHILD FILES of this root rather than modules |
| 179 | // of their own, so nothing outside src/server/ can name one: a second | 176 | // of their own, so nothing outside src/server/ can name one: a second |
| @@ -185,7 +182,7 @@ const mod_table = [_]ModSpec{ | |||
| 185 | // daemon itself when nobody handed it a --key — and for the shim | 182 | // daemon itself when nobody handed it a --key — and for the shim |
| 186 | // directory shell integration writes under the same 0700 policy. | 183 | // directory shell integration writes under the same 0700 policy. |
| 187 | // `pty` stays a row of its own: the ptyclient fixture consumes it. | 184 | // `pty` stays a row of its own: the ptyclient fixture consumes it. |
| 188 | .{ .name = "daemon", .path = "src/server/server.zig", .layer = 1, .link_libc = true, .imports = &.{ "term", "pty", "sockpath", "quic", "xdg", "proxy" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | 185 | .{ .name = "daemon", .path = "src/server/server.zig", .link_libc = true, .imports = &.{ "term", "pty", "sockpath", "quic", "xdg", "proxy" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, |
| 189 | // The agent-facing client. It speaks frames and owns no terminal, which | 186 | // The agent-facing client. It speaks frames and owns no terminal, which |
| 190 | // is the whole point — it attaches at 0x0 and never claims the grid. | 187 | // is the whole point — it attaches at 0x0 and never claims the grid. |
| 191 | // The transport modules are the CLI client's, minus everything that | 188 | // The transport modules are the CLI client's, minus everything that |
| @@ -194,8 +191,8 @@ const mod_table = [_]ModSpec{ | |||
| 194 | // wire contract and nothing else — no engine and no replica, muxa having | 191 | // wire contract and nothing else — no engine and no replica, muxa having |
| 195 | // nothing to draw: a fact of muxa.zig itself, which the one-row component | 192 | // nothing to draw: a fact of muxa.zig itself, which the one-row component |
| 196 | // no longer refuses on its behalf. | 193 | // no longer refuses on its behalf. |
| 197 | .{ .name = "agent", .path = "src/cli/muxa.zig", .layer = 1, .link_libc = true, .imports = &.{ "term", "sockpath", "quic", "xdg", "cliflags" }, .quic_tests = true }, | 194 | .{ .name = "agent", .path = "src/cli/muxa.zig", .link_libc = true, .imports = &.{ "term", "sockpath", "quic", "xdg", "cliflags" }, .quic_tests = true }, |
| 198 | .{ .name = "wsclient", .path = "test/wsclient.zig", .layer = 1, .link_libc = true, .imports = &.{ "term", "script" } }, | 195 | .{ .name = "wsclient", .path = "test/wsclient.zig", .link_libc = true, .imports = &.{ "term", "script" } }, |
| 199 | // Dialling, and what a chord means. The client is the only thing that | 196 | // Dialling, and what a chord means. The client is the only thing that |
| 200 | // predicts — the overlay is a local display decision and never becomes | 197 | // predicts — the overlay is a local display decision and never becomes |
| 201 | // state anybody else can see — but the predicting itself is interact's | 198 | // state anybody else can see — but the predicting itself is interact's |
| @@ -209,8 +206,8 @@ const mod_table = [_]ModSpec{ | |||
| 209 | // sits here rather than in either front so the CLI wall and the browser | 206 | // sits here rather than in either front so the CLI wall and the browser |
| 210 | // hub resolve a host line the same way. Nothing here WRITES that file — | 207 | // hub resolve a host line the same way. Nothing here WRITES that file — |
| 211 | // `wall_host.recordHost` and `webhub_main` do. | 208 | // `wall_host.recordHost` and `webhub_main` do. |
| 212 | .{ .name = "client", .path = "src/client/client.zig", .layer = 1, .link_libc = true, .imports = &.{ "term", "quic", "xdg", "sockpath" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | 209 | .{ .name = "client", .path = "src/client/client.zig", .link_libc = true, .imports = &.{ "term", "quic", "xdg", "sockpath" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, |
| 213 | // ---- layer 2 ---- | 210 | // ---- the two fronts ---- |
| 214 | // The browser hub's HTTP/WebSocket decisions: Origin gate, route table, | 211 | // The browser hub's HTTP/WebSocket decisions: Origin gate, route table, |
| 215 | // WS endpoint naming. Assets are injected (the exe root @embedFiles | 212 | // WS endpoint naming. Assets are injected (the exe root @embedFiles |
| 216 | // them), so its tests build no artifacts. It stays a row rather than | 213 | // them), so its tests build no artifacts. It stays a row rather than |
| @@ -218,11 +215,11 @@ const mod_table = [_]ModSpec{ | |||
| 218 | // module's root directory is the dirname of its root file, and | 215 | // module's root directory is the dirname of its root file, and |
| 219 | // `@import("../client/webhub.zig")` from src/cli is "import of file | 216 | // `@import("../client/webhub.zig")` from src/cli is "import of file |
| 220 | // outside module path" — a compiler rule, not a table choice. | 217 | // outside module path" — a compiler rule, not a table choice. |
| 221 | .{ .name = "webhub", .path = "src/client/webhub.zig", .layer = 2, .imports = &.{ "term", "client" }, .quic_tests = true }, | 218 | .{ .name = "webhub", .path = "src/client/webhub.zig", .imports = &.{ "term", "client" }, .quic_tests = true }, |
| 222 | // The CLI wall (`mux wall`): multiattach stripes in one terminal, one of | 219 | // The CLI wall (`mux wall`): multiattach stripes in one terminal, one of |
| 223 | // which can be ZOOMED — promoted to the terminal's size and typed | 220 | // which can be ZOOMED — promoted to the terminal's size and typed |
| 224 | // through. Same layer as webhub for the same reason — both sit on | 221 | // through. It sits beside webhub for the same reason — both are fronts |
| 225 | // client's Transport and `client.hosts`' grammar; neither may import the | 222 | // on client's Transport and `client.hosts`' grammar; neither imports the |
| 226 | // other; both resolve a spelling through `client.Target.fromSpec`. | 223 | // other; both resolve a spelling through `client.Target.fromSpec`. |
| 227 | // The interaction loop, the painter, the selector and the prediction | 224 | // The interaction loop, the painter, the selector and the prediction |
| 228 | // overlay are CHILD FILES of this root rather than modules of their own, | 225 | // overlay are CHILD FILES of this root rather than modules of their own, |
| @@ -232,8 +229,8 @@ const mod_table = [_]ModSpec{ | |||
| 232 | // loop's alone, which the root never spells, and the painter takes | 229 | // loop's alone, which the root never spells, and the painter takes |
| 233 | // `term.engine` and `term.protocol`; the decoder and the key table they | 230 | // `term.engine` and `term.protocol`; the decoder and the key table they |
| 234 | // also want reach them through `client`'s seams. | 231 | // also want reach them through `client`'s seams. |
| 235 | .{ .name = "wall", .path = "src/tui/wallview.zig", .layer = 2, .link_libc = true, .imports = &.{ "term", "client", "proxy", "spawn" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | 232 | .{ .name = "wall", .path = "src/tui/wallview.zig", .link_libc = true, .imports = &.{ "term", "client", "proxy", "spawn" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, |
| 236 | // ---- layer 3: the one binary ---- | 233 | // ---- the one binary ---- |
| 237 | // Four words, one image — and one row: the daemon's entrypoint, the | 234 | // Four words, one image — and one row: the daemon's entrypoint, the |
| 238 | // client's and the hub's are CHILD FILES of the dispatcher, so a second | 235 | // client's and the hub's are CHILD FILES of the dispatcher, so a second |
| 239 | // module claiming any of them is a file-in-multiple-modules compile | 236 | // module claiming any of them is a file-in-multiple-modules compile |
| @@ -251,7 +248,7 @@ const mod_table = [_]ModSpec{ | |||
| 251 | // downstream must refuse) and `sockpath` for the sun_path bound every verb | 248 | // downstream must refuse) and `sockpath` for the sun_path bound every verb |
| 252 | // checks before acting on a path. `testtmp` is the keygen round-trip's: | 249 | // checks before acting on a path. `testtmp` is the keygen round-trip's: |
| 253 | // it needs a directory to generate into, which the daemon never touches. | 250 | // it needs a directory to generate into, which the daemon never touches. |
| 254 | .{ .name = "mux", .path = "src/cli/mux.zig", .layer = 3, .link_libc = true, .imports = &.{ "daemon", "client", "wall", "agent", "webhub", "term", "proxy", "quic", "xdg", "spawn", "sockpath", "cliflags" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | 251 | .{ .name = "mux", .path = "src/cli/mux.zig", .link_libc = true, .imports = &.{ "daemon", "client", "wall", "agent", "webhub", "term", "proxy", "quic", "xdg", "spawn", "sockpath", "cliflags" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, |
| 255 | }; | 252 | }; |
| 256 | 253 | ||
| 257 | /// Comptime row lookup. Every hand-written module name in this file goes | 254 | /// Comptime row lookup. Every hand-written module name in this file goes |
| @@ -264,118 +261,47 @@ fn idxOf(comptime name: []const u8) usize { | |||
| 264 | @compileError("module table: unknown module '" ++ name ++ "'"); | 261 | @compileError("module table: unknown module '" ++ name ++ "'"); |
| 265 | } | 262 | } |
| 266 | 263 | ||
| 267 | fn layerOf(comptime name: []const u8) u8 { | ||
| 268 | return mod_table[idxOf(name)].layer; | ||
| 269 | } | ||
| 270 | |||
| 271 | comptime { | 264 | comptime { |
| 272 | // Both checks are O(edges x rows) name comparisons; the default 1000 | 265 | // Both checks are O(edges x rows) name comparisons; the default 1000 |
| 273 | // backwards branches does not cover a 21-row, 45-edge table (38 | 266 | // backwards branches does not cover a 21-row, 45-edge table (38 |
| 274 | // production imports and 7 test-only ones). | 267 | // production imports and 7 test-only ones). |
| 275 | @setEvalBranchQuota(20_000); | 268 | @setEvalBranchQuota(20_000); |
| 276 | for (mod_table) |m| { | 269 | for (mod_table) |m| { |
| 277 | for (m.imports) |dep| { | 270 | // Every edge names a real row, production and test-only alike, so a |
| 278 | if (layerOf(dep) >= m.layer) @compileError(std.fmt.comptimePrint( | 271 | // typo or a name left behind by a rename is a compile error here |
| 279 | "layer violation: {s} (layer {d}) imports {s} (layer {d}) — " ++ | 272 | // rather than the wiring loop's runtime `unreachable`. |
| 280 | "production imports point strictly downward; re-stratifying " ++ | 273 | for (m.imports) |dep| _ = idxOf(dep); |
| 281 | "is a deliberate edit to mod_table, never an accident", | 274 | for (m.test_imports) |dep| _ = idxOf(dep); |
| 282 | .{ m.name, m.layer, dep, layerOf(dep) }, | 275 | // A flagged row is also compiled against wasm32-freestanding, and |
| 283 | )); | 276 | // having a `.wasm` twin is exactly what says a module compiles |
| 284 | } | 277 | // there — so an import without one cannot. Mechanical rather than |
| 285 | // Test-only imports skip the direction rule but must name real rows. | 278 | // architectural, and failing here names the offending edge, where |
| 286 | for (m.test_imports) |dep| _ = layerOf(dep); | 279 | // the twin loop's later @panic could only say a module was missing. |
| 287 | // The wasm set must be closed under production imports: a twin can | ||
| 288 | // only import twins, so a row that gains an import from outside the | ||
| 289 | // flagged set fails here rather than at the twin loop's @panic. | ||
| 290 | if (m.wasm) for (m.imports) |dep| { | 280 | if (m.wasm) for (m.imports) |dep| { |
| 291 | if (!mod_table[idxOf(dep)].wasm) @compileError(std.fmt.comptimePrint( | 281 | if (!mod_table[idxOf(dep)].wasm) @compileError(std.fmt.comptimePrint( |
| 292 | "wasm row {s} imports {s}, which has no .wasm twin — the " ++ | 282 | "wasm row {s} imports {s}, which has no .wasm twin — a twin " ++ |
| 293 | "wasm set must be closed under production imports", | 283 | "can only import twins", |
| 294 | .{ m.name, dep }, | 284 | .{ m.name, dep }, |
| 295 | )); | 285 | )); |
| 296 | }; | 286 | }; |
| 297 | } | 287 | } |
| 298 | } | 288 | } |
| 299 | 289 | ||
| 300 | /// The domain folders under `src/`. A module lives in the folder of the thing | 290 | /// The folders the doc gate walks. Listed rather than globbed: a new folder |
| 301 | /// that owns it, and this is what "owns" costs: an app that links `term` and | 291 | /// under `src/` is a decision about who owns what, and a glob would let one |
| 302 | /// `client` to paint its own way must be able to do so without dragging a | 292 | /// appear — with every file in it ungated — as a side effect of a mkdir. |
| 303 | /// terminal in. `mux_core.wasm` already links that set with no tty anywhere, | ||
| 304 | /// which is the proof the rule generalises past this repo's own binaries. | ||
| 305 | /// | ||
| 306 | /// `root` is the shared layer-0 utilities both sides link; `foreign` is the | ||
| 307 | /// test fixtures under `test/`, which are nobody's domain and unconstrained. | ||
| 308 | const Folder = enum { root, engine, server, client, tui, cli, foreign }; | ||
| 309 | |||
| 310 | fn folderOf(path: []const u8) Folder { | ||
| 311 | if (!std.mem.startsWith(u8, path, "src/")) return .foreign; | ||
| 312 | const rest = path["src/".len..]; | ||
| 313 | const slash = std.mem.indexOfScalar(u8, rest, '/') orelse return .root; | ||
| 314 | const dir = rest[0..slash]; | ||
| 315 | inline for (@typeInfo(Folder).@"enum".fields) |f| { | ||
| 316 | if (std.mem.eql(u8, dir, f.name)) return @field(Folder, f.name); | ||
| 317 | } | ||
| 318 | fatal("folder rule: src/{s}/ is not a domain folder — add it to Folder " ++ | ||
| 319 | "or put the module in one that exists", .{dir}); | ||
| 320 | } | ||
| 321 | |||
| 322 | /// The folders the doc gate and the folder rules both walk. Listed rather | ||
| 323 | /// than globbed: a new domain folder is a decision about who owns what, and | ||
| 324 | /// it must be made here, in the enum above, and in the rules below together. | ||
| 325 | const src_dirs = [_][]const u8{ "src", "src/engine", "src/server", "src/client", "src/tui", "src/cli" }; | 293 | const src_dirs = [_][]const u8{ "src", "src/engine", "src/server", "src/client", "src/tui", "src/cli" }; |
| 326 | 294 | ||
| 327 | /// A folder edge the rules forbid and this repo still has, signed with its | 295 | /// The source bans, read off the PRODUCTION lines of the files under `src/`. |
| 328 | /// reason. Not a waiver mechanism to reach for: an entry here is a stated | 296 | /// They catch what the import graph cannot: a module needs no import to |
| 329 | /// debt, and the rule's whole value is that removing one is a visible diff. | 297 | /// spell an escape byte, a shell path or a fork, so these read the sources |
| 330 | /// Empty, and kept so — an edge nobody could sign is one nobody adds quietly. | 298 | /// themselves. The rules keep the numbers 4, 5 and 6 they were given, because |
| 331 | const FolderExemption = struct { from: []const u8, to: []const u8, why: []const u8 }; | 299 | /// files across the repo cite them by number in their own |
| 332 | const folder_exemptions = [_]FolderExemption{}; | 300 | /// `folder rule N exemption:` lines. |
| 333 | 301 | fn checkSourceBans(b: *std.Build) void { | |
| 334 | fn folderExempt(from: []const u8, to: []const u8) bool { | ||
| 335 | for (folder_exemptions) |e| { | ||
| 336 | if (std.mem.eql(u8, e.from, from) and std.mem.eql(u8, e.to, to)) return true; | ||
| 337 | } | ||
| 338 | return false; | ||
| 339 | } | ||
| 340 | |||
| 341 | /// The folder rules, checked over the same table the layers are. Layers say | ||
| 342 | /// which way an import may point; folders say which domain may know the other | ||
| 343 | /// exists — two different questions, and a legal layer edge can still be an | ||
| 344 | /// illegal domain edge (`client` importing `tui` points downward and is | ||
| 345 | /// exactly what must not compile into a headless app). | ||
| 346 | fn checkFolderRules(b: *std.Build) void { | ||
| 347 | for (&mod_table) |spec| { | ||
| 348 | const from = folderOf(spec.path); | ||
| 349 | for (spec.imports) |dep| { | ||
| 350 | const to = folderOf(mod_table[idx0(dep)].path); | ||
| 351 | if (folderExempt(spec.name, dep)) continue; | ||
| 352 | if (from == .client and to == .tui) fatal( | ||
| 353 | "folder rule 3 broken: {s} (src/client/) imports {s} (src/tui/) — " ++ | ||
| 354 | "tui imports client, never the reverse", | ||
| 355 | .{ spec.name, dep }, | ||
| 356 | ); | ||
| 357 | if ((from == .engine or from == .client) and | ||
| 358 | (to == .tui or to == .server or to == .cli)) fatal( | ||
| 359 | "folder rule 1 broken: {s} (src/{s}/) imports {s} (src/{s}/) — " ++ | ||
| 360 | "engine and client name no tui, server or cli module", | ||
| 361 | .{ spec.name, @tagName(from), dep, @tagName(to) }, | ||
| 362 | ); | ||
| 363 | if (from == .server and (to == .client or to == .tui)) fatal( | ||
| 364 | "folder rule 2 broken: {s} (src/server/) imports {s} (src/{s}/) — " ++ | ||
| 365 | "the daemon knows of no client and no terminal", | ||
| 366 | .{ spec.name, dep, @tagName(to) }, | ||
| 367 | ); | ||
| 368 | } | ||
| 369 | } | ||
| 370 | for (source_bans) |ban| checkSourceBan(b, ban); | 302 | for (source_bans) |ban| checkSourceBan(b, ban); |
| 371 | } | 303 | } |
| 372 | 304 | ||
| 373 | /// Runtime twin of `idxOf`, for the loops above that read the table as data. | ||
| 374 | fn idx0(name: []const u8) usize { | ||
| 375 | for (&mod_table, 0..) |m, i| if (std.mem.eql(u8, m.name, name)) return i; | ||
| 376 | fatal("module table: unknown module '{s}'", .{name}); | ||
| 377 | } | ||
| 378 | |||
| 379 | /// A byte a folder's PRODUCTION lines may not spell. The import graph | 305 | /// A byte a folder's PRODUCTION lines may not spell. The import graph |
| 380 | /// cannot catch a module that writes the bytes itself, so these read the | 306 | /// cannot catch a module that writes the bytes itself, so these read the |
| 381 | /// sources the way the doc gate does. | 307 | /// sources the way the doc gate does. |
| @@ -486,8 +412,8 @@ fn checkSourceBan(b: *std.Build, ban: SourceBan) void { | |||
| 486 | 412 | ||
| 487 | /// Every grant the table hands out must be one the source asked for: for | 413 | /// Every grant the table hands out must be one the source asked for: for |
| 488 | /// each name in a row's `imports` / `test_imports`, that row's root source | 414 | /// each name in a row's `imports` / `test_imports`, that row's root source |
| 489 | /// file has to contain `@import("<name>")`. The layer rules cannot see a | 415 | /// file has to contain `@import("<name>")`. The table's own checks cannot |
| 490 | /// stale grant — an edge nobody uses violates no direction — so a grant | 416 | /// see a stale grant — an edge nobody uses still names a real row — so a grant |
| 491 | /// outlives the code that needed it silently, and the table stops being a | 417 | /// outlives the code that needed it silently, and the table stops being a |
| 492 | /// description of the program. (Exactly how `exe` kept `cmd` after main.zig | 418 | /// description of the program. (Exactly how `exe` kept `cmd` after main.zig |
| 493 | /// stopped importing it; found in review 2026-08-14, and this is the check | 419 | /// stopped importing it; found in review 2026-08-14, and this is the check |
| @@ -708,7 +634,7 @@ fn zigFilesIn(b: *std.Build, sub: []const u8, paths: *std.ArrayList([]const u8)) | |||
| 708 | /// "comments say why, not how" and "code, comments, docs drift" since the | 634 | /// "comments say why, not how" and "code, comments, docs drift" since the |
| 709 | /// first commit; a week of drift showed that prose is instruction and only a | 635 | /// first commit; a week of drift showed that prose is instruction and only a |
| 710 | /// check that RUNS is codification — the same reasoning behind the comptime | 636 | /// check that RUNS is codification — the same reasoning behind the comptime |
| 711 | /// layer laws above. It gates REFERENCES, not length: a comment must still | 637 | /// table checks above. It gates REFERENCES, not length: a comment must still |
| 712 | /// name something real, and may take as many lines as saying so takes. | 638 | /// name something real, and may take as many lines as saying so takes. |
| 713 | /// | 639 | /// |
| 714 | /// The tool is a build tool, not part of the program, so it stays out of the | 640 | /// The tool is a build tool, not part of the program, so it stays out of the |
| @@ -770,7 +696,7 @@ fn docGate(b: *std.Build, target: std.Build.ResolvedTarget, check_step: *std.Bui | |||
| 770 | } | 696 | } |
| 771 | 697 | ||
| 772 | /// Test registration order — the order failures ARRIVE in, and deliberately | 698 | /// Test registration order — the order failures ARRIVE in, and deliberately |
| 773 | /// NOT the layers'. A suite that waits on a socket can wedge, and a wedged | 699 | /// NOT the table's. A suite that waits on a socket can wedge, and a wedged |
| 774 | /// step prints nothing at all; whatever runs before it is the only legible | 700 | /// step prints nothing at all; whatever runs before it is the only legible |
| 775 | /// catch. So the suites that CAN wedge go last: from `pty` on, every row | 701 | /// catch. So the suites that CAN wedge go last: from `pty` on, every row |
| 776 | /// either waits on a pty or imports `testtmp`, which is what a row asks for | 702 | /// either waits on a pty or imports `testtmp`, which is what a row asks for |
| @@ -797,7 +723,7 @@ comptime { | |||
| 797 | if (test_order.len != mod_table.len) | 723 | if (test_order.len != mod_table.len) |
| 798 | @compileError("test_order must cover every mod_table row exactly once"); | 724 | @compileError("test_order must cover every mod_table row exactly once"); |
| 799 | for (test_order, 0..) |n, i| { | 725 | for (test_order, 0..) |n, i| { |
| 800 | _ = layerOf(n); | 726 | _ = idxOf(n); |
| 801 | for (test_order[i + 1 ..]) |n2| { | 727 | for (test_order[i + 1 ..]) |n2| { |
| 802 | if (std.mem.eql(u8, n, n2)) @compileError("duplicate in test_order: " ++ n); | 728 | if (std.mem.eql(u8, n, n2)) @compileError("duplicate in test_order: " ++ n); |
| 803 | } | 729 | } |
| @@ -835,8 +761,8 @@ pub fn build(b: *std.Build) void { | |||
| 835 | checkGrantsUsed(b); | 761 | checkGrantsUsed(b); |
| 836 | // ...and no domain of a split file's tests goes unreached (nor the reverse). | 762 | // ...and no domain of a split file's tests goes unreached (nor the reverse). |
| 837 | checkSiblingTestsReached(b); | 763 | checkSiblingTestsReached(b); |
| 838 | // ...and every module still sits in the folder of the thing that owns it. | 764 | // ...and no production line under src/ spells a byte a source ban forbids. |
| 839 | checkFolderRules(b); | 765 | checkSourceBans(b); |
| 840 | 766 | ||
| 841 | // Two instances per row that has test grants: `mods[i]` is production — | 767 | // Two instances per row that has test grants: `mods[i]` is production — |
| 842 | // what the exes are built from and what every importer sees — and | 768 | // what the exes are built from and what every importer sees — and |
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -7653,3 +7653,51 @@ gone, with their baselines and the `doc-report` step; tiers 1 and 2 stay. | |||
| 7653 | - **What is unguarded now**: nothing stops a comment growing into narration | 7653 | - **What is unguarded now**: nothing stops a comment growing into narration |
| 7654 | again. That is a reviewer's job, and the trade is deliberate — a reviewer | 7654 | again. That is a reviewer's job, and the trade is deliberate — a reviewer |
| 7655 | can tell an essay from an explanation and a byte count cannot. | 7655 | can tell an essay from an explanation and a byte count cannot. |
| 7656 | |||
| 7657 | ## 2026-08-31 — the module-architecture enforcement is removed | ||
| 7658 | |||
| 7659 | `build.zig` had two mechanisms enforcing SHAPE on the import graph: layers | ||
| 7660 | (`ModSpec.layer`, `layerOf`, and a comptime check that every production | ||
| 7661 | import point at a strictly lower stratum) and folder rules 1–3 (engine and | ||
| 7662 | client name no tui/server/cli module; server names no client and no terminal; | ||
| 7663 | client never names tui), with a signed-exemption table for edges the rules | ||
| 7664 | forbid. All of that is gone, along with the `Folder` enum, `folderOf`, | ||
| 7665 | `FolderExemption`, `folder_exemptions` and `folderExempt`; | ||
| 7666 | `checkFolderRules` becomes `checkSourceBans`. | ||
| 7667 | |||
| 7668 | - **The trigger was a product primitive with nowhere legal to live.** | ||
| 7669 | Connecting a client to a daemon is the one operation this product exists to | ||
| 7670 | perform, and the daemon's own tests hand-roll it at 175 sites because no | ||
| 7671 | callable primitive exists where they can reach one. Every home for a | ||
| 7672 | ~40-line `dial` module was refused by the build, not by the design: `term` | ||
| 7673 | owns the attach encoders and is wasm-pinned, so it cannot open a socket; | ||
| 7674 | layer 0 may import nothing, so `sockpath` cannot compose dial+attach; an | ||
| 7675 | import must point STRICTLY downward, so a module `client` (layer 1) shares | ||
| 7676 | forces renumbering the whole graph; and folder rule 2 walls the daemon's | ||
| 7677 | tests off from `client.Transport.open`, the primitive the product does | ||
| 7678 | have. We were costing out a 175-site test sweep to avoid a 40-line module. | ||
| 7679 | - **A gate that makes the product change harder than the test rewrite is | ||
| 7680 | optimizing the wrong direction.** The layers described the graph honestly | ||
| 7681 | and then froze it; their failure mode is not a bad edge sneaking in but a | ||
| 7682 | good edge never being drawn, which nothing reports and no diff shows. | ||
| 7683 | - **The module table STAYS**, and is still where structure is written down: | ||
| 7684 | it wires every module and every import, so no table-module import can exist | ||
| 7685 | outside its loop. What changes is that adding an edge is now an ordinary | ||
| 7686 | diff instead of a re-stratification. | ||
| 7687 | - **Kept because they are not module architecture:** name-exists validation | ||
| 7688 | of `imports`/`test_imports` (a stale name is still a compile error); the | ||
| 7689 | `test_imports` twin mechanism, whose subject is SHIPPING — test scaffolding | ||
| 7690 | cannot reach a production instance — not graph shape; the wasm-closure | ||
| 7691 | check, reworded as the mechanical fact it is (a flagged row compiles | ||
| 7692 | against wasm32-freestanding, so an import without a `.wasm` twin cannot | ||
| 7693 | compile there, and failing at the edge beats the twin loop's later | ||
| 7694 | `@panic`); and source bans 4, 5 and 6, which pin BEHAVIOR — no VT authored | ||
| 7695 | outside `src/tui/`, every program exec'd as argv, one `posix.fork` site — | ||
| 7696 | by reading production lines the import graph could never see. The three | ||
| 7697 | keep their numbers, because files across the repo cite them in their own | ||
| 7698 | `folder rule N exemption:` lines. | ||
| 7699 | - **What is unguarded now**: a headless-app import (`client` reaching into | ||
| 7700 | `src/tui/`) is a reviewer's catch rather than a build failure. The stated | ||
| 7701 | invariants in CLAUDE.md are unchanged and `mux_core.wasm` still links | ||
| 7702 | `term` + the client core with no tty anywhere, which is the check that | ||
| 7703 | actually exercises the separation. | ||
src/cli/flags.zig
| Old | New | ||
|---|---|---|---|
| @@ -186,8 +186,8 @@ pub fn version(prog: []const u8, ver: []const u8) u8 { | |||
| 186 | } | 186 | } |
| 187 | 187 | ||
| 188 | fn versionTo(fd: std.posix.fd_t, prog: []const u8, ver: []const u8) u8 { | 188 | fn versionTo(fd: std.posix.fd_t, prog: []const u8, ver: []const u8) u8 { |
| 189 | // Both strings are the caller's: cliflags is layer 0 and cannot see | 189 | // Both strings are the caller's: cliflags imports nothing at all, and |
| 190 | // build_options, where the version it prints lives. | 190 | // that includes build_options, where the version it prints lives. |
| 191 | var buf: [64]u8 = undefined; | 191 | var buf: [64]u8 = undefined; |
| 192 | writeTo(fd, std.fmt.bufPrint(&buf, "{s} {s}\n", .{ prog, ver }) catch unreachable); | 192 | writeTo(fd, std.fmt.bufPrint(&buf, "{s} {s}\n", .{ prog, ver }) catch unreachable); |
| 193 | return 0; | 193 | return 0; |
src/engine/engine.zig
| Old | New | ||
|---|---|---|---|
| @@ -486,7 +486,8 @@ pub const Engine = struct { | |||
| 486 | 486 | ||
| 487 | /// Tracking modes AND report formats: a report in a spelling the | 487 | /// Tracking modes AND report formats: a report in a spelling the |
| 488 | /// application did not ask for arrives as garbage in its input. Field | 488 | /// application did not ask for arrives as garbage in its input. Field |
| 489 | /// names, not `protocol`'s — layer 0 does not know the wire. | 489 | /// names, not `protocol`'s — the engine does not import the wire |
| 490 | /// contract. | ||
| 490 | pub const MouseModes = struct { | 491 | pub const MouseModes = struct { |
| 491 | x10: bool = false, | 492 | x10: bool = false, |
| 492 | normal: bool = false, | 493 | normal: bool = false, |
src/engine/protocol.zig
| Old | New | ||
|---|---|---|---|
| @@ -792,8 +792,8 @@ pub const attach_len = 20; | |||
| 792 | /// fixed part, which is why the tail is a tail and not a versioned field. | 792 | /// fixed part, which is why the tail is a tail and not a versioned field. |
| 793 | /// One pattern, four verbs: attach, debug_dump, status_req, await_req. | 793 | /// One pattern, four verbs: attach, debug_dump, status_req, await_req. |
| 794 | pub const session_name_max = 32; | 794 | pub const session_name_max = 32; |
| 795 | /// The daemon's session cap, spelled here because `protocol` is layer 0 and | 795 | /// The daemon's session cap, spelled here because `protocol` imports no |
| 796 | /// cannot see `Server.max_sessions`. Not a second owner: server.zig asserts | 796 | /// daemon and cannot see `Server.max_sessions`. Not a second owner: server.zig asserts |
| 797 | /// the two are equal at comptime, so a daemon that raised its cap without | 797 | /// the two are equal at comptime, so a daemon that raised its cap without |
| 798 | /// this line does not build. | 798 | /// this line does not build. |
| 799 | pub const sessions_max = 32; | 799 | pub const sessions_max = 32; |
src/server/server.zig
| Old | New | ||
|---|---|---|---|
| @@ -21,7 +21,8 @@ pub const upgrade = @import("upgrade.zig"); | |||
| 21 | const proxy = @import("proxy"); | 21 | const proxy = @import("proxy"); |
| 22 | const TmpDir = @import("testtmp").TmpDir; | 22 | const TmpDir = @import("testtmp").TmpDir; |
| 23 | // The agent relay is a sub-file of this module, not a row of its own: it is | 23 | // The agent relay is a sub-file of this module, not a row of its own: it is |
| 24 | // one cluster of Server's state, and a module row would claim it is a layer. | 24 | // one cluster of Server's state, and a module row would make it a seam |
| 25 | // anything in the build could name. | ||
| 25 | const agent_mod = @import("server_agent.zig"); | 26 | const agent_mod = @import("server_agent.zig"); |
| 26 | const SessionTable = @import("server_sessions.zig").SessionTable; | 27 | const SessionTable = @import("server_sessions.zig").SessionTable; |
| 27 | const AgentRelay = agent_mod.AgentRelay; | 28 | const AgentRelay = agent_mod.AgentRelay; |
src/tui/interact.zig
| Old | New | ||
|---|---|---|---|
| @@ -119,7 +119,7 @@ pub const PrefixFilter = struct { | |||
| 119 | // The banner buffer must hold a whole prompt line — the `": "` and the | 119 | // The banner buffer must hold a whole prompt line — the `": "` and the |
| 120 | // cursor `"_"` on top of the spelling — and a comment cannot fail a | 120 | // cursor `"_"` on top of the spelling — and a comment cannot fail a |
| 121 | // build. Asserted here because the import only runs this way: interact | 121 | // build. Asserted here because the import only runs this way: interact |
| 122 | // is layer 2 and paint is layer 1. | 122 | // imports paint, never the reverse. |
| 123 | comptime { | 123 | comptime { |
| 124 | std.debug.assert(prompt_max + ": ".len + "_".len <= paint_mod.banner_label_max); | 124 | std.debug.assert(prompt_max + ": ".len + "_".len <= paint_mod.banner_label_max); |
| 125 | } | 125 | } |
src/tui/paint.zig
| Old | New | ||
|---|---|---|---|
| @@ -16,8 +16,8 @@ pub const sync_end = "\x1b[?25h\x1b[?2026l"; | |||
| 16 | /// Inclusive grid columns of one row, painted inverted. | 16 | /// Inclusive grid columns of one row, painted inverted. |
| 17 | pub const Span = struct { from: u16, to: u16 }; | 17 | pub const Span = struct { from: u16, to: u16 }; |
| 18 | 18 | ||
| 19 | /// A callback, not a shape: `select.zig` is layer 1 like this module, so | 19 | /// A callback, not a shape: `select.zig` is a sibling child file of the same |
| 20 | /// neither may import the other. | 20 | /// module root, and neither file imports the other. |
| 21 | pub const Highlight = struct { | 21 | pub const Highlight = struct { |
| 22 | ctx: ?*anyopaque = null, | 22 | ctx: ?*anyopaque = null, |
| 23 | span: ?*const fn (?*anyopaque, row: u16, cols: u16) ?Span = null, | 23 | span: ?*const fn (?*anyopaque, row: u16, cols: u16) ?Span = null, |