# Roadmap & status Authoritative version: [`docs/11-roadmap.md`](https://github.com/UnitBuilds-CC/Merkles-World/blob/main/docs/11-roadmap.md). ## Where the project actually is | Gate | State | |------|-------| | Architecture specified | **done** — design level only | | Engine code | **Phase 0 code plus the first behaviour and the first pipeline** — `contracts/`, `crates/coords`, `crates/ledger` (the A-10 hasher and the epoch commit path) and `crates/memory` (slabs). Still no engine runtime: that is Phase 1 | | SPEC conflicts (doc 13) | 9 registered — 4 locked (SPEC-02 → ADR-017; SPEC-01, SPEC-03, SPEC-09 → ADR-029, 2026-10-11), 4 proposals, 1 open (SPEC-07) | | Open questions | 61 tracked (six raised by measurement since 2026-10-12 — OQ-56 to OQ-61, each opened by a number rather than by reading prose; the licensing half of (licensing half of OQ-43 decided 2026-10-11: MIT OR Apache-2.0; OQ-45/46 raised by drafting A-03/A-08; OQ-47/48 by the two sign-offs below). **OQ-44 went live 2026-10-12** when the BLAKE3 pass landed: its bytes-in/digest-out entry point has no type refusing an `f32` | | Blocking artifacts A-01…A-10 | A-01 `contracts/coords.rs` **frozen 2026-10-11**; **A-10 `contracts/tree.rs` frozen 2026-10-12** on [ADR-030](Decision-log) and published vectors; **A-03 `contracts/delta.rs` and A-08 `contracts/tx.rs` drafted 2026-10-11, both DRAFT**; A-09 `contracts/errors.rs` drafted under a DRAFT header; A-02/A-04/A-05/A-06/A-07 unwritten | | Digest + GPU substrate | **ADR-028 (BLAKE3-256) and ADR-026 (`wgpu`) signed off 2026-10-11** — the two proposed ADRs that gated any engine code; they are the workspace's first external dependencies | | Research spikes S1–S10 | **partly run, and only the CPU-able halves**: S1's march-step law, S2 in three parts, S3 across three architectures plus the scalar-hash, WASM-SIMD and x86-codegen axes, S6 wherever a runner has a device, S7's first propagation run, S9's wgpu cost. What has *not* run is anything that needs a GPU attached (the R1/R2 halves) or a ruling first (S5's threshold, S4's upload). | | Validation gates | C1–C4 run in CI (x86-64 Linux / x86-64 Windows / ARM64 macOS + a wasm32 compile) | | Public repo | main published, wiki mirrored, licensed **MIT OR Apache-2.0** | ## Sequencing principle The design is deep and the temptation is to build the coolest layer first. Resist it: three contracts gate almost everything downstream, and one research question can invalidate an entire subsystem. So the order is ``` SPEC/OQ resolution → spikes S1–S10 → contracts/ frozen → P0 harness → P1 core loop (doc 13) (below) (doc 12) (doc 12) (docs 02–09) ``` ## Blocking artifacts Small, reviewable files that must exist *as artifacts* before implementation. Writing them is how the open questions get answered, so they are the only thing writable while questions are still open. | ID | Artifact | Blocked by | |----|----------|-----------| | A-01 | `contracts/coords.rs` — `GlobalVoxelPos`, `RegionKey`, `LocalOffset`, `SlabKey`, `SectorKey`, `AabbI64`, octree key derivation, `region_key()` / `local_offset()` | **frozen 2026-10-11.** Was SPEC-01/03/06 + **SPEC-09**: [ADR-029](Decision-log) locks SPEC-09, pins SPEC-01/03, and reclassifies SPEC-06 as doc 03's allocation choice rather than an addressing constraint | | A-02 | `contracts/ledger-api.wit` — frozen v1 host surface | OQ-34, ADR-013 | | A-03 | delta record layout — the packed `u64`, region-slot table, epoch header | **drafted 2026-10-11** as `contracts/delta.rs` (DRAFT) on [ADR-029](Decision-log)'s `\|16 slot\|36 intra-region\|12 payload\|`; gate P1.1 already runs against it as `p1_1_delta_record_round_trip`. Its freeze waits on **OQ-45** (what the 12 payload bits *mean*) and on **SPEC-06**'s slab capacity, which spike S2 must measure | | A-04 | macro field schema + rule library interfaces | OQ-01, OQ-08 | | A-05 | event tag registry (enumerated, versioned) | OQ-08, gate SM1 / spike S7 | | A-06 | circuit graph IR + stateful node interface | OQ-02 | | A-07 | SDF node grammar schema (`.fbs` + interpreter signature) | ADR-012 | | A-08 | `ActionTx` wire layout | **drafted 2026-10-11** as `contracts/tx.rs` (DRAFT) — doc 10's envelope, split as [ADR-015](Decision-log) splits its wire-delta row: `Target` is the address, `TxPayload` the value, and a slab mutation's address+value *is* an A-03 record. Freeze waits on **OQ-46** (what the ids are, how wide, which scheme signs) and on **OQ-30**/**OQ-23** for the serialized byte format | | A-09 | error taxonomy + determinism violation codes | doc 12 | | A-10 | Merkle tree shape — node kinds, arity, child order, empty rules, canonical byte order, epoch-root composition | **drafted 2026-10-11** as `contracts/tree.rs` on [ADR-030](Decision-log), the half [ADR-028](Decision-log) left open, **executed 2026-10-12** by `crates/ledger`, and **frozen 2026-10-12**. Writing the hasher settled two halves the ADR had left as prose: which `EpochHeader` fields the epoch root hashes (`epoch_index`, `record_count` — the other two roots enter once, from the header), and the false claim that lexicographic sort is the octree's walk order (it is Z-order; the root is position-keyed, so neither order moves it) | ## Spikes (throwaway, timeboxed) | ID | Question | Feeds | |----|----------|-------| | S1 | Can SDF raymarching hold 60 FPS at our content budgets? | doc 04, ADR-026 | | S2 | Is slab-allocator throughput real (10⁶ writes/sec, `p99 < 1 µs`, sub-ms Merkle pass)? | doc 03. **First numbers 2026-10-12, x86-64 Windows:** P1.2 **met** (57.2 M appends/sec, p99 200 ns). P1.3 **not met** — 52 ms for one full 64 KB slab of scattered edits (68,524 digests at 759 ns, of which raw BLAKE3 is 508 ns) against 7 ms for the same count clustered: the pass follows digests computed, so `12N` is a bound and not a cost, and OQ-49 asks which of target / budget / compaction frequency / scope moves. The O(N²) resolution pre-pass (46 ms) is gone — resolution happens at the leaf by `append`, and no published digest changed | | S3 | Are BLAKE3 roots bit-identical across targets (N2600 / RTX 3060 / Linux-Mesa / M1-browser)? | ADR-017/028 | | S4 | FlatBuffers → GPU zero-copy upload actually work? | doc 09 | | S5 | wasmtime + WIT determinism across Rust/TS/Python? | doc 08 | | S6 | Headless WGSL validation harness in CI | doc 12, ADR-003 | | **S7** | Can macro causality actually propagate? | dam in region A dries region B (**gate SM1**) | doc 05 — **the highest-risk spike**. **First numbers 2026-10-12** (`cargo run --release --example s7_propagation -p engine`, CI prints it as information): a 512-region integer river, dam at the source, frontier simulating only the disturbed width. Latency is distance over ring — the mouth dries at commit 501 / 251 / 126 / 64 for ring 1 / 2 / 4 / 8 — and cost against simulating the whole river every commit runs 57% → 95% across those rings, so the ring buys latency with work rather than with an explosion; docs/05's warning needs re-deriving against that curve. The mechanism SM1 depends on held: nothing beyond the frontier is loaded, simulated or written. **SM1's other half, "identically for two clients", measured 2026-10-16** (`cargo run --release --example s7_field_root -p engine`, CI asserts its two field digests): two clients handed the same 600 commits — one simulating all 512 regions, one using the stage-3 frontier — hash their `i32` field with `crates/ledger` and reach **one final root** (`4a99a06a…`, identical at every ring), so skipping regions changes *when* the answer arrives, not the answer. The disagreement is a window, not a corruption: the first commit from which the frontier client stays equal to the end is 512 / 257 / 129 / 66 for ring 1 / 2 / 4 / 8 — distance over ring again, plus a two-commit drain tail. **That window is OQ-01's multi-epoch lag with a number in front of it**: the rule costs peers that many epochs of agreeing-not about a region nothing has disturbed yet; how many are acceptable stays the owner's call. Two caveats because the gate leans on them: the field leaf is spike-local (kind `0xC7`), not A-04's schema, which is still unwritten, so this is not a contract root; and both clients run in one process, so the cross-target evidence is CI asserting the two pinned digests on three architectures, not two machines — P1.5's four targets cover ledger roots, not a macro field | | S8 | Do the jump methods work — closed forms / matrix exponentiation over Δt? | ADR-005, OQ-09 | | S9 | What does `wgpu` cost versus raw WebGPU on our pipeline? | ADR-003, ADR-026, OQ-26 | | **S10** | **What actually invalidates a compiled circuit graph — whole component, or dirty subgraph?** (a real build, 10⁴ nodes, lever flipped, recompiled in measurable time, with the granularity reported) | SPEC-07, OQ-22, doc 06 gates E1/E4 | **S7 is the one that can invalidate the architecture. Budget it first, not last.** S1, S2 and S10 are the three that can invalidate a *cost model*, and every "instantaneous" claim in docs 03–06 is a cost model. ## Phases - **Phase 0 — Foundations:** workspace, `contracts/` from A-01…A-09, CI (`cargo test`, `clippy -D warnings`, `fmt --check`, `cargo deny`, the S6 headless shader check), coordinate property tests (gates C1/C2/C3) passing on the three **architectures** C2 names — x86-64, ARM64, WASM — which is a smaller set than the four **targets** of the determinism suite below, and the camera-relative f32 demo: no precision jitter at `x = 10⁹`. That demo **is** gate C4's run — camera-space error ≤ 1 f32 ulp at the declared render radius (doc 02 shows "< 1e-4" is unreachable at 4096, so radius and epsilon are declared together, not assumed) — plus a working hot-reload loop. **Exit:** the P0 pipeline works on a real change, and two agents could implement against the frozen contracts without talking. - **Phase 1 — Engine runtime (domain-agnostic):** ledger & slabs (P1.1–P1.6), renderer (R1–R5), macro sim (SM1–SM6, needs S7 ✅), circuits (E1–E6, needs OQ-02 ✅), physics/audio/animation (PH1–DE1), WASM host (W1–W5), asset compiler (AS1–AS5). **Exit / pitch demo:** a booting engine, a rendered procedural scene, an edit that persists, and one deterministic hash matching across all four determinism targets (gate P1.5). - **Phase 1.5 — Multiplayer** (deferred, ADR-004): `ActionTx` broadcast, prediction/rollback, epoch root exchange, bisection repair, one topology chosen (M1–M7). Not before every Phase 1 gate is green. **Naming hazard:** "Phase 1.5" (this multiplayer stage) and "gate P1.5" (the determinism gate in doc 03, run during Phase 1) collide by accident; neither id is renumbered, so prose should say "the P1.5 determinism gate" and "the multiplayer phase". - **Phase 2 — The game ("Project Genesis"):** content only, through the public API — the official game is Mod #1. Vertical slice (one biome, one mob, one tool, one craft, one weather effect) → content expansion → ecosystem & events → low-end/browser optimization and prompt-to-mod packaging. ### Phase 0 status — 2026-10-12 Measured locally (x86-64 Windows) and on CI runs `38124937828`, `38150859744`, `38159161144`, `38179998839` and `38181630859` (x86-64 Linux, x86-64 Windows, ARM64 macOS), all at `rustc 1.92.0`. Six jobs green on each: three gate jobs, the `wasm32-unknown-unknown` build, `cargo-deny`, docs integrity. `38150859744` covers the ADR-029 sign-off, A-01's freeze and the licence; `38159161144` has A-03 and A-08; `38179998839` has A-10's contract; `38181630859` is the current `main`, with `crates/ledger` computing roots for the first time. | Evidence | Result | |----------|--------| | `cargo fmt --check`, `clippy --all-targets -D warnings` | clean | | `cargo test` | `4 passed` (A-09 taxonomy) + `10 passed` (A-03 delta record: P1.1 round trip, ADR-029's field order, the slot table's first-touch order and its 65,536 ceiling, the doc↔contract mirror) + `7 passed` (A-08 `ActionTx`: wire tags, channel per kind, the A-03 seam, `SequenceId`'s total order, the doc 10 mirror) + `8 passed` (A-10 shape: node kinds, the eight octant orderings, the derived empty ladder, the list's odd item, the epoch root's field order, the doc 03 mirror) + `11 passed` (C1–C4 gates, SPEC-01 masking, contract↔spec mirror) + `11 passed` (`crates/ledger/tests/root_vectors.rs`: the 15 published digests, the 13 empty levels pairwise distinct, child order hand-built against octant 1 rather than 2 or 4, position-keyed roots, the payload mask, the slot-reassignment hazard, and `node_parts` agreeing with the contract's reference `preimage`) + `5 passed` (`crates/ledger/tests/commit_path.rs`: the pipeline reproducing `SLOT_TABLE`/`REGIONS`/`EVENTS_ONE`/`EPOCH_0` from raw records, last-write-wins and its order sensitivity, absent-row vs empty-row, short scratch refusing) + `7 passed` (`crates/ledger/tests/bisection.rs`: gate P1.6's bisection walk) + `5 passed` (`crates/ledger/tests/read_path.rs`: the read side reproducing the root the commit built, an untouched voxel reading absent rather than zero) + `7 passed` (`crates/ledger/tests/bisection.rs`: gate P1.6's walk, the named voxel is the one that changed, an absent side reads None, ranked duplicates are order-independent and unranked ones are not) + `3 passed` (`contracts/tests/sdf_grammar.rs`: artifact A-07's kind table and the 16-byte record round-trip) + `5 passed` (`crates/memory/tests/slab.rs`: one store per append, refusal at capacity, `O(1)` reset, both candidate budgets as arithmetic, a 1 MB slab filled and recycled) = **118 passed, the same twenty non-empty suites on all three runners** (run `38249012218`, seven jobs green). The tests since the 104 note are `crates/assetc/tests/asset_round_trip.rs` (6: the reader walking bytes `flatc` packed, a writer-reader round trip, and seven untrustworthy buffers refused - short, wrong identifier, root past the end, a vtable that does not fit below its table, a vtable from a wider schema, a container declaring grammar version 2, and a header declaring a depth past `MAX_TREE_DEPTH`, both of the last two refused before a node is parsed), one more case in `crates/engine/tests/boot_is_reproducible.rs` (booting from an asset file on disk), and `crates/render/tests/raster.rs` (7: the CPU raster probe, coverage pinned against the analytic projection rather than a millisecond). SPEC-01's *masked, never clamped* rule was checked by injecting a clamp into `DeltaRecord::new` and watching the roll test fail with `x: 4095` against `x: 0`; the vectors were checked by reversing the child iteration in `octant_branch`, which moved `REGION_0` from `97627e32…` to `d748c899…` and failed two tests — while `child_order_is_x_low_bit_at_every_level` **stayed green**, because it re-derives through the builder under test. Only a pinned literal catches a shape break | | Gate C2 | the six `u64` lattice digests print **bit-identical on x86-64 and ARM64** (`0xa4659597a9d8aeca` … `0x8e6da94e370cbc70`); pinned in `crates/coords/tests/gates.rs` | | A-10 root vectors | `cargo run --release --example root_vectors -p ledger` prints the fifteen digests of doc 03 § The published vectors, and all three runners print them **byte-identically**, `EPOCH_0 91b97a52fc5137b50ec64550a94220daab0ffe05575bac6dbfda825a9947cc61` included — the first epoch root three machines have agreed on here. Not yet gate P1.5: spike S3 names four particular devices, and the browser half needs a WASM runner (OQ-41), not merely a compile | | Gate C4 | `max \|f32 − exact\| = 0` at radii 512 / 2048 / 4096 on every runner; casting globals first collapses 4032 of 4096 positions at `10⁹` | | `no_std` / WASM | `cargo build --target wasm32-unknown-unknown -p contracts -p coords -p ledger` finishes: the contract guard still bites, and the **hasher** compiles for the browser target, which is what makes P1.5's M1-browser row possible rather than aspirational | | `cargo deny check all -s` | `advisories 0 / bans 0 / licenses 0 errors, 6 warnings, 12 notes / sources 0`. The graph is **no longer empty**: BLAKE3 arrived with `crates/ledger` plus the five crates its build needs (`cc`, `cfg-if`, `arrayvec`, `constant_time_eq`, `cpufeatures`), all inside `deny.toml`'s allow list, so ADR-028's "first external dependency" is now cargo-deny's first real audit. The 6 warnings are still-unused allowances; `unused-allowed-license` flipped from `allow` to `warn` in that same commit, as the file instructed | | Doc integrity | `18 files, 341 local links, 61 OQ + 9 SPEC + 31 ADR + 10 artifacts + 10 spikes` → no findings. Getting here meant fixing the checker: it opened a quoted region on every apostrophe, so the lifetime ticks in A-03's mirrored block made it swallow real braces and report a phantom imbalance on a block that is character-for-character its contract | **In the tree:** A-01 `contracts/coords.rs` — **frozen 2026-10-11**; its DRAFT header came off when [ADR-029](Decision-log) was signed off and locked [SPEC-09](Open-questions). A-10 `contracts/tree.rs` — **drafted 2026-10-11, frozen 2026-10-12** on [ADR-030](Decision-log), and the first artifact frozen against computed digests rather than against prose. A-03 `contracts/delta.rs` and A-08 `contracts/tx.rs` — **drafted 2026-10-11, both DRAFT**: ADR-029 gave them a byte layout to be written against, so they are the record and the envelope, mirrored verbatim into docs 03 and 10 and pinned against drift by `delta_contract_matches_spec_document` and `tx_contract_matches_spec_document`. Neither freezes while **OQ-45** and **OQ-46** are open: A-03's payload field is opaque and A-08's identity widths are labelled drafts on purpose. A-09 `contracts/errors.rs` — still **DRAFT**: it waits on the A-02/A-05/A-07 payloads and on A-08 freezing (A-08 now *exists*, so what it owes A-09 is a decision, not a file), and now that A-01 is frozen, adopting A-01's newtypes for its coordinate payloads is a scheduled migration rather than a blocked one. And `crates/ledger` — the workspace's **first crate with behaviour**: `node_parts` is the only place a preimage is built, `EmptyLadder` derives the thirteen absent-subtree digests, and it is `no_std` on purpose so the same code can root an epoch in a browser. **Still unmet (rest of the Phase 0 exit):** the S6 headless shader check has no CI job (no WGSL exists to compile, [OQ-41](Open-questions)); the dev-profile loop is now two commands that work — `assetc shape.ron shape.bin` then `engine shape.bin` — and one that does not, a watch mode that reruns the first and hot-swaps the second; WASM is compiled but never executed, so C2 covers x86-64 + ARM64 execution and WASM compilation only; **A-04/A-05/A-06 have no file at all** and of the seven that do only A-01 and A-10 are frozen — A-02 (`ledger-api.wit`) and A-07 (`sdf.rs` + `sdf.fbs`) are written and machine-checked but stay DRAFT, as do A-03, A-08 and A-09. The **commit path landed the same day** (`crates/memory` appends records into slabs; `crates/ledger::commit` resolves last-write-wins and roots them, and its test reproduces `EPOCH_0` from raw `DeltaRecord`s), and spike S2 measured it: **P1.2 met** at 57.2 M appends/sec with p99 200 ns; **P1.3 not met** at 4.9 ms for the pass over one full 64 KB slab region and 93 ms for the whole commit, because the dedupe is still O(n²) and the `12N` model itself puts the hashing floor near 5 ms. The first implementation's O(N²) walk-order insertion sort was replaced by an in-place octant partition, the pass became linear (10³ → 907 µs, 10⁴ → 5.8 ms, 10⁵ → 52 ms), and **no digest changed** — the position-keyed claim doing work in measurements instead of prose. Whether the gate's target, the slab budget or the gate's scope moves is **OQ-49**; OQ-47's frame-0 visibility still has no code, because nothing projects a delta into `M_active` yet. 49 validation gates are defined across the subsystem docs, in families **C** (02), **P1** (03), **R** (04), **SM** (05), **E** (06), **PH/AU/AN/DE** (07), **W** (08), **AS** (09) and **M** (10); a milestone is met when its gates pass. ## The Bitwise Determinism Suite The name AGENTS.md §4 and ADR-017 cite as the acceptance test for anything another machine must see. It is not a new test framework — it groups gates that already exist, and this table adds no new gate: | Part | What it proves | Where it runs | |------|----------------|---------------| | C2 | identical hashes for the same `(seed, pos)` on x86-64 / ARM64 / WASM | doc 02, Phase 0 | | C3 | no precision jitter at `x = 10⁹` (Far Lands regression) | doc 02, Phase 0 | | C4 | camera-space error ≤ 1 f32 ulp at the declared render radius | doc 02, Phase 0 demo | | P1.5 | byte-identical epoch roots for one seed + transaction sequence on **N2600 / RTX 3060 / Linux-Mesa / M1-browser** | doc 03, produced by spike S3 | | W1–W3 | Rust / TypeScript / Python mods over the same WIT surface agree bit-for-bit | doc 08, produced by spike S5 | **Explicitly not in the suite:** gate R4 (frame hash across `wgpu` backends) is *informational* — a renderer output is not state, and promoting it to a gate would silently re-adopt the GPU-authoritative model ADR-017 rejects (doc 04). A frame hash is worth pasting as a diagnostic, never as proof of correctness. A feature that produces a value other machines must see is not done until its row here passes. ## Timeline honesty Every number the source conversation produced is an **unvalidated LLM estimate**, kept only because a plan needs a shape. Proof of concept "1 month", playable core "3–6 months / 1.5–2 accelerated", Minecraft-level diversity "9–15 months / 4–5 accelerated". The accelerations (reuse an existing runtime, WGSL headless auto-testing, agent orchestration discipline, noise-tree content bootstrapping, single-player-first) are **risk removals, not time savings** — removing a risk you were about to hit is not the same as finding weeks you didn't spend. **Planning position: commit to the gates, not the dates.** Nobody declares a milestone met by elapsed time; the first honest schedule comes out of P0/P1 gate data. Expect the accelerated numbers to be optimistic by a lot — three subsystems are novel (macro causality, stateful compiled-graph circuits, SDF frame budget), each pillar has one unproven claim, and agent-written GPU code is a debugging situation none of the estimates priced in. ## What we will NOT do - Start content work before the lighting/shading contract exists (OQ-10). - Add multiplayer before Phase 1 is green. - Re-open locked ADRs in code — re-open them in the decision log first. - Let a benchmark target be "whatever the current code happens to hit". ## Gate P1.6 has its walk `crates/ledger::repair::first_divergence` (landed 2026-10-12) descends a region root comparing both sides' child digests octant by octant until it reaches the one voxel that differs, and reports `(x, y, z)` plus each side's payload — `None` on a side that holds no record, because absence is the ordinary desync and a `0` would read as agreement with unedited terrain. Detection is ADR-023's root exchange; *choosing* a winner is OQ-46, deferred by ADR-004, so a hit today means a replay was not deterministic rather than asking for a merge. Tests also pin the sharper lesson: two records for one voxel resolve by `append`, so ranked duplicates are order-independent and unranked ones are not — a caller bug, measured instead of asserted. ## Artifact A-07 and gate P1.7 A-07 (`contracts/sdf.rs`, DRAFT, ADR-031 proposed) is the shape/material node grammar ADR-012 said must be a public surface: 15 kinds with stable numbering, arity fixed by kind table rather than by the file, a 16-byte four-word node record read from one WGSL storage buffer, pre-order with `subtree_len` so a sibling is arithmetic. Its drafting lost bits on write in the first layout (flags ate the top byte of a parameter count) - a test caught it, review did not - and that case is now pinned. Gate P1.7 was named in doc 03 for the half of OQ-47 that is testable today: a delta committed into epoch e is what the ledger reads back for that voxel, verified by rebuilding the read side and hashing it to the root the commit produced. The visibility half - an edit reaching M_active and drawn on the frame it was made - stays open with the renderer. What is still not done, plainly, as of this section (the later sections below move two of these): there is no renderer — a CPU raster now, still no GPU — no macro simulation, no compaction, and gate P1.5 had agreed on one machine rather than its four; it reports three architectures today, and still none of the named devices. Spike S2 measured P1.2 met (57.2 M appends/sec, p99 200 ns) and P1.3 not met (52 ms per full scattered slab against a sub-ms target, ~35 ms of it raw BLAKE3), and which of target / budget / compaction frequency / scope moves is OQ-49, a decision rather than a measurement. Gate P1.4 had no code at all then; it has a directory, tests and a measured cost now, and the section at the end of this page says what that cost implies. ## crates/render: the grammar, evaluated on the CPU Landed 2026-10-12 (`9f68a4a`, CI `38230129783` green on all six jobs). `validate`, `sdf_eval`, `march` and `normal` over the eleven kinds the evaluator implements, refusing Twist/Bend/Noise rather than guessing a distance for them. Two of doc 04's claims became measurements: - **The step guarantee** — a ray from six units out hits the unit sphere at `t = 5` in at most two steps, the hit lies on the surface, and a ray aimed away ends on the step budget. - **R5, as far as it can be tested without a GPU** — a scene and its millionth-scale copy, marched with proportionally scaled epsilons, need nearly the same steps and agree on the hit to under 1% relative. The attempt also found its own bound: the `f32` ulp near 1e-6 is about 1e-12, so an absolute epsilon finer than the representation can never be satisfied. That is what docs/02's camera-relative coordinates are for, not a knob to widen until the test goes green. R1 (1080p/60, 500 culled entities) and R2 (ten minutes of continuous editing) stay unmeasured: CPU steps per ray are not GPU nanoseconds per frame, and there is still no WGSL interpreter, no BVH and no `assetc`. The renderer workstream has a reference to agree with now; it does not yet have a renderer. ## A-07 is whole: grammar and envelope `contracts/sdf.rs` (kinds, fixed arity, the 16-byte record, pre-order with `subtree_len`) and now `contracts/sdf.fbs` (`2bd506a`), the compiled envelope that carries a tree: `ShapeAsset` with `grammar_version`, depth, a vector of fixed-layout `NodeRecord` structs and the `f32` pool, `root_type`, identifier `MLWS`. Nodes are FlatBuffers *structs* rather than tables precisely so the asset bytes are the GPU buffer - no repacking, no vtable on a per-pixel path. Checked with the real compiler: `flatc 24.3.25 --rust` emits `pub struct NodeRecord(pub [u8; 16])`, so the stride promise comes from a code generator rather than a comment, and a probe asset packed to 156 bytes. flatc is not a CI step yet; what CI enforces is that the two files still describe one layout (`contracts/tests/sdf_schema.rs`). What this does not unlock: `assetc` still does not exist, so nothing emits the format - AS1-AS5 stay open, and the distance from an authored tree to a drawn scene is still the WGSL interpreter, spike S6 and the export-lint decision (task #14) that gates it. ## The engine boots, and the gate table says what is met `cargo run -p engine` (2026-10-12, `7ff3200`/`9f68a4a` era) is the workspace's first binary. One process: three edits into a 64 KB slab, committed into an epoch root, read back through the ledger's read path, a four-node scene marched, and the slab reset in O(1). It asserts docs/03's published literals as it goes - `EPOCH_0 91b97a52…`, the region list, the slot table, the region root - which is why CI runs it on all three architectures as a check rather than a smoke test: a break anywhere in slab to commit to root to read surfaces as a mismatched digest. `crates/assetc` stage one lowers an authored `Shape` into those same A-07 records, deriving `subtree_len` instead of asserting it and refusing a bad axis, a NaN parameter, and a tree past the draft depth cap. It writes no bytes and reads no files yet, so AS1-AS5 stay open. docs/11 gained a gate-by-gate Phase 1 exit table with the evidence cited per row. Its summary, kept deliberately unflattering: **two of the demo's four clauses are met outright, one is met as pixels and unmet as a frame that meets a budget, and the fourth is met as amended** - the boot draws a procedural SDF scene that arrives as compiled content on a GPU and from a `.bin` on disk, a voxel edit persists through the ledger across a measured ten-minute span, and the Bitwise Determinism Suite targets agree on one epoch root as the amended register lists them. Spike S7's consensus half landed with it (`crates/engine/examples/s7_field_root.rs`): two clients handed the same 600 commits - one simulating all 512 regions, one using docs/05's stage-3 frontier - hash their integer field with `crates/ledger` and reach **one final field root** (`4a99a06a...`) at every ring, so skipping regions changes when the answer arrives, not the answer; and the first commit from which they stay equal is 512 / 257 / 129 / 66 for ring 1 / 2 / 4 / 8, which is OQ-01's multi-epoch lag with a measured cost in front of it. Two caveats, because gate SM1 leans on them: the field leaf is spike-local (kind `0xC7`) and A-04's schema is still unwritten, so this is not a contract root; and both clients run in one process, so the cross-target evidence is CI asserting the two pinned digests on three architectures, not two machines. What is *not* any of this: the four clauses say nothing about a window, a display, or a frame that meets a budget, and the demo has never been shown on one. ## The export ban is now an architectural question, not a style rule `unsafe_code = "forbid"` rejects a hand-written `#[no_mangle]`, which is AGENTS.md §5 working as intended. Building the `wit-bindgen` path anyway showed the generated code is rejected too - `export_name`, an `unsafe fn`, and an `unsafe block` all arrive from the macro. So gate P1.5's browser target and spike S6 wait on a policy decision (a scoped lint exception, an out-of-workspace ABI crate, or host-owned exports), not on missing effort. Three decisions now sit with the human: that one, OQ-49 (what gate P1.3 means now it is measured at 52 ms against a sub-ms target), and ADR-031's sign-off freezing A-07. ## The boot is a check, and the transforms landed Two follow-ups from 2026-10-12, both CI-verified (`38236434430`, `38237235123`): - **`crates/engine/tests/boot_is_reproducible.rs`.** The engine's output is evidence, so it is now enforced rather than observed: two spawns produce byte-identical stdout, the digests it prints are the ones doc 03 publishes, and an all-zero digest is refused outright. A ledger change that silently moves a root now fails from outside the crates that produce it, even if every in-crate test is updated to match the new number - which is the point of checking published values rather than internal ones. - **`crates/render` evaluates thirteen of A-07's fifteen kinds**, adding `Twist` and `Bend`. `Noise` stays refused: it wants the integer lattice hash and OQ-45's payload meaning. Two details are the substance rather than the code - `perpendicular(axis)`'s *ordering* is contract (a WGSL twin that picked the other pair would evaluate a different linear map and disagree node-deep), and `Bend` is documented as a shear with a chosen sign because the grammar fixes none. One test is the guard for the first: a sphere centred on the twist axis cannot tell how far it turned, and swapping the pair stops that being true. Honest about the two things these do not do: the reproducibility test can only spawn its own build profile, so the debug-versus-release equality stays a hand-measured note on gate P1.5's row; and an evaluated scene with thirteen kinds is still not a drawn one - no WGSL, no BVH, no culling, so R1 and R2 remain unmeasured and gate P1.5 still reports three architectures rather than its four devices. One axis of the fourth device was reachable from CI, and is now checked: `blake3` picks its implementation at runtime from CPU features, and one of P1.5's four targets is a **Celeron N2600** — an SSE2-era chip that will never execute the AVX2 path. CI therefore runs the published A-10 root vectors again under blake3's `pure` (scalar) build, on all three architectures: the fifteen digests are unchanged, so an ISA-dependent hash is no longer a failure class S3 has to discover on the machine. Stated precisely, because it is a proxy and not the device: this removes "we hope SIMD and portable agree" from the list of things the N2600 run could still reveal; it does not run on the N2600, and the gate stays at three architectures until that hardware reports. ## The asset pipeline writes a file now `crates/assetc` lowers an authored `Shape` into A-07's records (earlier note) and now serialises them: `writer::to_bytes` produces a `ShapeAsset`, and CI hands those bytes to the pinned `flatc`, whose own parser reports depth 1, the node's packed words, four parameters and the label (`83d0d70`, run `38239484945`). The verdict on the format therefore comes from code that did not write it. How it got there is the instructive part. The first draft flatc refused, which exposed three rules no local reader would have taught: the vtable sits *below* the table (soffset subtracted, not added), a field equal to its schema default is omitted rather than written, and the `MLWS` identifier follows the root offset. The second draft made the reference parser **segfault** - a vtable advertising a field the table never contained - which is why `tests/written_asset.rs` now verifies the arithmetic a reader resolves (root to table to vtable, advertised slots inside the declared object size, vector lengths against the buffer, the string's NUL terminator) instead of trusting a reader written by the same hand. That test then caught a third bug: an object size that left out the leading soffset. The last fix was a one-field off-by-one, a table's uoffset naming the vector header rather than its elements. Still not claimed: nothing reads a file from disk. The authoring format is doc 09's RON story and the content schema is OQ-10, so AS1 and AS3-AS5 stay open. And the writer stays hand-built on purpose - the `flatbuffers` crate's vector-of-struct support needs unsafe traits, which the workspace forbids, so generated code would require the same scoped-allow decision as task 14's bindings. ## assetc is a command, and one licence allowance was made in the open `assetc ` now compiles an authored shape file into the compiled blob the runtime will load (`4260879`, green on `1449750` / run `38241246999`, all 7 jobs). RON parsing lives only in this tool - AGENTS.md section 3 bans string parsing on the frame path and exempts tools - so the "assets are pre-compiled, nothing parses at runtime" claim finally has a place where it is true. CI compiles the fixture through the CLI and feeds the result to `flatc`, whose parser reads it back: the format is vouched for outside the repo that writes it. **A decision worth recording rather than burying.** `cargo-deny` rejected `unicode-ident 1.0.27`, whose licence is `(MIT OR Apache-2.0) AND Unicode-3.0` - under an `AND`, every conjunct has to be on the allow list, which carried `Unicode-DFS-2016` and not `Unicode-3.0`. Pinning was not available: `wit-parser` (which gives A-02 its parse check) and `serde_derive` (which gives assetc its RON input) both require the newer version. The options were widen the allow list, drop the RON path, or drop the WIT parse check; the list was widened, on the reasoning that a character-database licence is not the copyleft category the deny list exists to catch, **flagged in `deny.toml` against OQ-43 as needing ratification**, with a one-line revert if that is not the agreed call. This is licensing policy, so it is the user's decision - the agent's part was to make it visible instead of either leaving trunk red or changing policy quietly. One CI lesson from the same commit, because it generalises: the `flatc` step failed on a stale label assertion of mine while the file itself read back perfectly. When an assertion and an artefact disagree, check which one is lying before believing the failure. ## The frame cost is steps x nodes, and a missing convention blocks "an edit you can see" `raster_probe` now grows only the tree on a fixed 96x72 frame, which answers the question a content budget has to ask: what does one more node cost? Almost nothing to the march itself - 659,801 steps for one node against 627,890 for 31, a 5% move - and linearly to the work inside each step, about 1.5 ns per extra node per step on this machine. So the budget law is `steps x nodes`, the two bindable levers are the step budget and the node count, and `MAX_TREE_DEPTH` is load-bearing rather than tidy. The nanoseconds are the noisy half of that table: two runs of one binary on one laptop put the one-node grid at 1.9 ms and 3.5 ms while the step counts repeated exactly. Then the sweep went into CI, and the two halves separated cleanly: run `38254332031` prints **659,801 steps** for the one-node tree and **627,890** for the 31-node one on x86-64 Linux, x86-64 Windows and ARM64 macOS identically - digit for digit on three machines, because a march step belongs to the field along the ray rather than to the tree or the hardware - while the timing half varied as timings do, `ns per node-step` falling to 0.43-0.78 at 31 nodes against 1.58-1.62 at one, depending the runner. Hence doc 04's rule rather than preference: **bind node-steps, not milliseconds.** A budget in nanoseconds would be a claim about one machine, in a project that promises several. Trying to take the next step - making a persisted edit show up in the picture - found a gap instead of a task. Projecting an edited voxel into an A-07 cube needs to know which world span `GlobalVoxelPos` names: the unit cube `[x, x+1)`, or a cube centred on the integer. Doc 02 says a voxel is 1 unit and gate C1 pins `region*4096 + offset == pos`; neither says where the integer sits inside its own cube, and no other doc does. Nothing hashed depends on the answer, so it is not a determinism question - but the raster path, doc 07's collision and the eventual base terrain all have to agree, which is exactly what ADR-021's single coordinate authority exists to prevent. Inventing it in whichever subsystem asks first is the escalation case, so it is now **OQ-50**, flagged in doc 02's own unit table, and gate P1.7's row in the exit table names it as the reason the frame-visibility half has no code. ## Spike S2, part three: where the sub-millisecond line actually falls OQ-49 lists four things that could move for gate P1.3 - target, slab budget, compaction frequency, scope - and two of them were assertions rather than measurements. `cargo run --release --example pass_budget -p ledger` measures them (digests counted by `region_root_counted`, release profile, x86-64 Windows): | Edits in one region | Clustered digests / time | Scattered digests / time | |---|---|---| | 1 | 13 / 12 us | 13 / 10 us | | 8 | 24 / 14 us | 91 / 63 us | | 32 | 70 / 35 us | 354 / 249 us | | 128 | 260 / 124 us | 1,321 / 895 us | | 512 | 1,026 / 496 us | 4,938 / 3.383 ms | | 2,048 | 4,096 / 1.985 ms | 18,489 / 12.861 ms | | 8,192 (a full 64 KB slab) | 12,287 / 5.023 ms | 68,524 / 48.589 ms | **The 1 ms line sits between 128 and 512 edits per region per epoch, and the arrangement decides which side of it you are on.** Per digest the cost holds at roughly 408-938 ns, consistent with BLAKE3 being the floor rather than this code. Two findings contradict what the docs had been asserting. Spreading the same 65,536 edits over eight regions computes **548,192 digests** against **483,285** kept in one region - 13.4% *more* work, because separate trees cannot share interior nodes - so "batch the hash pass across regions" is a parallelism idea, not a work-reduction one. And committing 512 scattered edits with 1, 2, 8 or 16 slot-table rows costs 4.10-4.43 ms in every case, so the number of live regions per epoch is not the cost driver, against the instinct that more regions means more overhead. Which lever moves is still the owner's call - see [OQ-49](Open-questions). What changed is that the choice can now be made against numbers, and that CI prints the sweep on every runner as information: run `38251942742` repeats both digest totals exactly on x86-64 Linux, x86-64 Windows and ARM64 macOS, while its timings disagree about which arrangement is faster (spread measured 15.0% and 14.5% slower on the two Linux-class runners and 10.1% *faster* on ARM64 macOS). Counts are portable; a shared runner cannot rank two workloads 10% apart. That is also why the full-slab scattered count - 68,524 digests - matters: a second, independently written example reproduces the published figure exactly, after this file's first draft had a too-regular position generator reporting 12,287 digests for the same edit count, i.e. a faster pass over a different arrangement. ## The runtime reads the file: a reader, an oracle fixture, and an engine that boots from disk Three commits, one direction reversed. `assetc::reader::from_bytes` (`b4d4128`) was written against the opposite standard from the writer: it had to parse bytes this repository did not produce. `crates/assetc/tests/fixtures/probe_shape.flatc.bin` is the pinned `flatc 24.3.25`'s own output for the same JSON the CI job packs, committed because a test cannot run a compiler it does not have. That makes it the one place AGENTS.md section 8's ban on compiled `.bin` files is knowingly excepted, and the exception is the point - an oracle fixture is not a build artifact, and a reader validated only against its own writer can share a mistake while one fed a foreign implementation's bytes cannot. The fixture earned its keep before it was committed: the first draft read the `MLWS` identifier as the table's soffset, because a uoffset resolves relative to **its own** position and the root offset sits at byte 0. Field presence is read from the vtable rather than by position for the same reason a writer omits defaults - `grammar_version` equals its schema default in this asset and is therefore absent from the table entirely, so positional reading would hand back whatever bytes happened to follow. Five ways to corrupt a buffer are refused rather than guessed: too short, wrong identifier, a root offset past the end, a vtable that does not fit below its table, and a vtable advertising more slots than this schema has. The last refusal needed a hand-built buffer, since our writer places its vtable immediately before its table and can never reach six slots without tripping the bounds check first - a test that would have silently exercised a different branch than it claimed. Then the seam, in two steps. `crates/assetc/tests/cli.rs` (`b4d4128`) stopped asserting arithmetic about the CLI's output and started loading it: RON file in, `.bin` on disk, `reader::from_bytes`, `render::validate`, and a ray that marches the loaded unit sphere to `t = 3.0`. `engine ` (`19ff900`) made the same path the boot rather than the test: read the file, load it, validate it, march it. Pointed at the 97-byte asset the compiler wrote from `sphere.ron`, the boot prints `97 bytes read from target/sphere.bin (1 nodes, 4 parameters, depth 1, grammar version 1, stamped "sphere.ron")` and marches to `t = 3.1964` - the surface computed outside the code under test. With no argument it still compiles its own five-node scene into a 204-byte asset and loads those bytes, so the default demo stays self-contained, and debug and release print the same twelve lines. (That count is the era, not the present: the raster grid made it thirteen and gate P1.4's residency round trip made it fourteen, both described further down this page, and the equality between profiles has held at every step.) Two habits worth naming, because both caught something the other check missed. The disk-boot assertion was falsified before it was trusted: writing `r = 2.0` into the asset moved the boot's answer to `2.1964` and the test failed with the expected meeting point in its message. And `cargo clippy --all-targets -D warnings` failed where `cargo test` passed, on `skip(1).next()` - the gate list in AGENTS.md exists because the checks see different things, not because they are redundant. Cross-architecture evidence arrived with it, free: runs `38243292729`, `38243836093` and `38244520595` are green on all seven jobs, and the last prints the same nineteen non-empty suite totals - 109 - on x86-64 Linux, x86-64 Windows and ARM64 macOS. That is the first time one asset buffer, packed by `flatc` on Linux, has been shown to parse into identical nodes, pool and label on three machines. Two promises the loader was not keeping, found by reading doc 09 and the schema against the code they describe. `grammar_version` was carried, written and parsed, but never *compared* - the one thing the field exists for; and `contracts/sdf.fbs` states that `depth` is carried "so the loader can refuse before it walks anything", which nothing did. `40dab12` and `33fee95` make both real: `from_bytes` compares the version and the declared depth against `MAX_TREE_DEPTH` before reading a single node, because the version decides what every later byte means and the depth decides whether the marcher can budget the tree at all. The version comparison is exact equality, not a window: the accepted range and migration strategy are OQ-30's open question, and A-09's `SchemaVersionUnsupported` is where a policy belongs, so the loader states the boundary rather than inventing one. And the depth check is explicitly **not** gate AS5 - that gate is `assetc` rejecting an over-budget tree at compile time, which needs a per-tree budget the docs do not define yet (doc 04's five frame numbers are provisional, and none of them is per-asset). A harness note in the same spirit: run `38245836230` reddened trunk on one job, `cargo-deny`, and only because the pinned binary's download returned `504` from a CDN edge - the other six were green and the commit had nothing to do with dependencies. Both pinned downloads now retry three times (`67019d2`), which retires the transient while leaving the gate intact: an unfetchable pin still fails the step, no `continue-on-error` anywhere, and the sha256 check on `cargo-deny` still runs before it is used. A red trunk that is really a network event is how a later genuine finding gets dismissed as another flake. Still open, and now honestly listed as open rather than as missing: a GPU upload of the loaded buffer, a watch or hot-reload mode, a versioned authoring schema (OQ-10), and compile-time rejection of an over-budget SDF, so **AS1-AS5 stay open**; A-02's binding is still the blocked step behind gate P1.5's browser row. And the demo's second clause keeps its narrow wording - the scene is evaluated and marched on a CPU, not drawn: no WGSL, no BVH, no culling, so R1 and R2 remain unmeasured. ## Spike S7 runs: latency is distance over ring, and cost is a curve, not a cliff docs/05 proposes the three-stage resolution model, then concedes it has not validated the hard part: *how does region B know its neighbour's water table without loading it?* — with the named failure that the ring may be too small (effects arrive late) or too large (commit cost explodes). Both halves are numbers, so `cargo run --release --example s7_propagation -p engine` made a 512-region river of `i32` water tables, dammed the source, and advanced a frontier that simulates only the disturbed width each commit. | ring | commits until the mouth (500 regions away) dries | region-ticks, as % of simulating the whole river | |---|---|---| | 1 | 501 | 57% | | 2 | 251 | 79% | | 4 | 126 | 89% | | 8 | 64 | 95% | Three conclusions, one of them against the document: - **the *without loading it* half holds.** Nothing beyond the frontier is touched, so a commit's cost is bounded by the disturbed area, not by the world and not by the single edit that caused it; - **the *within one epoch* half does not, and that is the design rather than a defect.** Latency is distance over ring — the dam → riverbed → wells chain this page describes, now with numbers attached; - **cost is a curve, not the warned-about cliff.** A wider ring spends 57% → 95% of whole-river work: it buys latency by pulling the same work forward. OQ-01 should be argued against that curve rather than against the warning. Two mistakes were caught by running the model rather than reading it: the first field used proportional evaporation and left the lower river dry at equilibrium, so four of five distances reported `never` — a region already dry cannot be seen to dry; and the commentary drafted before the table printed claimed the work barely changes, which the 1.65× column contradicted. The printed numbers replaced the sentence. What the spike cannot reach is SM1's other half, *identically for two clients*: that needs a hashed macro field, and A-04's schema and A-05's tag registry do not exist. So S7 stays open, narrowed from "can causality propagate at all" to "how much multi-epoch lag will we accept" — with the ring as the dial and the trade-off measured. ## The roots run inside a WASM engine, and the blocker we cited for it was a misreading docs/11 said, from the export-lint discovery until today: *"WASM is compiled but never executed… the runner has to be `wit-bindgen` output, which needs A-02."* Each half of that is true on its own — `unsafe_code = "forbid"` rejects a hand-written `#[no_mangle]`, and it rejects what `wit-bindgen` generates — and the conclusion did not follow. A **WASI command** exports `_start` because the Rust runtime provides it, so the ban is never reached. The engine now builds for `wasm32-wasip1` (a 191 KB release module), runs in CI under Node's WASI via `tools/wasm_parity.mjs`, and its output is diffed against the native build's: **13 lines identical**, `EPOCH_0 91b97a52fc5137b5…`, the region list and the slot table among them — checked in debug and release before merging, green on main in run `38265695965`. It is a matrix job across all three runners now, and `38270620391` logs `parity ok on arm64` from the macOS runner and `parity ok on x86_64` from Linux and Windows — so the ARM in the gate's `M1-browser` row is covered by V8 on Apple Silicon: the instruction set, not the browser. The step refuses to be vacuous: an empty WASM output fails the job, because an empty diff between two empty files is a green check that means nothing. What that buys gate P1.5 is a fourth **kind** of target — different code generation, a different target's runtime, a different machine model for function calls, agreeing on hashed state. What it does not buy is spike S3's fourth **device**: `M1-browser` means a browser on an M1, and this is V8's WebAssembly on the runner. Nor are the marched `f32` and raster counts in those same lines evidence of anything — they agree only because the march is add, multiply and `sqrt`, they are presentation, and ADR-017 keeps frame hashes informational by policy. OQ-41's export-lint decision stays open and stays relevant for mods exporting functions a host calls; it was simply never what stood between this repository and running its own roots in a WASM engine. ## The field becomes a picture, on a CPU, with a measured cost shape Everything in `crates/render` answered one ray at a time, while docs/04's budget table - naive, perfect culling, realistic BVH - was three multiplications of guesses beside it. `render::raster` (`237c916`) marches a pinhole grid over the same `march()` and shades the surface normal, so the renderer workstream now has a measured shape to beat rather than only a target to hope against. The pinhole takes an image-plane half-height instead of a field angle, which keeps trigonometry out of ray generation entirely - and keeps every number the tests recompute recomputable. Measured on one machine, one 3-node tree (a ball on a ground plane), release profile, 96-step budget, `surface = 1e-3`: | Grid | Rays | Evaluations | Per pixel | Hit fraction | |------|------|-------------|-----------|--------------| | 96x72 | 6,912 | 314,554 | 45.5 | 67.0% | | 192x144 | 27,648 | 1,258,840 | 45.5 | 67.0% | | 320x240 | 76,800 | 3,495,846 | 45.5 | 67.0% | Per-pixel cost is flat with resolution. Hits average 20.6 steps, and the 33.0% of rays that find nothing are charged **69.6% of all evaluations** - which is ADR-024's whole argument expressed as a share instead of a derivation, and the number the culling work has to move. Why the counts and not the milliseconds: they repeated exactly, the times did not (the same 76,800-ray grid measured 53 ms and 86 ms in one session on one binary). So `tests/raster.rs` pins *coverage* against geometry an outside calculation can predict - a unit sphere at six units subtends `tan(asin(r/d))`, which is about 368 of 4,096 pixels, asserted within 8% - alongside depth, absence at the corner, the fact that doubling a radius changes coverage and depth together, and that two renders of one tree are one image byte for byte. The first scene helper in that file failed honestly at authoring time: it stamped `FLAG_IS_ROOT` on a child as well as the root, `validate` answered `RootCount(2)`, and three tests stopped instead of quietly rendering a picture two implementations would disagree about. The boot marches a grid of its own (`a2465dd`, step 6): 3,072 rays through the same loaded asset, reported as integers - 590 find surface, 243,008 evaluations charged, of which 238,272 (98.1%) belong to the rays that found nothing. Integers on purpose: the march and the normal use only add, multiply and `sqrt`, all exact under IEEE-754, so run `38250242415` prints that line byte-identically on x86-64 Linux, x86-64 Windows and ARM64 macOS - the first whole-pixel-grid comparison this repository has made between machines. It is recorded as a diagnostic in both docs, beside the number rather than after it, because gate P1.5 is a claim about hashed state and none of this is hashed (ADR-017, gate R4). What it does not claim, printed in the example's own header and in the module's: **this is not R1.** No GPU, no WGSL interpreter, no BVH, no culling, no window, and a normal-mapped colour that is deliberately not a material model, because `material_eval`'s lighting half is OQ-10's open question and a pretty picture is not a decision. Gate R1 and R2 stay unmeasured; what changed is that "renders a procedural scene" now has pixels behind it on the CPU, and the cost story has its first measured line. ## The first culling structure is an enclosure, and it disproved the 3x estimate it replaced `render::cull` derives one conservative sphere per subtree by set relation - union covers both children, a smooth union grows that cover by k because smin rounds outward by at most k, subtract and intersect take their first operand's bound because the result is a subset of it, mirror covers the child and its reflection - and `render_culled` puts one segment test in front of every ray. Plane, Repeat, Twist, Bend and Noise answer Unbounded, which means no early-out rather than a guessed box, because a bound that is too small deletes geometry and the result looks exactly like a correct render. That refusal is why the module is testable at all: the property asserted is image equality, not speed. `cargo run --release --example cull_probe -p render` (now a CI step, since it prints only integers) costs a 64x48 grid at 96 steps per ray: | Arrangement | Spheres | Bound r | Steps plain | Steps after the ray cull | Steps saved | |---|---|---|---|---|---| | cluster | 1 | 0.40 | 294,544 | 16 | 100.0% | | cluster | 8 | 2.20 | 291,594 | 14,250 | 95.1% | | cluster | 128 | 16.09 | 278,136 | 147,384 | 47.0% | | line | 8 | 4.60 | 291,164 | 80,540 | 72.3% | | line | 32 | 19.00 | 287,112 | 287,112 | 0.0% | | line | 128 | 76.60 | 287,112 | 287,112 | 0.0% | Spike S1's reading in doc 04 guessed an early-out was worth up to about 3x. The measurement says the variable was wrong: the same structure saves everything on a compact scene and nothing on a spread one, at the same 88 hits, because a single bound grows until it contains the frustum and every ray is merely permitted. That is ADR-024's hierarchy converted from a promise into a requirement with a baseline - the Steps saved column is a floor a hierarchy has to beat, and the node-visit table further down is where it does - and it arrived by building the weak version first rather than by arguing about it. Ten tests in `crates/render/tests/cull.rs` carry correctness, all of it agreement: pixels, depth and hit count identical to the unculled run with the saving exactly `skipped * max_steps`; interior points sampled from the evaluator and required to lie inside the claimed sphere, which makes the field the oracle instead of this module's arithmetic; and unbounded scenes skipping precisely zero rays while costing what they used to. They found no bug on the first pass, which doc 04 says plainly - their value is the next bound rule, not this one. R1 and R2 are untouched: these are counts of work avoided, never nanoseconds. **The per-node skips followed in the same unit, and the table above is why they read as a nothingburger.** `scene_bounds` builds one enclosure *per node* in a single reverse pre-order pass (children sit after their parent, so no recursion and no re-walk), and `sdf_eval_bnb` consults it inside the existing `eval_at` — one evaluator with an optional table, not a second implementation free to drift, which is the same argument the WGSL agreement suite exists for. A union's arm declines to evaluate its second child when that child's own bound is already farther than the value in hand. Two findings came out of that, and both are the useful kind. First, the precondition for a skip is not "the bound contains the surface" but **"the field is at least the distance to its own bound"** — a `min` preserves that, a `max` inherits it from the first operand (so `Subtract` and `Intersect` can be pruned below), a mirror keeps it, and `smin` **breaks it**, returning less than `min` by up to `k/4`, so no node under a smooth union may be skipped however far away its sphere is. That bit is computed structurally in the same pass and pinned by a test asserting the refusal. Second: the probe's first run reported the per-node layer as saving exactly nothing, and that was the instrument's fault rather than the structure's. `Stats::evaluations` counts **march steps**, and a skip inside a step changes no step count — the pruned path returns the identical distance, so the trajectory is bit-for-bit the same by construction (which is also the proof that skipping is safe). So `SceneBounds` now tallies **nodes entered**, `render_culled` counts without skipping and `render_bnb` skips: the same frame, the same steps, measured twice. `render` reports 0 in that field, because it has no table and will not print a number it did not measure. With the right unit the numbers appear. Where the single scene bound is worth **0.0%** — 32 and 128 spheres laid along a line, one bound swallowing the frustum, 88 hits either way — the per-node layer avoids **38.2%** and **46.3%** of node visits; on the 128-sphere cluster it avoids **77.3%**, and the share grows with the count instead of decaying. The two layers are complementary rather than one replacing the other: on a compact scene the ray test still does most of the work, and on a spread one it does none while the per-node bounds do nearly half. ADR-024's `O(log N)` skip now has a measured floor to be priced against, and it is the `Nodes unpruned` column above. **A third arrangement turned the same instrument on the build itself, and it is the most actionable number in this section.** The probe now scatters spheres with the same integer hash spike S2 uses and emits that scene twice - once in the order the centres were generated, once after a median split on the widest axis. Same spheres, same camera, same pixels, same steps, same unpruned visits: only the tree shape differs. Authored order lets the per-node bounds avoid 15.2% of visits at 32 spheres and 20.5% at 128; the median split avoids 46.1% and 54.3%. That is 2.6x to 3.0x more work removed by an emission decision, and the root bound tightens with it (r 138.0 to 84.7) because covering two spheres by midpoint-and-max is order-dependent too - a tree shape is looseness as well as traversal. It was made in `b1cf3bd`: `assetc::order::rebalance` flattens a plain-union forest into a set, sorts it along the widest axis by each item's extent and rebuilds it balanced, and `lower` runs it before a node is emitted - so eight spheres authored as a chain reach the marcher as four levels over the same 15 nodes, and the compiler carries the gain instead of waiting for an author to know it. The equivalence is the test rather than a comment: a chain and a balanced tree of one set of spheres lower to identical nodes, params and depth, and the lowered field equals the minimum of those spheres computed inside the test at 288 sample points - a check against arithmetic outside the code under test, not the implementation agreeing with itself. The engine boot printed the same asset bytes, the same `t = 3.0070` and the same 243,008 evaluations afterwards, which is the same claim observed from the other end. So the scope matters: reordering children is safe exactly under a plain union, where `min` is commutative and associative with no rounding question, and unsafe under `subtract`, whose operand order is its meaning - a `smooth union`'s children cannot move past its blend either, and a `plane` or `repeat` has no finite extent to sort on, so those forests come back unchanged and a test asserts each case rather than trusting the sort to behave. Still no gate moved: R1 and R2 are frame-time claims with no GPU, and no column here may be quoted as nanoseconds — work avoided has never been calibrated to time in this repository. **The cull gained a caller in the demo itself** (`40008e7`). The boot marches its 64x48 grid twice, unculled and with the scene enclosure, asserts in the binary that the two frames are identical pixels and depth, and prints both counts: 243,008 evaluations against 178,880, bound r 2.10, 668 of 3,072 rays proved empty. The line carries its own limitation, which is the useful part - 81% of the boot's rays miss but only 21.7% are *proved* empty, because a 2.1-unit sphere around a close scene fills the frame. That is the per-tile and per-node case arriving as a measured observation in the shipped demo rather than as an argument, and the next structure has a number to beat. **A frame now belongs to the caller, which is what AS4's promise needed to be checkable at all.** `render_into`, `render_culled_into`, `render_bnb_into` and `render_bnb_with` draw into a `Frame` the caller keeps, and `Frame::prepare` clears and reserves without ever shrinking - so at a stable resolution the two buffers stop allocating after the first frame. `examples/allocation_audit.rs` prints that, and prints its own limit too: counting allocations would need a `#[global_allocator]`, which is an `unsafe impl`, which `unsafe_code = "forbid"` refuses - the same ban that decided how `assetc` writes its bytes. So the audit distinguishes what it measures (buffer capacity that does not move across renders) from what it reads off the code (the bound table: two vectors per call unless the caller hoists it, which is exactly why `render_bnb_with` exists). Six tests in `tests/frame_reuse.rs` hold the sharper point: a reused frame is the same picture at every entry point, and a shrinking viewport reuses the bigger buffers rather than freeing memory the next frame asks for again. AS4 is now half measured - the CPU side - and its upload half still waits on OQ-51 with every other device claim in this phase. ## The reload loop exists, and its design is the failure path `assetc watch [--once]` recompiles an authored shape when its bytes change. The mechanism is the easy part; what is worth writing down is why the state machine is shaped the way it is. A watch loop that recompiles a broken file and leaves the last good asset on disk is a lie the engine then renders - the author deletes a shape twice and something else gets blamed - so `Watch::observe` fingerprints the authoring text instead of trusting an mtime (coarse enough to miss a save, loose enough to fire on a no-op), compiles before it touches the filesystem, writes to `.tmp` and renames only on success, and on *any* failure returns a `Failed` outcome that leaves the previous asset intact and does not advance the fingerprint, so repairing the file rebuilds instead of being dismissed as already compiled. Seven tests in `crates/assetc/tests/watch.rs` assert that behaviour without waiting on a timer - the first observation compiles, identical bytes report unchanged, an edit rebuilds, a malformed source leaves the old asset byte-for-byte intact with no stray temp file, a missing source is a readable refusal (editors delete-and-recreate, and some swap through a temp name on every save), and `--once` reports and exits non-zero so CI asserts instead of racing a poll. Not claimed: AS1's own clause ends at the GPU buffer, which needs a device and waits on OQ-51, and OQ-48 still has no gate for any reload path. A passing state machine is not the gate. ## Measured numbers now live in one manifest, checked from both ends The boot's whole-output digest was quoted in five places and went stale the moment its raster line grew to report the scene cull - CI measured a new value, the prose kept the old one, and nothing noticed. That is a documentation defect of exactly the kind the repo's verification rules are about, and it happened while someone was fixing evidence, which is the argument for a mechanism over attention. `docs/evidence.json` is that mechanism: one entry per measured number, recording the value, what it describes, which command measures it, which run first measured it, what it supersedes and which files must cite it. Two mechanical halves close it: `tools/check_docs.mjs` fails if a listed file stops containing the current value, or if a line cites a superseded digest without the current one beside it (there is deliberately no list of acceptable "history" words - a vocabulary rule can be satisfied by choosing a word, which is not a check), and the `roots in a browser` job fails if the digest it measures differs from the manifest. Both directions were proved before committing: the rule reproduced the drift immediately, and a planted stale citation is named on its line. ## AS2 becomes a gate: three machines, one digest per asset Doc 09 has always asked for "identical `.bin` assets from identical source on two machines ( deterministic serialization)" and nothing in the repository had ever compared two machines. CI has three, so each leg now compiles the same two fixtures, hashes the result, and a following job fails unless the digests form one identical pair set. `sphere.ron` alone would have been a weak witness - one node, four parameters - so `knot.ron` exists for this gate: twelve nodes five levels deep holding the values a parser is entitled to round differently (0.1, 0.3, -0.0, 1e7) and the union ordering the build-time normalisation derives from `f32` sort keys. `38305051150` prints `AS2 ok: 3 machines produced byte-identical assets from the same source`: `4f9da1a4...` and `52b85b4b...` on x86-64 Linux, x86-64 Windows and ARM64 macOS. The gate needed two fixes and a test of its own, and both bugs were in the checking rather than the serialisation. `sha256sum` on Git Bash prefixes names with `*` where Linux and macOS print two spaces, so the first version was red across three machines whose assets matched - failing in the worst direction a gate can. And the guard loop was written `printf | while`, whose body runs in a subshell, so its `exit 1` aborted nothing: a truncated artifact would have passed. The comparison now works on normalised (hash, name) pairs inside a `for` loop, the failure message names the lists that disagree instead of blaming the assets, and `tools/check_as2_gate.sh` exercises the whole body against synthetic artifacts in three cases - agree (must pass), one machine diverging (must fail), truncated file (must fail) - as a step in the docs job, because a gate whose logic is untested is a decoration. ## Gate P1.4 has its directory — and its cost says what the tiering doc must answer Until `5a67cf3` the eviction paragraph in doc 03 was prose with no code behind it: nothing tiered, nothing evicted, and `TierRestoreIncomplete` was a variant with no producer. `crates/ledger::tier` is that code — a residency directory of caller-owned cells plus a `hot_capacity`, no allocator, no clock of its own (the caller drives `tick`, because `TickId` is consensus-relevant), and answers of `AlreadyHot / Restored / Evicted { evicted } / NoRoom`. Two things were design rather than typing: - **Two capacities, not one.** How many regions may be hot is a different number from how many the table can *remember*. The first draft used one, so a third region could not be registered at all, its eviction dropped its arena range, and re-entry had nothing to restore — P1.4 failing in exactly the way the gate exists to catch, and failing quietly. Cold therefore means unreachable without a restore pass, never forgotten. - **Everything is keyed on a digest.** `tests/residency.rs`'s eight tests do not assert that the same records come back in some order; they re-root the restored range and require the region's pre-eviction root, because that root is what a peer compares and what a save replays against. Ties break to the lower cell index so a replayed walk evicts the same regions twice, and an unknown region restores as `None` rather than as an empty range — "nobody edited here" must not be what a failed restore looks like. `388b035` gave the round trip a caller: the engine boot demotes region 0, re-enters it, prints the restored root, and that digest is docs/03's published `REGION_0`, pinned from outside the process by `the_boot_brings_a_cold_region_back_as_the_root_it_left`. The cost half is the finding. `cargo run --release --example re_entry_cost -p ledger` re-roots a full slab-sized region: **68,524 digests**, which is spike S2's own published count for that many edits, and CI runs the step on all three architectures (`38279591441`) where the count repeats identically while the price per digest does not — 757 ns on ARM64 macOS, 486 on x86-64 Windows, 302 on x86-64 Linux, i.e. **51.9 / 33.3 / 20.7 ms for one region**. So re-entry is not a new problem; it is S2's hashing pass moved to a different moment, triggered by walking back instead of by closing an epoch. And on every runner the harness has, **a single region's restore is at least one of doc 04's 16.7 ms frames** — while a camera does not re-enter one region, it re-enters the ring around it. That closes off one design path by measurement rather than opinion: recompute-on-re-entry cannot fit the frame at any interaction radius. The lever left is whether `M_cold` stores each region's root beside its records — re-entry then costs a copy — which doc 03's tiering table does not currently say, and which is **OQ-49** arriving from the read side instead of the commit side. Per-edit time falling as regions fill (8.5 µs to 6.0 µs) is not a saving to chase: `ns per digest` is flat, and the drop is scattered leaves sharing upper paths — 94% then 69% of the `12N` bound — so splitting one large restore into several small ones multiplies the fixed part rather than removing it. P1.4 is still open, and says which half: correctness is enforced, cost is measured on three architectures, and **pop-in** remains a frame-time word that needs a renderer to be visible in. ## The boot's roots now run inside a browser — and the blocker that was named for it was not one `a4c22a4`. The gate P1.5 note on this wiki said the M1-browser row "still needs a browser", and the reason on record was the export-lint decision: a page would have to call an exported function, and this workspace forbids writing one. Both halves of that reasoning were checked by building the thing instead of repeating it, and both fell. A WASI *command* module exports `_start` because the Rust runtime emits it, so a page needs no export from this repository — only the host functions the module imports. `tools/wasi_shim.mjs` is that shim, and the refusals are its substance: `fd_read` answers EBADF and `path_open` answers ENOENT, because a shim that quietly opened files would be running a different program than the boot. Before any browser was involved, `tools/shim_parity.mjs` ran the module through that shim under bare V8 with no `node:wasi` at all and reproduced the native release boot byte for byte — the control that makes a browser result mean something, because without it a disagreement could always have been a bad shim. Then `tools/browser_parity_server.mjs` served the same two files from disk to a page, and the page's own `WebAssembly` API printed the same fourteen lines. **Three hosts, one SHA-256 over the whole output: `15c1e8af1b523d73…`** — native process, bare V8 plus shim, browser page. That pair is a CI job now (`roots in a browser`, on all three architectures, green at `38283568760`), and getting there produced a lesson worth keeping: the first version of the shim *remembered* the twelve imports a local build asked for, CI's toolchain built a module importing a thirteenth (`fd_fdstat_get`), and run `38282729718` failed all three legs on a `LinkError`. The harness was broken on the machine that was supposed to be measuring the boot. So `importObject` derives the set from `WebAssembly.Module.imports` and records every name it had to refuse rather than implement — CI prints `stubs called: none` — and the job runs the V8 control before it is allowed to be interesting. A runner with no browser prints SKIP instead of quietly passing. The limit that was written here at the time — "every browser available here and in CI is Chromium, so what closed is *browser as host* and what remains is a different wasm **engine**" — was correct and only lasted a few commits, because the engine turned out to be reachable too. `tools/safari_parity.sh` enables the `safaridriver` that ships with macOS, opens a WebDriver session with nothing but `curl`, points Safari at the same page and compares the digest the page reports. On the ARM64 runner (image `macos-26-arm64`) run `38297963941` prints `OK Safari/JavaScriptCore produced the native digest`, `15c1e8af1b523d73…` — the same bytes the native boot, the bare-V8 shim and the three Chromium legs agree on. So gate P1.5's determinism claim now rests on two independent WebAssembly implementations, WebKit/JSC and V8, on both x86-64 and ARM64, rather than on one implementation wearing four hats. What is still honestly open is the hardware: N2600, RTX 3060 and a Linux-Mesa machine, and the fact that a CI runner's Safari is not somebody's M1 MacBook. The harness pays for that with one more job — eight definitions, fourteen runner-level runs — and the leg's first attempt was red because the script slept two seconds and then asserted that a server it had not waited for was dead, which is the same lesson as gating on a browser's exit code, learned twice in one day. ## Compaction is an implementation now, and it moved a gate question in the unexpected direction `crates/ledger/src/compact.rs` (commit `081137c`, with the lint fix `3c805b5`) is doc 03's bake as a data transform: `Edit { rank, record }`, `voxel_of` (drop the payload bits and the packed word *is* the position), `assign_ranks` for callers using append order, `compact` collapsing each voxel run to its greatest rank in place, `write_records` handing the survivors back. No allocator, no allocation, and the root preserved by construction — because the frozen A-10 shape hashes a voxel leaf over its payload alone and resolves duplicates at the leaf by greatest append, so a region root depends on which payload won and on nothing else about the list. Five tests in `tests/compaction.rs` assert that rather than argue it: digest equality, idempotence checked against the survivor *set* (a right-length answer with a duplicate and an omission would pass a length check), the winner following arrival order under three permutations, a duplicate-free region passing through untouched, and short buffers refusing in both directions. Then `examples/compaction_ratio.rs` measured it at spike S2's scale — 2,048 distinct voxels, 1 to 16 writes each, both of S2's arrangements — and the honest result has two halves. **Digests: identical in every row** (18,489 scattered, 4,096 clustered), so hashing is priced by distinct voxels and doc 03's `12N` was always a bound on leaves, not records; that was the first reading, "compaction is not a lever on the Merkle pass". **Wall time: not flat**, because the walk partitions its leaf slice per entry at each of twelve levels, so compaction recovers roughly 1.6x of a scattered pass and 2.7x of a clustered one *at 16 writes per voxel*, and nothing at 1-2. For the workload gate P1.3 was quoted at — a full slab of **distinct** scattered edits, where `live == records` and the transform is the identity — the option is still empty, and the floor it exposes is ~13 ms for 2,048 voxels against a sub-millisecond target. What compaction buys unconditionally is capacity: 16x fewer records in an arena, 16x fewer leaf-scratch entries in a commit, same root. That makes it a SPEC-06 and cold-tier instrument first, and it is why OQ-49's candidate list is now target, budget, scope — with frequency partial at best. ## The descent is the third culling layer, and it is worth 443x on the rows one bound could not move The section above left the spread-scene rows at 0.0% of rays proved empty and said the next structure had to be more bounds rather than a better one. `SceneBounds::may_reach_subtree` (commit `61491cc`) is that: the same sphere question, asked of each child, stopping at any node the bound rule does not license — a smooth union (its field dips below its own bound), an unbounded kind, a counting table that must stay a baseline. Combinators contribute only the rule they actually grant: a hard union's zero set *is* the union of its children's, so either child reaching is enough; a subtract or intersect has a field at least its first operand's, so it inherits that operand's reach; a mirror duplicates surface into the reflected copy, so the descent stops there. The measured difference is not incremental. On exactly the rows where the single root bound proved **0.0%** of rays empty — 32 and 128 spheres along a line, both scatter arrangements — the descent proves **96 to 98%**, and the 128-sphere line's charged evaluations fall from 287,112 to **648**, a factor of **443**, on a frame asserted byte-identical. That is ADR-024's hierarchy measured rather than asserted, and the first number in this repository that says what a BVH is worth instead of only that one is due. It also produced the bug this module's whole design exists to prevent, caught by the frame comparison and not by reading the arithmetic. The first version tested each sphere's bare radius. A grazing ray that passes within `surface` of a sphere yet *outside* it is a hit as far as the marcher is concerned — the march stops wherever `|field| < surface`, and a sphere's field is distance minus radius — so the 8-sphere cluster row disagreed with the unculled frame by one pixel and one hit. A skip is a claim about the marcher's tolerance, not about geometry, and every enclosure consulted for one is now grown by `surface`. The seeded variant — return the entry distance and start the march there instead of at the camera — was built, measured, and removed on purpose. It saved 8 march steps out of 85,508 on a mirror row while shifting 4 to 12 pixels per frame by up to 3x `surface`: the hit *set* identical, the converged points not, because a trace starting later walks a different sequence of steps. Byte-identity is the instrument behind every number above and behind the three-architecture boot diff, so trading it for a saving two orders of magnitude smaller than the skip's own is a renderer policy decision, recorded as **OQ-53** rather than taken locally. ## What a blend costs: the culling licence is an authoring decision, now priced `cull_probe` runs the same scene twice — identical spheres, identical tree shape, identical camera — with every branch written as a `smooth union` (k 0.2) in one version and a plain `union` in the other: | 128 spheres | Branch kind | Rays skipped by the root bound | Rays skipped by the descent | Evals after the descent | Node work avoided | |---|---|---|---|---|---| | line | plain union | 0.0% | **97.1%** | 648 | 98.3% | | line | smooth union | 0.0% | **0.0%** | 287,128 | 0.0% | | cluster | plain union | 44.3% | **93.8%** | 1,656 | 97.5% | | cluster | smooth union | 32.8% | **32.8%** | 180,662 | 0.0% | A skip needs `field >= distance to the node's own bound`, and `smin` returns below `min` by up to `k/4`, so a blend poisons that precondition for as far up as it reaches — **one blended branch at the root costs the whole hierarchy, a factor of 443 on the line row**, and the per-node skip goes to zero with it. The frames stay byte-identical in every row, which is what makes this a price rather than a bug. The demo now says it out loud: the boot marches its grid a third way through the descent and prints the scene's own licence bit beside the counts, and because the boot's five-node scene unions a rounded box with a smooth-union pair, it prints `root licence refused` next to two identical culled counts (179072 evaluations, 666 of 3072 rays proved empty either way). Measured, not read off the code. Two things follow. For the **asset pipeline**, blending is a frame-cost decision and not only a look decision, and nothing in the current grammar records that in the file. **The licence was then measured, and the refusal stands anyway.** `the_smooth_union_licence_is_measured_and_not_yet_relaxed` samples four scenes - two lone blends (k 0.4 and k 1.5), a plain union as the licensed control, and the boot's own union-over-blend shape - on a 41-cubed lattice at 0.25 spacing, plus dense 0.005 sweeps along the lines where `smin` dips hardest (the axis through both centres, and both waist lines). Every blended scene keeps `field >= distance to its grown enclosure` outside it, with a worst margin of **exactly +k**, zero violations larger than the render tolerance, and zero points inside the marcher's hit band lying outside the bound. The growth by `k` absorbs the `k/4` dip and leaves `k` over - so the refusal is not needed for containment on these scenes, and relaxing it is a one-line change worth the 443x above. It was not flipped, and the reason is written in the test. A lattice is not a proof, and the claim is about every point in space. And the *licensed* plain union itself reaches -0.000000 at its far corner - its bound is tight to `f32` rounding - which is why violations are counted relative to `surface` rather than against zero: a rounding-scale miss sits inside the band the marcher stops in anyway, so it cannot delete geometry. **OQ-54** now carries both options: derive the bound from the quadratic `smin` actually implemented (the same formula the WGSL twin's text guards pin) and relax with the derivation written down, or keep the refusal and tell authors plainly that blending costs the hierarchy. ## OQ-54 closed: the licence was earned, the saving was an arm The two halves of the smooth-union question came apart under measurement. **(a) The licence was owed to a blend.** The sphere stored for a smooth union is grown by `k` while `smin` dips below `min` by at most `k/4`, so outside the grown sphere `field >= min(a,b) - k/4 >= D_cover - k/4 = D_grown + 3k/4`. `respects[NODE_SMOOTH_UNION]` is now `first_ok && second_ok` (commit `22986bb`) - and granting it changed cull_probe's blended rows by **nothing**, which retracts the claim published with the licence table that relaxing it was worth the 443x. A blend's zero set lies *between* its children, outside both child spheres, so no descent may decompose a blend: the grown cover sphere is the tightest statement a blend can make. **(b) The saving was an arm a blend can take.** `smin(a,b,k)` equals `min(a,b)` exactly when `h = max(0, 1 - |b-a|/k)` collapses to zero - once the children are `k` apart the blend has nothing left to add - so `eval_at` now skips a second child whose licensed bound exceeds `a + k` (commit `da0d470`). The comparison is **strict**: `a + k` in `f32` can sit a rounding step below the true separation, and `h^2 k/4` is then tiny but nonzero - an overestimate of the field is exactly what a sphere march cannot afford. And `k.max(0.0)`, because a non-positive strength makes `smin` a plain `min` and the threshold must then be the union arm's `a`. Blended node work avoided, on the four rows that read 0.0% before: | Blended (k 0.2) | Spheres | Visits unpruned | Visits pruned | Work avoided | |---|---|---|---|---| | cluster | 32 | 688,086 | 548,603 | 20.3% | | line | 32 | 1,277,892 | 820,930 | 35.8% | | cluster | 128 | 3,898,950 | 1,336,864 | **65.7%** | | line | 128 | 5,172,420 | 2,814,446 | **45.6%** | Frames byte-identical throughout. The belt is asymmetric on purpose: the new test also runs a tight-blend scene (children 0.2 apart under a strength of 1.5, never `k` apart anywhere the camera reaches) and requires it to avoid **nothing**, so a blanket skip fails as hard as a dead one. For doc 09, what an author gets is a proved skip *inside* each blend and no hierarchy *through* one. The boot's printed line did not move, so the published digest stays `c6c4ec046bec1260`. ## The demo got a scene, and ADR-024 became visible in the shipped binary The boot's scene used to be one compact object. Its single enclosure was already tight, so the finding `cull_probe` published - one bound saturates the moment geometry spreads - was only ever visible in an example nobody runs. Commit `c04ad6e` gives the demo a scene: the hero (a rounded box unioned with a smooth-union pair) plus three spheres spread across the space, joined by plain unions. Eleven nodes, depth 4, 348 asset bytes instead of 204 - and the descent, `assetc`'s median-split rebalance and the blend's k-aware skip arm now have something real to work on inside `cargo run -p engine`. What the boot prints is the disagreement the hierarchy exists to win: ``` scene raster : 64x48 grid marched on a CPU - 605 of 3072 rays found surface, 241779 evaluations, 241779 with the scene cull (one bound r 10.79, 0 of 3072 rays proved empty), 177843 with the hierarchy descent (666 proved empty, root licence granted) ``` The single bound's radius grew from 2.10 to 10.79 and its proofs fell to **zero** - the line arrangement in miniature, arrived at in the demo rather than in a probe - while the bound hierarchy proves 666 rays empty and removes **26.4%** of the frame's charged steps, with all three frames asserted pixel- and depth-identical inside the binary. The hero's probe ray still lands at `t = 3.0070` after 8 evaluations, which is the invariant that says adding distant geometry did not move the near surface, and the hand-assembled cross-check grew to the same eleven nodes so the lowered field and an independent construction still agree. 605 of 3072 rays find surface (was 590). Nothing hashed moved - epoch root, region list, slot table and the re-entered `REGION_0` are docs/03's published digests - and the boot's fourteen-line output hash retargets to `649eff38a5529b1b…` with the reason recorded in `docs/evidence.json`. ## The frame work budget: a ceiling that holds, and what it actually costs `render::raster::render_bnb_budget` (commit `731e66d`) draws as much of a grid as a stated number of **charged evaluations** pays for — the same unit `Stats::evaluations` reports, so a budget and the number it produced are comparable without a translation step. The decision is made against `max_steps + 1`, the worst a ray can cost, *before the ray starts*, which is what makes the ceiling a bound rather than an intention. Pixels past the cut carry `NOT_DRAWN_DEPTH`/`NOT_DRAWN_RGB` and `Stats::not_drawn` counts them: "never marched" and "marched and found nothing" are different facts. `examples/budget_probe.rs` — an informational CI step, integers only — prices the curve on a 64x48 grid where the unbounded hierarchy render costs 182,144 charged evaluations and finds surface on 1,000 of 3,072 rays: | Budget | Pixels drawn | Undrawn | Surface kept | |---|---|---|---| | 1/16 | 203 | 2,869 | 0.0% | | 1/4 | 673 | 2,399 | 0.0% | | 1/2 | 1,511 | 1,561 | 36.4% | | 3/4 | 2,573 | 499 | **100.0%** | | full | 3,068 | 4 | 100.0% | The loss is not linear in pixels: a quarter of the work keeps nothing visible, because the walk is tile-major from the top left and the top-left tiles are sky, while a quarter *off* the ceiling drops 499 pixels and every single hit survives. So a ceiling is a statement about the frame's composition, and the walk order is policy — the version a real renderer wants is centre-out, keeping the subject and sacrificing the corners. The `full` row is honest about conservatism: a budget equal to the measured total still leaves four pixels undrawn, because those rays reserved the worst case and spent less. Seven tests pin the ceiling at nine levels, the byte-identity of everything a cut draws, monotonicity, tile-boundary prefixes and run-to-run equality; none of it is a frame time, and gate R1 still waits on a GPU. ## The budget walk order, measured: same ceiling, 0% or 25% of the scene `render::raster::Scan` (commit `2f05c82`) makes the visit order an argument on the ceiling call: `TileMajor` (blocks of eight, top-left first — the documented default) or `CentreOut` (tiles sorted by integer squared distance from the frame's centre, ties by row then column, so the sequence is a total order that cannot depend on how a backend rounds a square root). `budget_probe` runs both at every ceiling on one scene, one bound, one march: | Ceiling | Tile-major: drawn / surface kept | Centre-out: drawn / surface kept | |---|---|---| | 1/16 | 203 / 0.0% | 352 / **25.0%** | | 1/8 | 322 / 0.0% | 789 / **59.0%** | | 1/4 | 673 / 0.0% | 1,147 / **72.6%** | | 1/2 | 1,511 / 36.4% | 1,840 / **97.0%** | | 3/4 | 2,573 / 100.0% | 2,389 / 100.0% | At a sixteenth of the frame's work, the order is the difference between an empty image and a quarter of the scene. Centre-out also buys *more* pixels for the same money at the low end, because it reaches the provably-empty tiles earlier and a skipped tile costs nothing to decide — which corrected an assumption the probe had been asserting: equal ceilings do **not** buy equal pixel counts, since tiles differ in cost. That is the finding, not a bug: a ceiling spends against the frame's composition, so the walk order is a content decision. Nine budget tests now include both orders drawing a prefix in their own order with every drawn pixel equal to the unbudgeted frame, and centre-out keeping strictly more surface at an equal ceiling. 173 tests in 27 suites. Trunk note worth keeping: CI's clippy is 1.99 and the laptop's is 1.92, so a constant-size `chunks_exact_mut` that checked clean locally red-trunked three architectures on a lint that does not exist in the older toolchain — fixed in `2b91b29` by the boring construction. ## The demo draws under a ceiling: half the work, 99% of the scene Commit `362a060` gives the boot a fourth march of its 64x48 grid — half the hierarchy render's charged work, spent centre-out — and it prints: ``` scene budget : ceiling 88921 of 177843 charged evaluations - 1475 pixels drawn, 1597 left undrawn, 599 of 605 hits kept, centre-out ``` Half the frame's work keeps **99.0%** of its surface. That sentence is what gate R1 will eventually be priced with and what AS5 will eventually reject an asset by, so it belongs in the shipped binary rather than only in an example. Three things are asserted inside the boot, not printed and hoped: the ceiling was not overstepped, the pixel bookkeeping closes, and no pixel the budget paid for differs from the unbudgeted hierarchy render's. The output is fifteen lines now, and CI run `38322714045` is green on all ten job definitions — native, the bare-V8 shim, Chromium on three architectures and Safari on JavaScriptCore all print the same bytes, with the digest `167a0cade1327d0e…` recorded in `docs/evidence.json`. That makes the **position of the cut** a cross-implementation agreement: the ceiling lands in the same place in every engine, which is the renderer's version of what gate P1.5 claims about the ledger. Nothing hashed changed. Worth the scar tissue: getting there took three red CI runs, because CI's clippy is 1.99 and the laptop's is 1.92 — a constant-size `chunks_exact` and a division guarded by a zero test were errors there and invisible here. The lesson in the hygiene memory is now "write the boring construct a newer linter cannot flag", and the evidence-citation guard was retuned from per-line to per-file for the same reason: a check that produces thirty findings per retarget gets switched off, and a switched-off check guards nothing. ## Region streaming landed on the CPU path, and it saved something different than expected `crates/render/src/stream.rs` (commit `3ab8557`) renders a world of regions: one compiled shape per region cell, placed at `cell * 4096` in gate C1's arithmetic, composed as the **minimum of their fields**. That composition is forced rather than chosen — A-07 has no translation node (`repeat` tiles at a fixed period, `mirror` reflects about zero), so two authored shapes in different cells cannot be one tree. Whether the grammar should grow an instance kind is **OQ-55**, filed as a contract decision with its cost attached: an interpreter arm, a validator rule, the WGSL twin's agreement, and the asset-graph question an instance implies. `examples/region_cost.rs` — now an informational CI step, so its asserts run on three architectures — corrected the assumption before it was published: | Regions in world | Planned | Charged evaluations | Bound checks | Frame changed | |---|---|---|---|---| | 1 | 1 | 1,248 | 3,072 | no | | 7 | 1 | 1,248 | 21,504 | no | | 21 | 1 | 1,248 | 64,512 | no | | 64 | 1 | 1,248 | 196,608 | no | **The march does not cost the world at all** — identical charged work and byte-identical frames from one region to sixty-four — because every region already carries a per-ray bound. What streaming removes is the **test**: one sphere check per region per ray, which is the term that scales with the size of the world rather than the screen. Three tests in `tests/stream.rs` hold the picture fixed: a single region at its own cell equals the single-tree renderer pixel for pixel, depth for depth and charge for charge; a region out of range is invisible whether or not it is planned, with the plan proven non-vacuous by a range that does reach it; a seven-cell world frames identically under a one-region plan. The per-ray region test grows by the marcher's hit tolerance for the reason the cull already does — a grazing ray is a hit to the march, and a skip that disagrees with the march is a missing pixel. ## The demo spans three region cells, and OQ-57 turned out to be an assertion that could not be true `examples/region_diagnose.rs` (`1e1efe6`) ran the demo's own multi-region configuration and varied one factor per row. The result killed the mechanism this page previously carried: **the hit set is identical in every configuration** — 56 of 3,072 rays, prop cell in or out, 96 steps or 512, slab ridge or sphere ridge, either grid size. Nothing is starved by composition. What differs is a stopped *depth* in the third decimal (7202.292 against 7202.295), which at that magnitude is three `f32` units in the last place and well inside the 1e-2 tolerance that defines a hit: composing cells changes the trajectory, so the trace settles elsewhere in the same band. The per-cell interval traversal written to "fix" it was implemented, measured — it changed nothing — and deleted. The shipped consequence (`e1087dc`, CI green on all ten job definitions at `38326245766`) is step 9 of the boot: three region cells — the hero's prop-scale spread, a 900×120×900 ridge, a 500-unit hill — each compiled through the same lower → bytes → reader path as the hero, composed into one frame, printed as ``` region world : 3 cells +4096 units apart, all planned - 56 of 3072 rays found surface, 19000 charged evaluations, 2824 proved empty by their bounds; dropping the prop-scale cell leaves the hit set identical and moves no stopped depth by more than 0.005 at distances up to +12288 units ``` with the assertions stated as they can be true: identical hit sets, stopped depths within four times the tolerance, all three cells planned. That is the rule to carry forward — **across region cells a frame compares in pixels and hits, and in depth only to within the march's own tolerance** — which is OQ-53's seeding finding and the WGSL twin's 2.4e-7 cross-vendor deviation arriving from a third direction, and one more reason gate R4 stays informational. The sixteen-line output, digest `a651b7f33e2ac0c6…`, is now what native, bare V8, Chromium on three architectures and Safari/JavaScriptCore all agree on. ## 1080p, measured and derived: what a frame ceiling actually buys Gate R1 ("1080p/60") and AS5 (a compile-time ceiling) want the same missing number, so `examples/frame_cost.rs` measured it on the demo's own scene and kept the three kinds of figure apart. - **Measured on this CPU:** ~78 charged evaluations per pixel, flat across 96x72 / 192x144 / 320x240 (78.02 / 78.08 / 78.15) at 25.5–26.1 ns per evaluation. Resolution costs work; it does not change work per pixel — that is the law everything else is extrapolated from. - **Derived, labelled as such:** an unbounded 1080p frame is ~1.62e8 evaluations, ~4.1 s here, ~248× R1's 16.7 ms. Arithmetic on a CPU reference renderer, not a GPU frame time, so R1 stays unmet by this file rather than answered by it. - **Measured at 1080p under ceilings:** | Ceiling (charged evaluations) | ms | Pixels drawn | Hits kept | |---|---|---|---| | 2,000,000 | 378.3 | 126,493 | 36.2% | | 8,000,000 | 806.0 | 279,383 | 67.3% | | 20,000,000 | 1,220.3 | 467,894 | 89.1% | | 45,000,000 | 2,164.0 | 758,375 | 100.0% | So the ceiling is the dial between "mostly sky" and "kept its subject", stated in the same unit the compiler rejects on — which is why AS5 can be armed the moment someone picks a row, and why no row was picked here: choosing one is R1's frame-time call and R1 has no GPU (OQ-51). One correction made on the way: the reference is the ceiling-free *hierarchy* render, not an uncullled one — at 2.07 million pixels the difference is minutes of runtime for no difference in the picture. ## Pop-in becomes a frame count: the ceiling spread across frames, losslessly `render_bnb_refined` (commit `2ec471a`) spends a frame's ceiling over several frames: existing pixels stay, the ceiling is charged only on what is still `NOT_DRAWN_DEPTH`, and the walk resumes where the last cut stopped. The legitimacy test is byte identity — a frame finished across passes equals one finished at no ceiling, cumulative hits land on the single-pass count, coverage strictly grows, no pass overruns (`tests/budget.rs`). A budget that changed the picture would be a rendering mode, not a schedule. That converts gates R3 and P1.4's "no **visible** pop-in" into a number available before any display exists. Demo region, ceiling = 1/4 of the culled frame's charged work: | Scan order | Frames to complete | First frame's share of the hits | |---|---|---| | `CentreOut` | **5** | **25.1%** | | `TileMajor` | 9 | 1.4% | Same ceiling, same field, same march — the order alone is a 5x difference in when the subject appears, which is why centre-out is the default for transitions. The word "visible" still waits on a display, and the display on the `wgpu` licence decision (OQ-51); what no longer waits on either is the count. ## A far region cell was rounding geometry away, and gate R5 found it (2026-10-13) R5 says "zoom to 1e-6 world units on a pig snout with no polygonation". The test the crate had for it marched the scene **at the origin**, which is the easy case and not the claim. `examples/deep_zoom.rs` now runs one snout — a body sphere of r 1.0 with a bump of r 0.02 in front — through a 64x64 grid at `surface = 1e-6`, four ways, against the local baseline: | regime | hits of 4,096 | max depth delta | mean normal angle (rad) | worst normal angle (rad) | |--------|---------------|-----------------|-------------------------|--------------------------| | local (the origin) | 4,096 | 0.000000 | 0.000193 | 0.000846 | | far, camera-relative | 4,096 | 0.000000 | 0.000193 | 0.000846 | | far, absolute | 4,096 | 0.004965 | 0.071092 | 0.145554 | | far, region cell (244,140) | 4,096 | 0.000000 | not shaded | — | Row 3 is doc 02's rule stated as a price. The number to watch is the **normal**, not the depth: a polygonated snout still reports a plausible distance. Row 4 was a bug in shipping code when it was first measured. `crates/render/src/stream.rs` formed the world point from the absolute camera position and subtracted `cell * 4096` afterwards. At cell 244,140 the origin is 999,997,440 and the `f32` ulp there is 64 units, so `camera.origin + dir * t` rounded back to `camera.origin` for every `t` under 32 and the subtraction returned a flat zero — the snout collapsed along the axis the cell lay on and **1,280 of 4,096 pixels missed** (2,816 hits, 0.034 depth delta). The fix is the ordering doc 02 already required: `Region::relative_origin` subtracts the cell from the eye *first*, where IEEE-754 gives the difference exactly for two floats of comparable magnitude (Sterbenz), `t` then accumulates in the region's own frame, and `enclosure_relative` keeps `plan` and the per-ray bound out of world coordinates as well. After it, row 4 is row 2 — every pixel hits and every depth matches the local baseline bit for bit. `tests/stream.rs::a_cell_a_billion_out_draws_the_same_picture_as_the_same_cell_at_zero` pins pixels, depth, hit count and charged work. OQ-56's retirement of "the camera-relative-placement worry" is corrected rather than deleted: its angular-size half stands (a sub-pixel object four cells away *should* render as nothing), but its precision half rested on a probe that stopped at 12,288 units, where the ulp is 0.00049 and this mistake is invisible. The remaining question — how far from world zero an `f32` render path may go, and whether that number belongs in `contracts` — is **OQ-58**, and it is the Architect's. R5 stays met in part: the gate is about an image on a display, and there is no device yet. The same example then answers the question the fix left open. Sweeping the cell index from 0 to 2^28 + 1 — world zero to 1.0995e12 — every row returns **4,096 of 4,096 hits with a depth delta of exactly 0.000000**: with the subtraction ordered correctly the frame does not care where in the world it sits, because the eye and the cell origin cancel exactly and `t` never sees a large number. What degrades is *placement*, which is the quieter failure: | cell index | world origin | origin − exact `i64` (voxels) | `f32` ulp there (voxels) | |------------|--------------|------------------------------|--------------------------| | 1 | 4,096 | 0 | 0.0005 | | 4,096 | 16,777,216 | 0 | 2.0 | | 244,140 | 999,997,440 | 0 | 64.0 | | 16,777,217 | 68,719,476,736 | −4,096 | 8,192.0 | | 268,435,457 | 1,099,511,627,776 | −4,096 | 131,072.0 | So the two thresholds are 2^24 **world units** (cell 4,096), past which an `f32` origin cannot name a fraction of a voxel, and 2^24 **cell index**, past which the index conversion itself is wrong by up to a whole region. Nothing in a frame reports either: the picture stays perfect while the shape sits somewhere *near* where the ledger says it is, which is why the number belongs in a contract rather than in whichever subsystem notices first. The third figure is R5's own — an `f32`'s ulp crosses 1e-6 near 16 world units, so the gate's tolerance is unreachable in absolute coordinates beyond that, and camera-relative arithmetic is the only thing that makes it reachable a billion units out. The sweep runs in CI on all three architectures (step `Deep zoom, four coordinate regimes`, run `38332751812`) and the runners print the same table digit for digit. Stated narrowly on purpose: these are presentation floats and nothing hashes them, so the agreement is not a P1.5 clause and adds no device to S3's list. What it establishes is that the ordering rule is compiler-independent — a WGSL twin has no rounding excuse for disagreeing, and the deleted-geometry bug above was a real bug, not one machine's arithmetic. ## The hierarchy moves inside the region, and the demo now pays for it (2026-10-13) `cull_probe` priced ADR-024's ray-level descent on one tree and `region_cost` priced what a per-region bound saves across cells. Neither priced the case a streamed world actually is: a cell spread wide enough that **its own cover sphere swallows the frustum** — the `cull_probe` rows that prove 0.0% of rays empty — so the coarse test does nothing and the children are all that is left. `stream::render_bnb` now asks the descent of each region's tree, in that region's coordinates from that region's eye (the same ordering rule as the fix above: a bound table is a statement about the tree's frame, not the world's), and `render_with_tables(cull::counting_bounds)` runs the identical walk that prunes nothing, so the saving is a measured difference rather than an argument. One cell, a 64x48 grid, 3,072 rays: | ridge | nodes | plain evaluations | with the descent | rays proved empty | node work avoided | |-------|-------|-------------------|------------------|-------------------|-------------------| | 9 spheres, spacing 1.2 | 17 | 229,888 | 5,632 | 0 → 2,336 | 85.6% | | 9 spheres, spacing 8.0 | 17 | 283,884 | 876 | 0 → 2,948 | 97.4% | | 33 spheres, spacing 8.0 | 65 | 283,884 | 876 | 0 → 2,948 | 96.4% | | 65 spheres, spacing 6.0 | 129 | 294,912 | 11,904 | 0 → 2,948 | 96.0% | Three readings. The prize is bigger than node work: a ray the cell's cover sphere permits but no child can reach is proved empty *before the march starts*, so it is charged nothing — the evaluation column moves, not only the visit column. Cost stops tracking the world and starts tracking the neighbourhood: 33 spheres cost exactly what 9 spread spheres cost, the same 876 evaluations, because 24 of them are never entered. And it is not free or universal: the tight row buys 85.6% instead of 97.4%, every frame pays a bound table per selected region (O(nodes), once per frame rather than per ray), and none of this is a frame time — R1 still waits on a device (OQ-51). Step 9 of the boot marches its three-cell world both ways and asserts the two pictures are one pixel for pixel: **174,881 charged evaluations and 1,039 rays proved empty** by the cell bounds, against **57,665 and 2,260** with each region's children asked. The hero's `t = 3.0070` did not move, and no hashed value did either — EPOCH_0, REGIONS, SLOT_TABLE and the re-entered REGION_0 still agree with doc 03, and the WASM leg still matches the native build line for line. Only the whole-output digest moved (`d66eff19…` → `594a8b1cc9add7d2…`) because a printed line grew; `docs/evidence.json` carries the new value with the old one superseded and the reason attached, which is what the CI browser and JavaScriptCore legs now print. The test that owns the law is one-directional: pixels, depth and hits equal, evaluations and proved-empty rays allowed to move only in the safe direction. Its first version asserted evaluations *equal* — saving confined to node visits — and failed, which is the measurement correcting the expectation rather than the code. CI's Format step caught a second mistake on the same unit: the boot was edited after the last `cargo fmt`, so a local clippy-and-test pass was green while all three runners went red. A local pass is not the trunk gate. ## The demo now performs the carry, and it cost nothing but frames (2026-10-13) Step 9b of `cargo run -p engine` refines one streamed frame of the shipped three-cell world at a ceiling of **one ray charge** (`max_steps + 1` = 97) until nothing is short of, and asserts inside the binary that the settled frame is pixel-for-pixel and depth-for-depth the picture the unbudgeted descent march found. Measured on that world: **813 frames**, cumulative charge **57,665 — exactly the single pass's**, with 2,260 rays proved empty along the way. CI's three implementation legs (native, Chromium, Safari/JavaScriptCore) all print the resulting seventeen-line output as the same digest `3c67db5f51644de6…`, so the demo's shape is a cross-implementation agreement, not one machine's stdout; no hashed value moved. Two conclusions, and the second is a defect-shaped finding rather than a win: - **Refinement is free in work and expensive in frames.** The carried total lands on the single pass's charge to the unit, so a ceiling is a statement about *latency to a complete frame* — the thing a pitch demo is actually judged on — not about cost. - **The ceiling is charged before the reachability test, so free rays go uncollected.** The gate asks whether the next ray could fit its worst case, and only then whether it hits anything; once the answer is no, the frame stops, leaving thousands of zero-cost pixels waiting for the next frame. Frame one settles a single ray out of 3,072. Reordering is arithmetically sound (a proved-empty ray cannot breach a ceiling) but changes which pixels are drawn at a given ceiling, which is exactly the prefix property `tests/budget.rs` exists to pin — so it is filed as **OQ-60** rather than slipped in as a quiet edit. ### P1.4's pop-in clause, measured on the arriving region's own pixels (2026-10-13) The gate's wording is about the picture a re-entered region contributes — "shows prior edits with no visible pop-in" — and every number we had counted pixels, not *whose* pixels. `examples/region_pop_in.rs` attributes them: render the composite with the re-entered cell and without it, difference the depths, and watch only that footprint fill as the frame is refined. On the demo's geometry (a 400-unit hill one cell over, owning **70 of 3,072 pixels — 2.3%**, which is OQ-56's angular-size fact in a demo costume), at a one-ray-charge ceiling: | order | pixels settled by frame 1 | of which the arriving region's | footprint 50% | footprint complete | frame complete | |---|---|---|---|---|---| | centre-out | 8 | **0** | frame 41 | frame 76 | frame 111 | | tile-major | 224 | **0** | frame 75 | frame 110 | frame 111 | Two conclusions the frame average hides. First, "the frame is done" is the wrong question: what arrives first is the cheap, provably-empty majority, and in either order the region you just paid a full Merkle pass to restore contributes *nothing* to frame one. Second, the walk order that made no difference to frames-to-finish (57 frames either way, on the synthetic ridge) makes roughly a **2×** difference to the arriving region's own surface — so OQ-60's reorder has to be judged on a region-attributed measure, because the two metrics disagree about what a good first frame is and only one of them is what a player sees. The instrument runs in CI as an informational step beside the carry curve; the Linux, Windows and macOS runners print the same table. "Visible" pop-in still waits on a display and therefore on OQ-51: these are frames, not milliseconds. ## The same ceiling, spent across cells — mirrored from docs/04 (2026-10-13) The budget sections above spend a ceiling inside `raster`, which owns one tree. The frame the demo composes is a minimum of several regions, and a ceiling across cells is a different claim unless it is the same law — so `stream::render_budget` spends it the same way: the decision is made against `max_steps + 1`, the worst a ray can cost, *before the ray starts*; tiles are walked in the order the caller asks for; a pixel the cut never reaches reports `Frame::NOT_DRAWN_DEPTH` / `NOT_DRAWN_RGB` rather than a plausible sky. It is not a second march — the per-ray work moved into one private `march_ray` that the plain, descent, counting and budgeted walkers all call, because two copies of the composite march is exactly the fork that would later show up as "the budgeted frame disagrees with the full one". `tests/stream.rs`'s ceiling test is the gate: ceilings from zero to twice the full cost, none overrunning, every drawn pixel equal to the unbudgeted frame in colour and depth, `rays == drawn + not_drawn` at every level, and an unlimited ceiling byte-identical to the descent's frame in both buffers. On the same wide ridge the descent prices (33 spheres, spacing 8.0, one cell, 3,072 rays), fractions of that frame's own unbudgeted cost: | ceiling | order | charged | pixels drawn | rays proved empty | surface kept | |---------|-------|---------|--------------|-------------------|--------------| | 1/8 | centre-out | 13 | 22 | 21 | 0.8% | | 1/8 | tile-major | 13 | 1,238 | 1,237 | 0.8% | | 1/4 | centre-out | 129 | 43 | 29 | 11.3% | | 1/4 | tile-major | 129 | 1,259 | 1,245 | 11.3% | | 1/2 | centre-out | 343 | 106 | 60 | 37.1% | | 1/2 | tile-major | 343 | 1,322 | 1,276 | 37.1% | | 3/4 | centre-out | 564 | 158 | 75 | 66.9% | | 3/4 | tile-major | 564 | 1,758 | 1,675 | 66.9% | **Coverage differs by 11× to 56× and the share of surface kept does not differ at all.** The reason is the descent itself: a proved-empty ray is charged zero, so a walk that spends its ceiling on cheap tiles finalises most of the grid while barely touching the budget, and the expensive rays near the subject are the same handful whichever tile it starts from. `pop_in`'s single-tree case — a scene that fills the frame, where no per-region test throws the sky away — produced the opposite advice from the same law (25.1% of hits in the first frame centre-out against 1.4% tile-major). So on a streamed frame with the descent working, **walk order decides how fast the *background settles*, not when the subject appears**, which is why the order stayed policy rather than becoming a default tuned to one fixture. Two facts follow, and both cost a failed attempt to learn: - **A ceiling below one ray's worst case draws nothing, and that is correct.** The gate is decided against `max_steps + 1` before the ray starts, so a budget smaller than one ray cannot afford a single ray: the frame stays entirely at `NOT_DRAWN_*` and charges zero. That is what makes the ceiling trustworthy as a gate rather than a statistic — and it is also indistinguishable from a stalled resume, which is what the first refinement attempt was mistaken for. Consequence for callers: **specify ceilings in ray charges, not as a fraction of a frame.** `full/8` of a 32×24 grid is less than one ray, and a caller who picks that number will watch a renderer do nothing and assume it is broken. - **Frames to finish do not depend on walk order, even though pixels per frame differ by 56×.** At one charge per frame the wide-ridge grid settles in **57 frames centre-out and 57 tile-major** at 1/8, 3 at 1/2 and 2 at 3/4, and the cumulative charge across all 57 frames is exactly the single unbudgeted pass's **876** — asserted at every ceiling and in both orders. `stream_descent` prints that carry measured; the column it once *derived* was wrong in both directions at the tightest ceiling — 140 frames centre-out and 3 tile-major estimated, against 57 measured in each order — so it is gone rather than corrected. ## The WASM SIMD axis, measured and inert (P1.5, S3 — 2026-10-13) `blake3` and the SDF evaluator compile to WebAssembly SIMD when the module is built with `-Ctarget-feature=+simd128`, which is exactly the difference between a browser that has SIMD and one that does not. That axis was measured rather than assumed: build the engine for `wasm32-wasip1` with `+simd128`, build it again with `-simd128`, run each under Node's WASI, and slice the boot block out to the `// boot complete` marker. Both modules produce **the same 17 lines and the same sha256 (`496a4dfadf084698…`) as the native build**, which means SIMD codegen moves neither the hashed lines - `EPOCH_0` `91b97a52…`, `REGIONS`, `SLOT_TABLE`, the re-entered `REGION_0` - nor the presentation lines. Combined with the scalar-hash CI leg (the A-10 vectors reproduced under blake3's `pure` build on all three architectures) and the Safari-on-ARM64 job (which prints the native digest from real WebKit), the codegen surface the four named devices could differ on has been narrowed considerably. Two honest limits. The comparison is **not yet a CI step** - it is documented with its exact commands so the next harness addition has a target - and none of this is the user's hardware: the N2600, the RTX 3060 box, the Linux-Mesa machine and the M1 browser itself still have to run `tools/s3_device_check.sh`. ## SIMD-invariance is now checked per architecture in CI (P1.5, S3 - 2026-10-13) `tools/wasm_simd_parity.sh` builds the engine for `wasm32-wasip1` with `+simd128` and, in the single-runner job, with `-simd128` too, runs each under Node's WASI, slices the boot block from the header to `// boot complete`, and hashes it. The job matrix adds one `+simd128` leg per OS; each runner already diffs its default module against its own native build, so invariance follows transitively without a second rebuild. Run `38360391644` prints the verdict four times - Linux, Windows, ARM64 macOS and the full check - with the boot block at 17 lines and sha256 `496a4dfadf084698…` in every configuration. It is a check, not a printout: exit 1 on disagreement (proven by tampering one digit of a depth and watching it report the line and fail), exit 2 when node or the wasm target is missing. Two bugs were found by running it rather than reading it - a filename that collapsed both variants onto one file (making the "full" check vacuous, and it had already merged once as green), and a Windows step that inherited PowerShell until it asked for `shell: bash`. Neither the harness nor this axis is gate P1.5's remaining clause: the four named machines still have to run `tools/s3_device_check.sh`. ## x86 codegen level cannot move the boot output or the asset bytes (P1.5, S3, AS2 — 2026-10-13) The four machines in gate P1.5 differ in real instruction sets: the N2600 has no AVX2 and no FMA, while a desktop box on the same list has both. `tools/cpu_codegen_parity.sh` asks the question without owning the silicon — build the engine and the asset compiler at the default target and again at `-Ctarget-cpu=x86-64-v3`, then compare. Boot digest `3c67db5f51644de6…` (the published one) and the compiled `knot.ron` stream `52b85b4b13e5429a…` are **identical in both builds**: no hashed value, and no serialized asset byte, depends on vector width or FMA contraction. CI runs it as a step in the WASM job (run `38361528310`, green). Why the harness refuses to be vacuous: the first attempt at this check "reported" a mismatch that was really a build failure whose empty output got hashed — `e3b0c442…` is the hash of nothing and it looks like a value. A failed build, an empty boot, or an empty `.bin` now exits 2 (harness could not run) rather than producing a comparison, and every variant is checked against the baseline build so a run that rebuilt nothing cannot pass. Same lesson the SIMD harness taught a few minutes earlier, where a filename collapsed `+simd128` and `-simd128` onto the same file and the "three-way" check hashed one module twice — and had already merged as green. ## Which determinism axes CI checks, and the rules that stop them passing vacuously (2026-10-13) Axes, all on every push: three architectures native; the whole engine as a `wasm32-wasip1` module under Node's WASI on each; the same module in bare V8, in Chromium, and in WebKit/JavaScriptCore driven by real `safaridriver` on an ARM64 Apple-silicon runner; the hasher on `blake3`'s scalar `pure` build; the module compiled with WASM SIMD on and off; and native binaries at the default x86 target and at `x86-64-v3` (AV2+FMA) — the ISA gap between gate P1.5's Celeron and its desktop box. All agree on the 17-line boot block (digest `3c67db5f51644de6…`) and on the compiled asset bytes. Peer-comparison legs need rules that pinned-expectation legs don't: an expected digest cannot be satisfied by an empty run, because the empty hash is a constant that simply fails the pin. So the comparisons now refuse an empty asset or empty module output; two fixtures hashing identically (one stream named twice); the empty-input digest `e3b0c442…` on either side; and any native reference lacking the boot's closing marker — two legs agreeing about truncated output is agreement without a boot. `tools/check_as2_gate.sh` carries negative cases for each rule, so the guards are demonstrated rather than trusted. The cautionary tale, recorded because it is the one that matters: a filename built with `tr -d '+-'` collapsed SIMD-on and SIMD-off onto the same file, so the three-way check hashed one module twice and merged **green while structurally unable to fail**. Three of the four holes found today were in harness code written that same session. Verify a harness by running it and reading the paths it prints — and select CI runs by commit SHA, because `gh run list --limit 1` immediately after a push returns the previous commit's run. ## The device command gained an opt-in ISA axis (P1.5, S3 — 2026-10-13) `tools/s3_device_check.sh --codegen` now asks the second question each of gate P1.5's machines can differ on in one pass: not only "does this box reproduce the published roots" but "does its output move when the same source is compiled at a different x86 codegen level" (baseline vs `x86-64-v3`, i.e. without and with AVX2+FMA — the actual gap between the N2600 and the RTX 3060 box). Verified end-to-end on a Windows x86-64 host before shipping: exit 0, WASI leg agreeing line for line, and `this machine's output does not move across x86 codegen levels`. `--help` prints usage, an unknown option exits 2 rather than being ignored, and the default run is unchanged — the flag is opt-in because a second full release build lands hardest on the slowest machine the gate is asking anyone to boot, and a harness that wastes a Celeron's afternoon gets skipped instead of run. A self-correction worth recording: while wiring this up I nearly added a CI step running a probe that is already in CI twice over. Checking before pushing caught it; the fix was a revert, not a merge conflict. ## What the GPU spike measured in CI, and why R1 still cannot be measured there (S9, R1, ADR-026) PR #1's adapter probe and WGSL-agreement steps ran on all three CI architectures (run `38272893103`). The result is the reason gate R1 is not a code gap: | runner | adapters visible | device opened? | what that can and cannot prove | |---|---|---|---| | ARM64 macOS | `Apple Paravirtual device`, Metal, IntegratedGpu | yes | WGSL runs and agrees with the CPU reference; a paravirtual GPU is not a frame-budget measurement | | x86-64 Windows | `Microsoft Basic Render Driver`, Dx12, **type Cpu** (only adapter) | yes (software) | same: validates the shader, cannot time a frame | | x86-64 Linux | none | no | the step prints `No device means no frame curve: gate R1 cannot be measured here` and skips, by design | With a device, the A-07 WGSL master interpreter matched `crates/render` on **all thirteen implemented kinds within 0.0001 world units** (widest `twist`, 0.000001; 729 sample points per kind) - the contract's "two implementations, one answer" claim, now executed rather than asserted, and labelled presentation-only: not P1.5, not R4, not R1. S9's cost half, measured on a Windows x86-64 host before the PR: third-party packages **34 -> 104** (66 new), cold build **132 s**, `target/debug` **1,160 MB -> 1,904 MB**. What it settles is that the abstraction's price is known and that `cargo-deny` rejects exactly one licence in it - `hexf-parse`, CC0-1.0, via `naga`, arriving through the SPIR-V/DXIL backend paths so no feature trim removes it for a desktop build. The frame curve S9 was finally asked to produce needs a real GPU: hardware, then OQ-51. ## P1.6's repair half landed, and its walk cost is measured (2026-10-13) `ledger::repair::catch_up` settles the one divergence shape that needs **no causal rule**: a peer whose records are a verbatim subset of the leader's. It returns the leader's root, and refuses every other case — `Disagrees` names the voxel whose payloads clash, `NotBehind` refuses the wrong direction instead of pretending to merge, `NoRoom` refuses to truncate into a state neither peer holds. Choosing a winner when both sides really disagree stays OQ-46, deferred with multiplayer by ADR-004; this function deliberately does not guess. Both halves of the gate now run in the shipped binary — step 3b of `cargo run -p engine` diverges a peer missing one of three appends, asserts the bisection walk names that voxel (`None` on the side that never got it), reconciles it to the leader's root, and re-runs the walk to show nothing remains. Detection had never been demonstrated there before; the boot is where a wrong answer becomes visible. The cost note in the source used to be a rationale ("up to 8 subtree hashes per level per side… the measurement belongs with the spike"). It is now a measurement (`cargo run --release --example p16_walk_cost -p ledger`, digest counts, not microseconds — timings do not repeat across machines): finding a divergence costs **1.26× to 2.23×** the two root passes a caller runs anyway to notice one at all — 372 digests against 167 at 8 records, 46,903 against 37,287 at 2,048 — and the ratio *falls* as the region grows, because the walk's extra work is one path down twelve levels while the root pass touches every leaf. Agreeing sides cost the two passes only; a lag costs no walk. CI reproduces the numbers so the quote cannot rot. ## OQ-51's spike re-measured against current `main` (2026-10-13) The wgpu spike branch is the thing standing between this project and gates R1/R2, and its evidence was 25 commits and a day old. It was re-measured in a **local scratch merge** — the branch itself was left untouched, nothing pushed to the PR — with these results: | what was checked | result on the merged tree | |---|---| | build | clean | | `cargo test --workspace` | **199 tests, zero failures** — trunk's 194 plus the branch's own five in `crates/gpu/tests/wgsl_validates.rs`, so this is the merged tree's count, not a new trunk baseline (trunk is still 194 in thirty suites) | | `cargo clippy --all-targets --all-features -- -D warnings` | **clean** | | WGSL ↔ CPU reference, all 13 implemented kinds | **within 0.0001 world units**, widest `twist` at 0.000001 (Microsoft Basic Render Driver, Dx12) | | the CC0-1.0 licence chain | **unchanged** — `hexf-parse` ← `naga` ← `wgpu-core`/`wgpu`, still not feature-trimmable | | merging `main` into the branch | **one conflict, in `.github/workflows/ci.yml` only**, additive on both sides; union resolution = 63 steps. A chore, not a decision | | dependency cost, comparable yardstick | `main`'s `-p engine` resolves **19** third-party packages; the spike's `-p gpu` resolves **75** | Two honesty notes kept in the record: the PR's original **34 → 104** was a workspace-wide count measured by a *different method* than the 19/75 above, so the two must not be compared; and the part that decides this gate — `cargo-deny`'s licence and advisory verdict — **could not be run locally**, because the binary isn't installed here, which is exactly why it belongs to CI on a real merge rather than to anyone's assurances. A caveat written fifteen minutes earlier ("not re-measured against the current tree") was falsified by doing the measurement, and was replaced rather than left in place. ## The device command gained a browser leg (P1.5, S3 — 2026-10-13) Gate P1.5's fourth named target is an **M1 browser**, and `tools/s3_device_check.sh` — the one command built for real hardware — reached only the native and WASI legs. The capability was never missing: CI has driven Chromium-family (`browser-execute`) and Safari/JavaScriptCore (`browser-jsc`) for a week. What was missing was a command for the person holding the machine, so `--browser` now delegates to the same harnesses and compares each engine against **that machine's own boot output**, not the published digest, so a stale `docs/evidence.json` cannot manufacture a false agreement or a false failure. `tools/browser_parity_check.sh` runs Safari via `safari_parity.sh` wherever `safaridriver` exists and a Chromium-family binary wherever one does, and prints its own inventory line — `SUMMARY: browser legs: safari=… chromium=…` — because the way this project has repeatedly closed a gate by accident is a leg that quietly did not run. Exit 2 when no engine could be driven at all, which is explicitly not a pass. **The first run produced a fabricated finding, and the harness was changed for it.** The leg originally used whatever module sat on disk, so it compared a fresh native boot (`db71f8a0…`) against bytes left from an older commit (`7c71da7c…`) and reported `CHROMIUM LEG DISAGREES` — identical in shape to the P1.5 result it exists to catch. It now rebuilds `wasm32-wasip1` by default (`MLWS_SKIP_BUILD=1` opts out for someone deliberately testing a fixed artifact) and prints the module's sha256 and byte count so the record says which bytes ran. Re-run on the same Windows x86-64 host: module `3b841513d6f3a6cc…`, 284,779 bytes, Chrome reported on the first attempt, browser digest equal to the native boot and to `docs/evidence.json`, exit 0. The Safari leg reports `SKIPPED(not-macos)` here, which is the honest answer on this machine and the reason the inventory line exists. **CI now runs the same script** (repo `1581db3`, run `38400699273`, all jobs green). The `browser-execute` job's inline Chromium step was a second copy of the same comparison — the fork rule this project states about marchers applies to harnesses too — so the job calls `MLWS_ENGINES=chromium MLWS_SKIP_BUILD=1 MLWS_NATIVE_OUTPUT=host.txt bash tools/browser_parity_check.sh` and keeps the two guards the script leaves to its caller: the closing-marker check and the declared-vs-measured comparison against `docs/evidence.json`. A leg returning status 2 prints a loud `SKIP` rather than reddening trunk, because a runner without a browser is a fact about the image, not about the floats. Read from the logs rather than from the green badge, all three runners drove a real browser: `db71f8a0bf5818d6…` from Chrome on Windows (`890086d4f784ff67…` module), on ARM64 macOS (`4b05122c8819297f…`) and on x86-64 Linux (`214a2a7d25ee91e8…`) — the module hashes differ per platform because each runner builds its own, and the digest the boot prints does not. ## Gate P1.5's device clause became data the build checks (P1.5, S3 — 2026-10-13) The demo's third clause — one epoch root bit-identical across the four Bitwise Determinism Suite targets — is the one this project cannot close from a workstation, and it was also the only one nothing enforced. Its state lived in prose, which can say *not met* long after the last row stopped being true, and can say *met* one commit before anyone notices that nobody booted a machine. `docs/evidence.json` now carries `gate_p1_5_targets`: the four named machines, the command that exercises each, their status. All four read `"not run"`. `tools/check_docs.mjs` holds the rules, and each was proven by violating it rather than by reading it: | violation planted | result | |---|---| | an `"agrees"` row citing a boot digest from an older commit | `FAILED` — evidence about one code state cannot close a gate about another | | an `"agrees"` row with no digest | `FAILED` — a verdict that names no bytes | | a `"diverges"` row with no note | `FAILED` — the most valuable record the register can hold | | docs/11 stating a count the register does not hold | `FAILED` | | README.md stating a count the register does not hold | `FAILED` | | a target quietly dropped from the register | `FAILED` — four named targets, or the clause shrank | | everything restored | `OK: no findings` | The derived count is written into the prose wherever the gate's status appears, so the sentence a reader sees is checked against the data: currently **0 of 4 named targets have reported**. `s3_device_check.sh --target