a73x

43fb76c4

docs: hygiene-kit doctrine — the table is the law, verdicts print, scopes stated

a73x   2026-08-14 09:43

Commit message
docs: hygiene-kit doctrine — the table is the law, verdicts print, scopes stated

Names the silent-module-loss hazard class where the comptime-error
finding already describes it. Appends the hygiene-kit doctrine to
the 2026-08-14 section: the layer table as computed topological
strata (extract-and-diff proof, 76 edges unchanged), the two
adjudicated cross-layer edges, test-only imports as a declared
column, refAllDecls scope stated honestly (pub-only, plain vs.
recursive variant per module, and why), leak verdicts that print
instead of changing exit codes, the persistence soak phase's fd/RSS
bounds, and `zig build check` as the pre-commit gate.

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

docs/decisions.md
Old New
@@ -2623,7 +2623,8 @@ Seven, each stated as the rule it became.
2623 simply vanishes from the run (227 of 251 tests, and no property 2623 simply vanishes from the run (227 of 251 tests, and no property
2624 named) rather than failing loudly. Length-asserts go first and the 2624 named) rather than failing loudly. Length-asserts go first and the
2625 slices get sized at runtime, so a bad mutation fails as a test rather 2625 slices get sized at runtime, so a bad mutation fails as a test rather
2626 than as an absence. 2626 than as an absence. This hazard class is named "silent-module-loss"
2627 elsewhere in this file and in the module roots' refAllDecls comments.
2627 3. **Flipping a conjunction's operands is not negation.** Shared 2628 3. **Flipping a conjunction's operands is not negation.** Shared
2628 structure absorbs it — two paths on one filesystem share a `dev`, so 2629 structure absorbs it — two paths on one filesystem share a `dev`, so
2629 swapping the operands of the identity check changed nothing 2630 swapping the operands of the identity check changed nothing
@@ -3212,3 +3213,59 @@ two dialects.
3212 `sudo: a password is required` (exit 1): this box grants no non-interactive 3213 `sudo: a password is required` (exit 1): this box grants no non-interactive
3213 sudo, so the run half is pending install. valgrind not installable 3214 sudo, so the run half is pending install. valgrind not installable
3214 non-interactively; run `sudo pacman -S valgrind` then the script. 3215 non-interactively; run `sudo pacman -S valgrind` then the script.
3216 - **The layer table is the law.** build.zig's module graph is declared data:
3217 a table row per module (name, root, frozen stratum, production imports,
3218 test-only imports), the wiring loop derives every grant, and a production
3219 import that does not point strictly downward is a comptime error — as is
3220 a table-row rename (comptime idxOf) and a wasm row importing a non-wasm
3221 row (the wasm set is closed under production imports). Layers are
3222 COMPUTED topological strata, frozen 2026-08-14 — hand-assignment was
3223 tried first and misplaced engine on the first draft. Changing the
3224 architecture now means editing the table, which is the point. Proven by
3225 extract-and-diff: 76 edges before and after, empty diff.
3226 - **Two adjudicated edges.** `server -> replica` is test-only (sole use is
3227 the applyFrame test helper) and lives in the test_imports column.
3228 `client -> proxy` is production (ignoreSigpipe in the live attach path)
3229 and is grandfathered with the debt comment at client.zig — relocating
3230 ignoreSigpipe to a leaf is the recorded fix, deliberately not taken here.
3231 - **Test-only imports are a declared column, not an accident.** The testtmp
3232 pattern (production modules importing test scaffolding used only inside
3233 `test` blocks, kept out of release binaries by lazy compilation) is now
3234 stated per-row. The strata computation excludes the column.
3235 - **refAllDecls in every test-loop module root, scope stated honestly.**
3236 Pub decls only — std.meta.declarations sees nothing private. Narrower
3237 still than the original hazard framing: in 0.15.2 a pub decl with an
3238 unresolvable type fails eagerly once the module is touched at all (the
3239 planned probe proof did not reproduce; two isolated repros confirmed),
3240 so the gap this closes is the module that is imported but never
3241 referenced by name — plus forcing analysis where nothing referenced the
3242 decl. quic/quic_server/quic_client use the plain variant (the recursive
3243 walk would analyze the whole wolfSSL/ngtcp2 cImport namespace); engine
3244 was downgraded to plain because recursion reaches pre-existing comptime
3245 errors inside vendored ghostty-vt (re-promote when the dep is bumped);
3246 wasm_core is excluded because it is not in the native test loop and a
3247 block there would never run.
3248 - **Leak verdicts print, never panic.** All three binaries check
3249 `gpa.deinit()` and print `<binary>: LEAK: allocations outlived deinit`
3250 to stderr on `.leak` — never an exit-code change, because muxd's exit
3251 code carries the session shell's. e2e captures every daemon's stderr and
3252 sweeps all captures at the end, behind a canary that fails if the sweep
3253 has nothing to read; agent.sh checks the detached-daemon log after each
3254 clean stop, post process-exit, and fails loudly if the daemon outlives
3255 the stop (a wedged daemon must not read as leak-free). Proven by
3256 deliberate leak: marker appeared, e2e failed at the sweep, agent failed
3257 all three leakcheck sites. kill -9 paths print nothing — no false
3258 positive, no coverage, stated. Known uncovered captures: $OUT.q and the
3259 other mid-suite rm'd files (recorded debt).
3260 - **The persistence soak phase owns the classes 6a cannot see.** One
3261 daemon, N client lifecycles: every cycle must prove its client attached
3262 (a vacuous cycle fails the phase); fd count must return to baseline
3263 exactly, with one settle resample to absorb the unacked-detach race; RSS
3264 gets a 4MB growth bound past a 3-cycle warmup (allocators retain pages;
3265 equality would flake). The zombie /proc pitfall is guarded (`VmRSS`
3266 absent on a dead child reads as empty, not 0). Failure evidence is swept
3267 into the faildir, never rm'd. First real numbers: fds 5->5 exact, RSS
3268 +108 kB over 8 cycles.
3269 - **`zig build check` is the pre-commit gate**: fmt (including
3270 build.zig.zon) + unit tests, seconds. e2e/agent/soak stay separate
3271 steps — minutes-long and process-spawning.