Roadmap and status - UnitBuilds-CC/Merkles-World GitHub Wiki
Authoritative version: docs/11-roadmap.md.
| 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 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 |
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)
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 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'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 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, the half ADR-028 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) |
| 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 propagate β dam in region A dries region B without loading B? | doc 05, gate SM1 β the highest-risk spike |
| 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.
-
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 atx = 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):
ActionTxbroadcast, 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.
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 was signed off and locked SPEC-09. A-10
contracts/tree.rs β drafted 2026-10-11, frozen 2026-10-12 on ADR-030, 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); 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 DeltaRecords), 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 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.
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.
- 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".
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.
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.
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 = 5in 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
f32ulp 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.
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.
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: one of the demo's four clauses is true, one is true at the ledger level, two are not started - there is a booting engine and a persistent edit, and there is no drawn scene and no four-device agreement.
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.
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/renderevaluates thirteen of A-07's fifteen kinds, addingTwistandBend.Noisestays 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), andBendis 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.
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 <in.ron> <out.bin> 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.
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.
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 ledgermeasures them (digests counted byregion_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. 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.
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 <asset.bin> (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.
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.
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.
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.
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 aplaneorrepeathas 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.
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 <output>.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.
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.
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.
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 asNonerather than as an empty range β "nobody edited here" must not be what a failed restore looks like.388b035gave 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 publishedREGION_0, pinned from outside the process bythe_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.
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.
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 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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.rsexists to pin β so it is filed as OQ-60 rather than slipped in as a quiet edit.
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 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 + 1before the ray starts, so a budget smaller than one ray cannot afford a single ray: the frame stays entirely atNOT_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/8of 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_descentprints 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 <label> prints the paste-ready record for one row and exits 2 on an unknown label β the first version printed
its refusal after the verdict line and still returned 0, which is a green run that produced no artifact. The
record carries host and os beside the verdict, because which machine a label names is a claim the harness
cannot verify: the record this workstation printed says M1-browser, host Ian-Dell, and is honest about
being neither.
The checker is checked too (6d43db4, CI 38403614341). Those rules are all rules about absence β a row
with no digest, a count nobody verified, a target that stopped being listed β and an absence rule is satisfied
just as happily by a checker that stopped working. tools/check_docs_selftest.mjs copies the tree to a temp
directory, plants each of the ten violations in turn, requires the validator to report every one, then requires
the restored copy to report nothing (ten "must fail" cases would also be passed by a checker that always fails).
Measured in CI's docs job, read from its log rather than from its badge: baseline clean, caught: ten times,
OK: all 10 planted violations are caught. And the suite was itself falsified β with the stale-digest rule
sabotaged to accept anything it prints NOT CAUGHT and exits 1.
"194 tests in 30 suites" appears on README's front page, in docs/11's status table and in
docs/evidence.json, and the only thing that checked it was that those three agreed. Three claims checked
against claims is the failure mode this project has now hit twice β once where CI's browser leg caught a digest
every local cross-check had waved through.
The gates job now pipes its own cargo test --workspace into tools/check_suite_size.sh, which sums the
per-suite test result: ok. lines and compares the tally with the manifest: exit 1 on disagreement, exit 2 when
nothing was measured β empty input, a tally that parses to zero tests, or a run containing failures, since
counting only the passing lines would understate and pass. The step declares shell: bash, because on the
Windows runner the default would swallow the pipe.
Read from the three runners' logs rather than from the badge: each prints
RESULT: the published test count is what this run measured - 194 tests in 30 suites. Falsified the same way as
every other check added today: deleting three suites from a saved tally produces
MISMATCH: the run measures 161 tests in 27 suites while the docs publish 194 tests in 30 suites and exit 1.
The parser's own first draft read the word passed instead of the number before it β caught by running it once,
which is the only way anything in this section has ever been caught.
raster_probe renders a 320Γ240 grid and hands the pixel buffer to a PPM in the temp directory β the demo's one
viewable artifact. It printed the path and the size and asserted nothing about the file, so the bytes a person
would open were never compared with the frame the marcher produced.
Now the example reads the file back and requires every pixel byte to equal the frame in memory, then requires
the depth buffer's surface count to equal the march's own stats.hits. Measured, and printed by CI's own log on
the Linux runner: all 230400 bytes equal the frame in memory, 51444 of 76800 rays found surface, matching the march's own count.
The statistic's own first draft is the lesson here: it counted pixels that are not pure black and returned 76,800 of 76,800, because the sky is a gradient β a number that cannot disagree with any world. It was caught by reading what it printed, not by checking whether the step was green. When adding a measurement, ask which wrong world would make it change.
region_pop_in measures a 64Γ48 grid because those are the demo's own rays and the footprint percentages have
to be the demo's β but a two-cell composite is not legible at 64 pixels wide. The example now re-marches the
identical scene at 256Γ192 and writes it as a PPM, under the same rule raster_probe follows: read the file
back, require every byte to equal the frame in memory, and require the surface count to equal what that march
reported. The camera is consumed after the measurement loop, so no tabulated figure moved.
All three CI architectures print the identical line β 256x192, all 147456 bytes equal the frame in memory, 1720 of 49152 rays found surface β which is presentation-byte agreement across vendors. It is informational:
the objective's remaining clause is about hashed state on the four named devices, and this is neither hashed
state nor those devices.
cargo run --release -p engine -- --frame-ppm hands step 9's world_frame β the frame the shipped binary
already asserts equal to the descent's picture β to a 256Γ192 PPM, nearest-neighbour upscaled from the marched
64Γ48 because two cells meeting is not legible at that size. It is the boot's own path rather than an example,
so nobody has to trust a second copy of the demo's geometry to see what the demo drew. Verified by reading the
file back and asking it to agree with the march: 259 of 3072 rays found surface, every byte equal to the frame in memory β the same 259 the region line beside it prints. Opt-in, not a second march, and the default boot
still hashes to the published db71f8a0bf5818d6β¦, so nothing anyone cites moved. The flag is filtered out of the
asset-path argument, or engine --frame-ppm would try to load a file named after an option.
How it red-trunked, and the lesson is old: the first version was green on every local check β build, tests,
clippy β and failed clippy 1.99 on all three gates architectures for a constant-size chunks_exact(3) where the
newer lint demands as_chunks::<3>().0. This laptop's clippy has no such lint. Fixed in a1948a0, trunk green
again. For lints, CI's clippy is ahead of yours; local green is not trunk green, and this has now cost a red
trunk twice with the same shape.
The cause is not an agent's stale machine: rust-toolchain.toml asks for channel = "stable", which is not a
version. CI resolves whatever stable its runner image carries β 1.99 as of today β and a workstation resolves
its own (1.92 here). A newer clippy is a superset of lints, and with -D warnings that gap is a build
failure nobody can reproduce locally. Both of today's red trunks came from that mechanism.
Two responses, deliberately split by who owns them:
-
Procedure (shipped):
tools/local_gate.shruns the trunk checks in CI's order βfmt --check,clippy --all-targets -D warnings,cargo test --workspace, docs integrity, the docs-checker's own self-test, and the AS2 gate logic β stops at the first failure, and prints the local rustc and clippy against what the most recent CI run reported. Measured on this workstation: six steps ok on 1.92/clippy 0.1.92, and where the CI version cannot be fetched the line says so instead of implying agreement. If the two version lines differ, CI's lint set is larger than the one that just passed. -
Question (filed as OQ-62): pin an exact stable in the toolchain file, keep
stableand live with the lag, or pin in CI alone. Pinning interacts with the manifest'srust-version = "1.92", so it is a toolchain story decision rather than a commit an agent should make quietly. The register now holds 62 open questions.
Two commits closed the register's last soft shape. tools/check_docs.mjs now requires an "agrees" row to
record which legs ran, requires those legs to include native β a machine that never computed the roots has
not reported anything β and requires the M1-browser row specifically to include browser. The hole is the
ordinary one: Safari's remote automation is off by default, the leg reports that as a skip, the run carries on
and prints verdict: AGREES (correctly: the machine did reproduce every published vector), while nothing reading
the register could tell that the engine the target names never ran.
s3_device_check.sh --target <label> now inspects the legs it is about to emit and says so beside the record,
remedy included, so the operator meets the refusal on the device rather than in CI three files away. Both paths
exercised on this workstation: browser leg unable to run (MLWS_ENGINES=safari on Windows) gives
legs: "native,wasi" plus the warning; Chrome driving gives legs: "native,wasi,browser" and no warning.
Trunk green at 50438b0.
The self-test grew with the rules β 13 planted violations, all caught, pristine control clean β and the older
no-commit case gained an honest legs value in the same edit, because the legs rule fires first and a planted
case that trips a different rule proves nothing about the rule it names.
tools/check_boot_digest.sh runs the boot, hashes its 18 lines and compares the result with
docs/evidence.json's published digest. It exists because of one specific incident: a citation edit that
landed inside the boot's own printed line changed the whole-output digest while no hashed vector moved, so the
local suite β build, clippy, 194 tests, cross-file consistency β stayed green and only the CI leg that hashes a
real run caught it. A claim verified only against other claims is the hole; this is the check that reaches into
it before a push rather than after.
tools/local_gate.sh is now seven steps: fmt --check, clippy --all-targets -D warnings, cargo test --workspace, docs integrity, the docs checker's own self-test, the AS2 gate logic, and the boot digest.
Measured three ways on this workstation: the honest run exits 0 at db71f8a0bf5818d6β¦; a manifest pointing at
the superseded 7c71da7c⦠exits 1; a manifest with no claim to read exits 2. A check that cannot measure is
never allowed to report a pass.
s3_device_check.sh --preflight answers "what can this machine actually check?" in 1.7 s without building:
cargo and rustc versions and host, the wasm32-wasip1 target, node, a browser binary, safaridriver on macOS,
the sha256 tool, and which of doc 03's four vectors it found β exiting 2 when even the native leg is impossible.
The N2600 is the slowest box the gate names and the release build is the expensive part of the command, so
discovering afterwards that node is missing spends a compile to learn the legs were skipped anyway. Its own
output also surfaced a defect: the header line a device run is asked to paste re-prefixed every field of
rustc -vV, duplicating the host line and emitting rustc: binary: rustc. Now it prints version and host.
The MISSING branches have not been exercised β this machine has every tool β so a bare box is their first
real test, and that is said rather than implied.
docs/11's Phase 1 exit table was a day behind its own work. Four rows now state what is measured: P1.5 as
0 of 4 named devices reported (a checked register rather than a paragraph), P1.7's frame-visibility blocker as
measured rather than open, the scene row as two viewable artifacts CI compares byte-for-byte against the march,
and a heading dated for the reconciliation. Citations were re-derived from git log, because the first draft
credited --frame-ppm to two commits that were not it.
OQ-50's ruling is now a paste, not a drafting task. docs/13 carries the proposed doc-02 row: a voxel names
the half-open span [x, x+1) x [y, y+1) x [z, z+1), centre at +0.5, one cite for renderer, doc 07 collision
and future base terrain (ADR-021). It is written as a section under the register β giving a proposal its own
question id is the exact mistake OQ-61 was opened over β and it is applied nowhere, because the ruling is the
Architect's and no code converts a voxel to world space yet, so no subsystem has voted.
One command records a device run β and it found a latent break in the checker's own tests (P1.5, S3 β 2026-10-13)
tools/record_device_run.sh takes the JSON line a device printed and does the bookkeeping in one place: the
label must be a real register row, an agrees record must carry the current boot digest, a hex commit, a
date and legs including native (plus browser for M1-browser), the derived N of 4 named targets have reported count moves in docs/11 and README in the same step, docs integrity gets the last word, and a failing
validator restores all three files. It is a dry run unless --apply.
Two bugs came out of running it rather than reading it. Its first version read the node verdict after stripping
the OK| prefix, so the field names shifted one slot and it proposed bumping the prose to 4 of 4 for a
single agreeing row β the restore path caught it before anything was left behind, which is the behaviour it was
written to have. The second was worse and independent of it: two of the checker's fourteen self-test cases
hardcoded the counts 0 and 1, so the moment a device genuinely reported 1 of 4 the violation they plant would
have been the truth, and CI would have failed on exactly the day the gate advanced. Both cases now derive their
numbers from the register under test β proven green against a 0-of-4 register and a mutated 1-of-4 one, 13
planted violations caught either way.
tools/record_device_run_selftest.sh runs as a step in the docs job and exercises the recorder inside a
throwaway copy of the repository with its own git history β so the restore path is real and the working tree is
never at stake. Four bad records must be refused with the three governed files untouched (a digest other than
the published one, an M1-browser row with no browser leg, a label that is not a register row, malformed
JSON); one good record must land in all three with docs integrity still passing; a dry run must change nothing.
Seven behaviours, all observed in the runner's log at 6830cde.
Two defects worth the telling. The recorder wrote the validator's log into target/, which does not exist on a
fresh clone β the redirect would have failed and the restore branch would have fired on a valid record; it now
uses a temp path. And three bugs were in the new test itself, each found only by running it: --apply placed
outside a command substitution became a bare command, and a whole-tree git status check counted the test's own
log files as evidence of a dirty tree.
The operations doc described a loop that ends in "green" and listed none of the machinery, so a new agent would
relearn every lesson the hard way. It has a Verifying before you claim section now: tools/local_gate.sh
(seven steps in CI's order, ending in the boot's whole-output digest against docs/evidence.json), the docs
checker and its 13-violation self-test, s3_device_check.sh --preflight then --browser --target <label>,
record_device_run.sh with its own refusal test, and the browser-parity script that CI and a device operator
now share deliberately.
Seven traps, each tied to something that actually happened here rather than to taste: a check you have not
falsified is not a check; stable is not a pin and cost two red trunks on 2026-10-13 when CI's newer clippy
rejected lints the local one never saw; a verification command placed behind a pipe reports the pipe's exit
status (three times in one day, once with the wrong interpreter as well); a green run does not prove the step
ran, since a leg can pass by skipping, on one runner of three, or against an empty artifact; a harness must
rebuild or hash what it compares, because a stale module once fabricated a gate finding; CI runs are selected by
headSha and their status starts at queued; and these files are CRLF, which in a scripted edit nearly
deleted a register row and left a bash parser blaming the wrong character.
frame_cost and pop_in publish figures the docs quote β the steps Γ nodes frame-cost law, and the
single-tree 25.1% centre-out against 1.4% tile-major β and neither ran in any CI step. Both were run locally
first (exit 0, values matching the quotes) and then wired in as informational steps at 438ffb16.
That push went red on asset bytes on ARM64 macOS and on asset bytes agree across machines (AS2) β jobs the
commit does not touch. Re-running the failed jobs came back all green on attempt 2, so it was a runner flake.
Two things follow. A red trunk you did not cause is still yours to resolve: re-run, confirm, then report β
reverting a correct change to silence an infrastructure failure would have reopened the coverage gap and buried
the cause. And the cascade was itself informative: with one of three legs missing, the AS2 job refused to
conclude that the machines agree instead of quietly comparing the two that ran. That is the fail-closed
behaviour the vacuous-green rules were written for, observed for free by a flaky runner.
cpu_codegen_parity.sh asked one question of one architecture: it skipped every non-x86 host, so the axis that
separates the N2600 (no AVX2, no FMA) from the desktop box had no counterpart for the machine gate P1.5's
fourth target actually is. It picks its spread by architecture now β x86-64 baseline against x86-64-v3, arm64
baseline against -Ctarget-cpu=native, i.e. the generic ARMv8 build versus this runner's full NEON feature set β
and skips anything else with a stated reason. --print shows what a host would be compared without building,
which is how both branches were checked on a machine that is only one of them.
A step runs it on the ARM64 leg; read from that job's log rather than its badge:
RESULT: on arm64, boot output and compiled asset bytes are identical across 1 codegen levels [native] and the default baseline β hashed lines and asset serialization alike.
What it does not do is close the clause. An Apple CI runner is a proxy for the user's M1 MacBook, not that machine, and the device register still reads 0 of 4: codegen breadth is a determinism question about compilers, the open item is a determinism question about four pieces of hardware nobody has booted this on.
The register checked that a record's commit looked like a hex SHA and never that it resolved to a commit. It
is the field the whole chain hangs on β how a verdict on a machine gets tied back to the code state that
machine built β so a mistyped or invented SHA satisfied the rule while making the claim impossible to recheck.
Recording now refuses a commit the clone cannot resolve and says what to do about it: fetch it, or record from
the checkout the run was built in.
Deliberately not enforced in CI: actions/checkout defaults to a depth-1 fetch, where a legitimate
older ancestor SHA genuinely does not resolve. The check belongs where the history exists β the operator's
clone, which is also where the register is written.
The self-test gained the case, and its accepting records switched to the throwaway clone's real HEAD, because
a test that invents its own SHAs would soon fail for the right reason. Eight behaviours, all verified in the
docs job's own log on 403d773.
The merge chain is worth reading as a sequence of checks catching each other, because none of it was found by looking:
| what broke | what caught it |
|---|---|
| the register and prose still said 194 tests / 30 suites | all three gates jobs went red on the CI-recomputed count β the local gate has no such step, so a green pre-push run coexisted with a red trunk |
ignore = [], β a trailing comma from a line-joining patch |
cargo-deny refused to parse the policy at all: an unreadable policy enforces nothing, and it says so |
[advisories] unmaintained = ["paste"] |
cargo-deny 0.20 rejected the key outright (it does not exist) |
ignore = ["paste"] |
accepted, then read as a yanked-crate waiver: warned that nothing matched, and still failed on RUSTSEC-2024-0436
|
| the real waiver | the id taken from the tool's own output, not guessed; a waiver written from an invented identifier would be fabricating the evidence it exists to record |
| merged files not rustfmt-clean | the pre-push gate refused the push before it happened |
Final state: advisories ok / licenses ok / bans ok / sources ok, all jobs green at 4797142. Cost, measured:
66 extra crates, one licence allowance, one advisory waiver, and the WGSL-vs-CPU agreement step now runs on
every push. What it did not buy is an R1 number. buildbox has a working GTX 1070 β driver 580.178.04,
Vulkan 1.4 visible to vulkaninfo, after also needing ian added to the render group β yet wgpu enumerates
zero adapters there, with enabled_backend_features = VULKAN | GL and no loader output even under
VK_LOADER_DEBUG=all set correctly inside sg render: it never reaches libvulkan. That is an
ash/dlopen-level question, still open. And this session's laptop turned out to be a GeForce MX250, below
docs/04's "mid-range dGPU" bar, so its numbers are labelled, not quoted as the gate's answer.
The first GPU measurement, and it is a design verdict rather than a pass (R1, R2, S4, ADR-012 β 2026-10-13)
crates/gpu/examples/frame_curve.rs measures the one quantity that can run on a device today β the A-07 WGSL
master interpreter at one field evaluation per pixel on a three-node tree β and times it two ways, because
the two costs are not interchangeable: hand-off to the queue, and completion including the copy-back a harness
needs to look at a result.
| device | 1080p submit-only | 1080p with read-back |
|---|---|---|
| GeForce MX250, Vulkan (this bench) | 0.137 ms | 91.35 ms |
| Apple Paravirtual, Metal (CI's ARM64 runner) | β | 54.97 ms |
Against R1's 16.67 ms budget for a whole frame, the reading is not "close, needs optimisation" β a renderer that computes distances on the GPU and copies them to the CPU every frame cannot meet R1 on this class of hardware whatever the shader does, so the march has to live on the device. That is ADR-012's design restated as a number instead of an intention.
Two guards came out of running it rather than writing it. Interpreter::open() prefers the software adapter on
purpose (the agreement check wants identical results across CI architectures and a laptop), which meant the
first timing run silently measured the Microsoft Basic Render Driver β a CPU β so open_hardware() exists, and
it retains non-CPU adapters rather than merely ranking CPU last: the Windows runner has nothing else, and
would otherwise have reported a software rasteriser as a GPU result. It now prints SKIP frame_curve: no device
there and on Linux, benches on macOS, and the whole run is green at 226e8b2.
What the number is not: not a march (~45.5 evaluations per pixel measured on the reference), not culled, not a BVH, not the gate's mid-range dGPU, and never hashed (ADR-017; R4 is informational). R2 was still unmeasured then β it needs continuous terrain editing inside a running frame loop, which is the edit-to-geometry step OQ-50 still gated. OQ-50 has since been ruled and both halves of that loop have run; see the two sections below.
Three wrong diagnoses preceded the right one, and each was exposed by running something rather than by
reasoning further: wgpu-hal was blamed for declining silently after grepping its lib.rs β where it indeed
logs nothing β while vulkan/adapter.rs warns freely; the binary under test was a stale build with no logger
wired in, so "the loader is never reached" described that build rather than the library; driver, ICD, loader
layers and /dev/dri permissions were each eliminated on the way.
The actual cause: that host's gcc is a Nix store wrapper, and binaries linked by it don't consult the
system ld.so.cache β so dlopen("libvulkan.so.1") and dlopen("libEGL.so") fail there while vulkaninfo,
linked by the distro toolchain, works fine. Fixed by linking with /usr/bin/gcc-13, recorded in that machine's
~/.cargo/config.toml and not in the repository: one box's toolchain is not a project requirement.
What came out the other side reframes R1 rather than confirming a guess. On a GTX 1070, one A-07 field evaluation per pixel across a full 1080p grid completes in 13.46 ms β inside R1's 16.67 ms budget β while the ~46 evaluations a real march needs cost 551.5 ms. So the gate is not a shader-speed question that tuning closes; the budget buys roughly one evaluation per pixel and culling, the hierarchy and the step budget have to cover a ~33Γ gap. The laptop's absolute figures are labelled unstable β 91 ms and 627 ms for the same pass minutes apart β which is why R1's eventual verification needs a distribution, not a headline number, and why timings stay outside the hashed surface entirely (ADR-017, R4 informational).
An accepted edit now reaches the frame β P1.7's missing leg, built and measured (OQ-50, P1.7, ADR-021 β 2026-10-14)
cargo run -p engine --example visible_edit walks the whole path for one edit: append to the slab, commit,
read the record back, resolve it to a GlobalVoxelPos with voxel_at, derive its centre from doc 02's
ruling ([x, x+1), so corner + 0.5), and march a frame with and without it. This is the first consumer of
that convention and the only place in the workspace that turns a voxel coordinate into a world position β
voxel_at stays pure index arithmetic, so nothing pre-decided the question before it was ruled.
The claim is arithmetic, not adjectives: at 8 units the centre ray's surface moves 9.000 β 7.513 where the span predicts 9.000 β 7.500, and 32 of 3,072 pixels shift beyond the march's tolerance. At 512 units zero pixels do, because one world unit spans 0.062 of the frame β OQ-56's angular-size fact turned into a guard: a probe that did light up at that range would be the bug, so the assertion points the other way. Three caveats are printed rather than buried: 3,040 of the changed pixels at 8 units are sub-tolerance step jitter from a larger root enclosure, not visible surface; A-07's box takes no centre parameter, so the voxel is drawn as its inscribed ball and a true cube still waits on OQ-55's instance node; and nothing here is hashed, so no P1.5 or R4 claim attaches (ADR-017).
Gate R2's first measured edit loop β and the assertable quantity turned out to be an integer (R2, P1.7 β 2026-10-14)
cargo run --release --example r2_edit_loop -p engine (now an informational CI step) fills a real Slab to
1 / 100 / 1,000 / 6,000 records β 6,000 being ten minutes at ten edits a second β and re-marches the same
64Γ48 frame at each stage, asserting P1.7's placement on every marched frame. The gated quantity is
Stats::evaluations, not a duration: 18,284 charged evaluations per frame at every occupancy, asserted equal,
printed identically by the Linux, Windows and ARM64 macOS runners. Two instrument lessons came from running
the falsification rather than reasoning about it. A plant can be inert: raising max_steps from 96 to 189
changed the charge by nothing and exited 0, because every ray in this frame converges long before the cap β the
cap is not a term the frame pays β while coupling the stop tolerance to occupancy (one part in 1e7 per record)
did fire, on a 0.02% change (18,284 β 18,280). And the count first fired for the wrong reason: 19,712 against
18,284 was the fixture's own edit walk climbing into a second z layer, which genuinely changes the marched
scene, so the walk is held in one plane and the comparison is about occupancy alone. The milliseconds are
printed and asserted nowhere β the first draft's 1.3Γ stage-median bar failed at 2.50Γ purely on machine load,
and two runs of identical code put the 6,000-record median at 3.44 ms and 8.69 ms.
cargo run --release --example device_edit_loop -p gpu is the same loop over the same slab and the same
deltas() read-back, with the rays handed to interp.wgsl's march_rays instead of render::raster, measured
on a GeForce MX250 over Vulkan. A device hands back fewer assertable terms, so the two that are integers or
geometry carry the claim: 3,072 rays find surface at 1 / 100 / 1,000 and 6,000 records, asserted equal across
a 6,000Γ change in occupancy, and at every one of those occupancies the device agrees with the reference
ray-for-ray β 0 hit/miss disagreements, worst depth deviation under 0.002, the anchor on_device_march uses,
which is what keeps a dispatch the driver folded away from arriving here as a frame. The centre pixel returns
7.5129 at every stage, where doc 02's placement says 7.500 and the same grid over unedited terrain says
9.0000 β so an accepted edit arrives in a device frame at the distance the ruling gives it. That assert is
live: marching the unedited tree inside the loop fired it at occupancy 1 and exited 101.
Two limits are stated in the output rather than buried. The flat hit count is a guard, not the edit's signature: the terrain plane is 2048 units wide, so all 3,072 rays find surface with or without an edit, which is why the visible-edit claim rides on the depth instead. And durations behave even worse on the device than on the CPU β two runs of identical code put the stage medians at 3.679β3.900 ms and then 7.222β20.559 ms β so nothing timing-shaped is asserted anywhere in the loop (ADR-017). What stays open is structural: the edit reaches the device by a host-side tree rebuild, because A-07 has no instance/translate node in which to place a voxel (OQ-55). R2 is therefore measured on a renderer that keeps the ray loop on the GPU and is still not met as a frame β R1's 16.67 ms at 1080p is unmet, and the ten-minute / 36,000-frame span is derived from a flat count, not run.
AS4's hot-path clause: a compiled shape uploaded once, bound per frame (AS4, ADR-015, S4 β 2026-10-14)
Interpreter::march had always re-uploaded the node records and the parameter pool β one upload, one dispatch,
one copy back, which is exactly what a correctness probe wants and exactly what a running frame must not do.
upload + march_uploaded now split the two halves (crates/gpu/src/device.rs), with march being their
composition so no existing caller changed behavior. cargo run --release --example as4_upload_reuse -p gpu (a
CI step) asserts the two terms a device can honestly report: the six reused frames match the per-frame path
bit for bit on all 3,072 depths plus the hit count, and the cost term is a byte count β 80 shape bytes
resident once against 98,304 ray bytes every frame. The equivalence is falsified inside the binary by uploading
a shape with one parameter moved (radius 2.0 against 3.5), whose depths are required to diverge β they do, first
at ray 0, 14.856176 against 14.850224 β because "they match" would otherwise also be satisfied by a bind that
ignored the buffers entirely.
The finding outruns the clause it closes: the shape is never the per-frame cost, the rays are β 80 bytes against 66,355,200 at 1080p β and a real renderer generates rays on the device and uploads neither. Consistent with that, the durations were indistinguishable (11.123 ms reused against 11.662 ms per-frame upload) and, per ADR-017, no duration is asserted anywhere.
cargo run --release --example r2_ten_minute_span -p engine (a CI step) marches 36,000 consecutive frames
of the 64x48 reference march while one terrain edit is appended to a real Slab every sixth frame β ten edits a
second at sixty frames, both numbers taken from the gate's own wording, not from tuning. Occupancy therefore
grows continuously rather than in the four stages r2_edit_loop sampled, and the invariant is asserted on
every frame: 18,284 charged evaluations on all 36,000, compared against frame 1's value at the moment each
frame is marched, plus doc 02's placement under the eye, plus the slab handing back exactly the records it took.
So doc 03's O(1) hot-path claim now has the span R2 names behind it.
Two runs of the loop's own falsification, and one failure worth more than a pass. Planting a second z layer
from record 3,000 fired the charge assert at frame 18,001 (19,712 against 18,284); marching the unedited tree
after frame 20,000 fired it at 20,001 (16,184). A third plant was built to be charge-neutral so it would reach
the placement assert alone β shift the centre one voxel along x β and it exited 0: it moved terrain and
camera together, and the march is camera-relative, so it changed nothing about the frame. The placement assert
is therefore recorded as not independently proven in this instrument (every violation tried also moves the
charge, and the charge check runs first), while the same depth check is falsified in device_edit_loop, where
saturated hits leave it the only live term.
No duration is asserted, and the reason is now measured three times over: three runs of this code put the median at 1.94 / 2.40 / 3.54 ms and the p99.9 at 8.51 / 14.27 / 108.03 ms β one code's tail ranging 12.7x between runs. What remains open is unchanged and named: this is the CPU reference at 64x48, the device loop still samples four occupancies, R1's 16.67 ms at 1080p is unmet, and an edit still reaches the GPU by a host-side tree rebuild because A-07 has no instance node (OQ-55).
The CI leg reproduces the integer and not the clock (run 38621355849). All three gates jobs print
18284 evaluations per frame, asserted equal on all 36000, while their timing columns disagree by an order of
magnitude: median 0.68 ms on x86-64 Linux (p99.9 0.86 ms, a 1.27x tail), 0.69 ms on ARM64 macOS (2.39, 3.47x),
0.97 ms on x86-64 Windows (1.68, 1.73x) β against 1.94-3.54 ms medians and an 8.51-108.03 ms tail on the busiest
laptop. Same code, same 36,000 frames, four machines: the count travels, the tail belongs to the box.