d22a2aa1
build: folder rule 4 reads production lines, not test bodies
a73x 2026-08-28 20:25
Commit message
CLAUDE.md
| Old | New | ||
|---|---|---|---|
| @@ -53,10 +53,10 @@ engine and a client can link those folders and paint its own way: | |||
| 53 | 53 | ||
| 54 | `build.zig`'s `checkFolderRules` enforces it: engine and client name no tui, | 54 | `build.zig`'s `checkFolderRules` enforces it: engine and client name no tui, |
| 55 | server or cli module; server names no client and no terminal; tui imports | 55 | server or cli module; server names no client and no terminal; tui imports |
| 56 | client and never the reverse; and nothing under `src/engine/` or `src/client/` | 56 | client and never the reverse; and no line outside a `test` block under |
| 57 | spells `termios` or an escape byte without a `// folder rule 4 exemption:` line | 57 | `src/engine/` or `src/client/` spells `termios` or an escape byte without a |
| 58 | saying why. Both known debts are signed in `folder_exemptions` and those | 58 | `// folder rule 4 exemption:` line saying why. The known debts are signed in |
| 59 | markers. | 59 | `folder_exemptions` and those markers. |
| 60 | 60 | ||
| 61 | Layers are enforced in the same module table (grep `.layer =` for the graph). | 61 | Layers are enforced in the same module table (grep `.layer =` for the graph). |
| 62 | 62 | ||
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -450,10 +450,16 @@ fn idx0(name: []const u8) usize { | |||
| 450 | /// import graph cannot catch a module that writes the bytes itself — so this | 450 | /// import graph cannot catch a module that writes the bytes itself — so this |
| 451 | /// reads the sources, the way the doc gate does. | 451 | /// reads the sources, the way the doc gate does. |
| 452 | /// | 452 | /// |
| 453 | /// The escape hatch is a line saying `folder rule 4 exemption:` and why. It | 453 | /// `test` blocks are skipped: driving an engine with VT bytes is how a test |
| 454 | /// is per FILE and deliberately blunt: a file that has to spell VT bytes is a | 454 | /// speaks to a VT, and a fixture that types an escape is not the module owning |
| 455 | /// design fact worth one visible line, not a per-site suppression nobody | 455 | /// a terminal. Line arithmetic rather than a parser, sound for the doc gate's |
| 456 | /// reads. | 456 | /// reason — `zig fmt --check` is already a gate, so a container-level `test` |
| 457 | /// opens at column 0 and its `}` closes there and nowhere else. | ||
| 458 | /// | ||
| 459 | /// The escape hatch for what remains is a line saying `folder rule 4 | ||
| 460 | /// exemption:` and why. It is per FILE and deliberately blunt: a file that has | ||
| 461 | /// to spell VT bytes is a design fact worth one visible line, not a per-site | ||
| 462 | /// suppression nobody reads. | ||
| 457 | fn checkNoTerminalBytes(b: *std.Build) void { | 463 | fn checkNoTerminalBytes(b: *std.Build) void { |
| 458 | const needles = [_][]const u8{ "termios", "\\x1b[", "\\x1b]" }; | 464 | const needles = [_][]const u8{ "termios", "\\x1b[", "\\x1b]" }; |
| 459 | for ([_][]const u8{ "src/engine", "src/client" }) |sub| { | 465 | for ([_][]const u8{ "src/engine", "src/client" }) |sub| { |
| @@ -463,14 +469,31 @@ fn checkNoTerminalBytes(b: *std.Build) void { | |||
| 463 | const src = b.build_root.handle.readFileAlloc(b.allocator, path, 4 << 20) catch |e| | 469 | const src = b.build_root.handle.readFileAlloc(b.allocator, path, 4 << 20) catch |e| |
| 464 | fatal("folder rule 4: cannot read {s} ({s})", .{ path, @errorName(e) }); | 470 | fatal("folder rule 4: cannot read {s} ({s})", .{ path, @errorName(e) }); |
| 465 | if (std.mem.indexOf(u8, src, "folder rule 4 exemption:") != null) continue; | 471 | if (std.mem.indexOf(u8, src, "folder rule 4 exemption:") != null) continue; |
| 466 | for (needles) |n| { | 472 | var in_test = false; |
| 467 | if (std.mem.indexOf(u8, src, n) != null) fatal( | 473 | var lineno: usize = 0; |
| 468 | "folder rule 4 broken: {s} spells `{s}` — driving a terminal " ++ | 474 | var it = std.mem.splitScalar(u8, src, '\n'); |
| 469 | "is src/tui/'s job, and {s} must link into an app that " ++ | 475 | while (it.next()) |line| { |
| 470 | "paints its own way. Move the bytes, or write one line " ++ | 476 | lineno += 1; |
| 471 | "`// folder rule 4 exemption: <why>` in the file", | 477 | if (in_test) { |
| 472 | .{ path, n, sub }, | 478 | if (std.mem.eql(u8, std.mem.trimRight(u8, line, "\r"), "}")) in_test = false; |
| 473 | ); | 479 | continue; |
| 480 | } | ||
| 481 | if (std.mem.startsWith(u8, line, "test ") or | ||
| 482 | std.mem.startsWith(u8, line, "test{")) | ||
| 483 | { | ||
| 484 | in_test = true; | ||
| 485 | continue; | ||
| 486 | } | ||
| 487 | for (needles) |n| { | ||
| 488 | if (std.mem.indexOf(u8, line, n) != null) fatal( | ||
| 489 | "folder rule 4 broken: {s}:{d} spells `{s}` outside a test " ++ | ||
| 490 | "block — driving a terminal is src/tui/'s job, and {s} " ++ | ||
| 491 | "must link into an app that paints its own way. Move the " ++ | ||
| 492 | "bytes, or write one line `// folder rule 4 exemption: " ++ | ||
| 493 | "<why>` in the file", | ||
| 494 | .{ path, lineno, n, sub }, | ||
| 495 | ); | ||
| 496 | } | ||
| 474 | } | 497 | } |
| 475 | } | 498 | } |
| 476 | } | 499 | } |
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -1,5 +1,10 @@ | |||
| 1 | # Decision log | 1 | # Decision log |
| 2 | 2 | ||
| 3 | Paths cited in entries before 2026-08-28 predate the folder move: what these | ||
| 4 | call `src/X.zig` now lives under `src/engine/`, `src/server/`, `src/client/` | ||
| 5 | or `src/tui/`. The entries are the record of what was decided and are left as | ||
| 6 | written. | ||
| 7 | |||
| 3 | ## 2026-08-07 (M1) | 8 | ## 2026-08-07 (M1) |
| 4 | 9 | ||
| 5 | - **Language: Zig.** The engine dependency (ghostty-vt) is a Zig module; a C | 10 | - **Language: Zig.** The engine dependency (ghostty-vt) is a Zig module; a C |
src/engine/delta.zig
| Old | New | ||
|---|---|---|---|
| @@ -7,7 +7,6 @@ | |||
| 7 | //! Engine and protocol are the whole of its world — no daemon, no clients, | 7 | //! Engine and protocol are the whole of its world — no daemon, no clients, |
| 8 | //! no sockets — which is what lets the tracker be driven directly by a test | 8 | //! no sockets — which is what lets the tracker be driven directly by a test |
| 9 | //! holding nothing but an engine. | 9 | //! holding nothing but an engine. |
| 10 | // folder rule 4 exemption: the tests drive the engine with VT bytes; no production line here writes an escape. | ||
| 11 | const std = @import("std"); | 10 | const std = @import("std"); |
| 12 | const Engine = @import("engine").Engine; | 11 | const Engine = @import("engine").Engine; |
| 13 | const proto = @import("protocol"); | 12 | const proto = @import("protocol"); |