Open questions - UnitBuilds-CC/Merkles-World GitHub Wiki
Authoritative version:
docs/13-risks-open-questions.md
โ the project's control document. Read it before writing code that touches a subsystem.
Three registers: SPEC = an internal contradiction in the design that must be resolved before code ยท OQ = a missing decision or an unproven claim ยท Claims = quantitative assertions inherited from the source conversation that nobody has measured.
Status vocabulary: open ยท proposal (written, needs sign-off) ยท locked (an ADR exists) ยท
spike-required (an experiment decides it).
The rule: if your subsystem has a row in open state, write the decision, don't write the code.
These are genuine conflicts inside the source material. Each one silently breaks a pillar if code is written against the wrong reading. 9 registered: 4 locked โ SPEC-01, SPEC-02, SPEC-03, SPEC-09 โ 4 proposals, and 1 open (SPEC-07). One proposal is now half-closed: SPEC-04's hashing role is settled by ADR-028's sign-off, its terrain-continuity role is not.
| ID | Conflict | Resolution | Status |
|---|---|---|---|
| SPEC-01 |
LocalOffset is "12-bit uint (0..4095)" in prose but u16 in the Rust |
keep u16 field type, 12-bit valid range; masked, never clamped |
locked 2026-10-11 โ pinned by A-01's freeze; ADR-029's consequences leave LocalOffset as drafted, so amending it now takes the contract-change route (AGENTS.md ยง2) |
| SPEC-02 | The sketch lets the GPU compute block state, but WGSL f32 transcendentals differ per adapter โ every GPU reports desync |
ADR-017: simulation computed in deterministic Rust/WASM; the GPU projects, never decides | locked |
| SPEC-03 | A bitpacked u64 holding X/Y/Z (12 bits each) + material ID cannot also hold an i64 global position |
region-slot indirection: the slot table lives in the epoch header and the region key itself (3รi64) is never packed. Field widths: the original |16 region slot|16 intra-slab|32 value| split is superseded by ADR-029's |16 region slot|36 intra-region|12 payload|
|
locked 2026-10-11 โ ADR-029 signed off, which settles the field widths. How the 12-bit payload divides (material / flags / quantity) is still doc 03's call |
| SPEC-04 | "cryptographic hash (like SHA-256)" vs "spatial hash / Morton" vs "Perlin/Simplex" for spatial continuity | two hashing roles: BLAKE3 for identity/roots, an integer spatial hash / noise lattice for terrain continuity. No doc yet says how base f(pos, t) stays coherent across region boundaries โ that is OQ-27 |
hashing half locked 2026-10-11 โ ADR-028 signed off: BLAKE3-256, 32-byte digests, domain-separated prefixes, published test vectors. Terrain half open (OQ-27) |
| SPEC-05 |
S_{t+1} = f(S_t) (iterative CA) vs "fast-forwarding takes milliseconds / O(log ฮt)" |
per-field clocks: every rule declares iterative {cap}, closed_form or integral; iterative rules advance by min(ฮt, cap) and are never claimed exact beyond the cap |
proposal |
| SPEC-06 | Slab identity: volume? column? plane? A whole region is 4096ยณ = 2ยณโถ voxels (64 GiB at 1 B/voxel); one 4096ยฒ cross-section is still 2ยฒโด (16 MiB) | canonical (RegionKey, axis, plane_index) โ a 256ยฒร64 plane tile = 2ยฒยฒ voxels (4 MiB), 3 orientations |
proposal (doc 03) โ ADR-029 (accepted 2026-10-11) took the slab out of the record address, so this is an allocation/streaming choice rather than an addressing constraint. Geometry still undecided, no longer blocking A-01 or A-03 |
| SPEC-07 | Circuit cache invalidation: whole component or dirty subgraph? | undefined in the source; measured by spike S10 (doc 11) on a real build | open |
| SPEC-08 | "The player sleeps" as the commit boundary is single-player; multiplayer has no single sleeper | one global epoch on quorum-or-timer (ADR-027); compaction stays a policy callback (ADR-004) | proposal |
| SPEC-09 | The SPEC-03 record cannot address the SPEC-06 slab: intra-slab offset 16 bits indexes 2ยนโถ = 65,536 positions, while the slab holds 2ยฒยฒ voxels and a region holds 2ยณโถ โ so the 16 bits is neither a voxel index nor a slab index as written |
three ways were open: (a) widen the record to 16 + 36 + 32 = 84 bits, rounded to 96 or 128; (b) shrink the slab to โค 2ยนโถ voxels and add more slabs per plane; (c) keep 8 bytes and make the 16 bits an index into a per-region sparse edit list. None of the three was taken: ADR-029 drops the slab from the address and splits the u64 as |16 region slot|36 intra-region (3ร12)|12 payload| โ exactly 64 bits, and slabs become append-only record buffers
|
locked 2026-10-11 by ADR-029's acceptance. A-01 coords.rs freezes with it and A-03 / A-08 have a byte layout. Re-open only if REGION_SHIFT stops being 12 โ 3ร12 = 36 holds exactly for a 4096ยณ region |
- OQ-01 โ Can distant causality actually propagate? Dam in region A dries region B within one epoch, without loading region B. The project's central unknown; the ledger's entire value proposition depends on it. Decided by spike S7, gate SM1.
-
OQ-02 โ How do stateful/sequential circuits work (latches, counters, feedback) in an acyclic
f(inputs, t)solver? Circuits are unimplementable without this, and automation is a core pillar. - OQ-03 โ What does the server store and validate if it holds no voxel arrays? Defines hosting cost, the "zero heavy lifting" claim, and the cheat model.
- OQ-04 โ What do you actually do in the game? The design is an engine; no gameplay loop exists.
- OQ-10 โ Lighting model. Shadows, GI/AO, sky, day-night: the source conversation never covered it, and nothing can be authored until shading is defined.
OQ-01, OQ-02, OQ-03, OQ-08 (event tag semantics: one-shot, latch, or accumulating scalar?), OQ-09 (which CA rules admit jump evaluation), OQ-13 (does a 10,000-edit-per-epoch player break the "near-zero storage" claim?), OQ-15 (does anything need sub-voxel precision?), OQ-29 (are the cheapness claims verified or asymptotic hand-waving?), OQ-42 (what is ฮt in per field โ ticks, seconds, or days?).
OQ-10, OQ-12 (transparency, water, volumetrics under raymarching), OQ-26 (answered 2026-10-11 by
ADR-026's sign-off โ wgpu is the GPU substrate and the raymarcher is ours, written
as a WGSL compute pipeline; the cost half of the question is still owed by spikes S1/S9),
OQ-27 (noise evaluated at runtime or precomputed), OQ-28 (does laziness freeze an
unwatched clock's displayed state?), OQ-39 (minimum-spec performance budget โ no target means no
definition of done), OQ-40 (audio engine/DSP and voice management ownership).
OQ-17 (several mods mutating the same field in one epoch), OQ-22 (circuit recompile cost mid-edit โ
measured by spike S10 together with SPEC-07), OQ-23 (event log only, or materialized region fields
too?), OQ-24 (compaction cadence beyond sleep),
OQ-30 (FlatBuffer schema versioning and migration โ old worlds must load in new engines),
OQ-37 (names and repo layout for two products โ it used to carry the licence too, decided
2026-10-11 as MIT OR Apache-2.0), OQ-38 (target platforms: browser makes WASM+WGSL a hard
requirement; console contradicts the mod story), OQ-45 (how do the delta record's 12 payload bits
divide between material id, flags and quantity? ADR-029 fixed the width and A-03
packs it as one opaque u16, but 12 bits is not enough for all three meanings โ and the answer is
baked into every region root created after it), OQ-47 (which gate measures frame-0 edit
visibility? The mechanism is specified โ a local delta projects into M_active on the frame it is
made, rollback is one store of the previous u64 back into its slot โ but P1.2 measures commit
throughput and OQ-18 is multiplayer round-trip, so the one property that makes the world feel
responsive is currently only prose. Its trigger moved 2026-10-12: crates/ledger computes region and
epoch roots now, crates/memory appends records into slabs and ledger::commit roots them, and
ledger::tier is the residency directory the tiering table assumed โ but nothing projects a delta into
M_active, so the gate that names
when an edit becomes visible โ is still owed.) OQ-49 (gate P1.3 asks for a Merkle pass over a full dirty slab array in < 1 ms. Spike S2 measured
that in three parts, and the numbers moved as the code did: the O(Nยฒ) resolution pre-pass cost 46 ms per
full slab and is gone โ resolution now happens at the leaf โ leaving 48.6โ52 ms scattered / 5โ7 ms
clustered for 8,192 edits, of which roughly 508 ns of each 700โ760 ns digest is raw BLAKE3. The pass is
hash-limited, not code-limited. cargo run --release --example pass_budget -p ledger is the decision
packet: the 1 ms line falls between 128 and 512 edits per region per epoch (128 scattered 895 ยตs, 512
clustered 496 ยตs); batching across regions computes more digests rather than fewer (548,192 versus
483,285 for the same 65,536 edits โ counts CI repeats identically on three runners, timings that disagree
and even flip sign on ARM64 macOS); and 1 to 16 slot-table rows commit in 4.10โ4.43 ms, so the number of
live regions is not the cost driver. So which moves: the sub-ms target, the slab budget, the compaction
frequency, or the gate's scope? It also decides SPEC-06 from the other end, because an N that must shrink
to a few hundred for a gate to hold stops being a capacity question. A second direction arrived
2026-10-12, from the read side: gate P1.4's re-entry costs the same pass โ 68,524 digests for one
slab-sized region, at least one 16.7 ms frame on every runner CI has โ so whatever P1.3's answer is,
P1.4 inherits it unless M_cold stores each region's root beside its records. A smaller N per epoch
therefore helps the commit and does nothing for the restore, which is a thing the decision should know
before it is made.)
OQ-50 (which world span does a voxel coordinate name - the unit cube [x, x+1), or a cube centred on
the integer? Doc 02 fixes the size and the region arithmetic but not the placement, and no other doc
does. Presentation-only: nothing hashes a voxel's position, so this is not a determinism question. But
the raster path, doc 07's collision and the future base terrain must agree, which is ADR-021's single
coordinate authority doing its job. Found 2026-10-12 while wiring the demo's "an edit you can see"
path, which cannot be built on a guess - so it is a question, not a TODO.)
OQ-52 (do A-09's error variants need payloads? LedgerError::TierRestoreIncomplete is fieldless, so
the code that detects a failed restore cannot say which region failed. ledger::tier consequently
returns Option and an Admission that carries a RegionKey, and crates/memory keeps a crate-local
slab error by its own comment rather than adding a variant โ i.e. two subsystems are already inventing
error shapes because the frozen contract cannot express what their call sites hold. A-09 is the shared
surface, so either its variants gain the identifiers (RegionKey, slab index, epoch index) as a proper
contract change with a migration note and an ADR, or the doc states that A-09 is a code band only and
each crate owns its error type โ in which case the variants no crate can construct should be labelled
host-side expectations instead of APIs. Flagged rather than decided: contracts/ is the Architect's file.)
OQ-53 (must a culled frame be byte-identical to the unculled one, or only identical within the
march's own tolerance? SceneBounds::may_reach_subtree skips whole rays and leaves the bytes exactly as
they were. The same descent could also hand back the entry distance and start the march there; that variant
was built and measured โ 8 march steps saved out of 85,508 on a mirror row, against 4 to 12 pixels per frame
shifting by up to 3x surface, same hit set, different converged points, because a later start walks a
different sequence of steps. Byte-identity is the instrument that makes every culling number believable (a
too-small enclosure otherwise looks exactly like a correct render) and it is what the three-architecture
boot diff compares, so the trade gives up a verification instrument to collect a saving two orders of
magnitude smaller than the skip already won. A seeded march is still deterministic โ this is not a
determinism question, it is a renderer-policy one, and it should be settled before the GPU path where the
empty approach is a real cost. Found by building the optimisation and measuring it, commit 61491cc.)
OQ-49 addendum (2026-10-12, from the compaction work): the "compaction frequency" candidate was
measured rather than assumed, and it splits. crates/ledger::compact collapses records to one survivor per
voxel and the Merkle pass computes the same digest count in every row at S2's scale (18,489 scattered,
4,096 clustered) โ hashing is priced by distinct voxels, so 12N was always a bound on leaves โ but the
wall time is not flat, because the walk partitions its leaf slice per entry at each of twelve levels: ~1.6x
recovered on a scattered region and ~2.7x on a clustered one at 16 writes per voxel, nothing at 1 or 2. For
the case the gate quotes โ a full slab of distinct edits, where compaction is the identity โ the option is
empty and the floor it exposes is ~13 ms against a sub-millisecond target. Compaction's unconditional win is
capacity (16x fewer records, 16x fewer leaf-scratch entries, same root), which belongs to SPEC-06 and to
P1.4's cold tier. Target, budget and scope remain the live candidates and the decision stays the owner's.
OQ-11 (mod signing and distribution โ without it the sandbox promise is decorative),
OQ-14 (anti-cheat: is client authority over local deltas exploitable?), OQ-18 (latency and
concurrency targets), OQ-31 (exploit economics: mods spawning unbounded entities to force expensive
field evaluation), OQ-34 (WASM permissions and quotas โ a mod enumerating world state is a
memory-exhaustion vector), OQ-35 (concurrent edits to the same circuit input or entity field),
OQ-36 (watch-lists: who may register interest in another region's field, and at what memory cost),
OQ-46 (wire identities and trust: what a PeerID, ActorID, EntityID and ModuleID are, who
issues them, how wide they really are, and which scheme signs an ActionTx. A-08 carries each as a
labelled draft integer and the signature as opaque bytes, because inventing a width or an algorithm
here would be a fake commitment a real scheme breaks โ and doc 10 says the envelope is dominated
by the signature, so the one field that cannot shrink is the one nobody has chosen),
OQ-61 (opened 2026-10-13, because six places in this repo cited a question that does not exist): when two
peers hold different payloads at the same voxel, which one survives? ledger::repair::catch_up settles
the case that needs no rule โ a verbatim-subset lag โ and refuses this one by returning Disagrees with the
position and both payloads. Unowned: the causal rule (append order is per-writer and meaningless across
peers without a tie-break), whose order wins ties, whether the loser's edit is dropped or replayed on top,
and what the loser is told. The six citations pointed at OQ-46, which is wire identities; an unowned
decision is one that never gets made, which is why this row exists.
OQ-04, OQ-05 (UI/HUD, input mapping, accessibility โ specified nowhere), OQ-06 (vertical-slice scope, and whether the SDF look reads as charming or as a blobby demo scene), OQ-07 (save/on-disk layout, corruption, multi-world โ where engines die in production), OQ-16 (physics cost for many entities, LOD physics), OQ-19 (how players learn circuits that don't behave like redstone), OQ-20 (per-module limits and fuel metering), OQ-21 (prompt-to-mod moderation and model choice), OQ-25 (audio tracing cost per voice), OQ-32 (mod unload semantics and state cleanup), OQ-33 (mod version migration windows).
OQ-41 (the agent harness itself: orchestration tool, worktree strategy, a CI runner that can do
headless WebGPU, how prompts and contexts are versioned with the code). Phase 0 settled part of it โ
there is a workflow that runs the Rust, WASM-compile, dependency and doc-integrity gates โ but the
headless-WebGPU runner is still undecided, so spike S6 has no CI job,
Corrected 2026-10-12, against this row's own earlier claim: that running the ledger's roots in WASM
waits on the export-lint decision, because a runner would have to be wit-bindgen output. It does not.
A WASI command exports _start from the Rust runtime, so this repository writes no export and no
unsafe; the engine builds for wasm32-wasip1, runs in CI under a WASI host, and matches the native
build byte for byte (38265695965 on main). A second assumption in the same sentence fell the same
day: a browser has no WASI, so running the boot in a page was assumed to need a function the page
calls โ the export path, and therefore this decision. It needs a host shim instead:
tools/wasi_shim.mjs supplies the host functions the module imports โ derived from the module, after
run 38282729718 taught that a remembered list breaks on whichever toolchain CI happens to use โ
tools/shim_parity.mjs proves that shim
under bare V8 reproduces a native boot byte for byte, and tools/browser_parity_server.mjs has a page's
own WebAssembly API print the same fourteen lines โ one output SHA-256 (15c1e8af1b523d73โฆ) across
native, shim-under-V8 and browser (a4c22a4), repeated on three architectures by CI's
roots in a browser job (38283568760). The three-way choice below still stands for what it was
always about โ a mod exporting functions the host calls โ and what is left of gate P1.5 is a non-V8 wasm
engine plus the named hardware: every browser reachable here and in CI is Chromium, so the engine under
test was V8 each time โ until tools/safari_parity.sh drove the safaridriver that ships with macOS
over plain HTTP WebDriver and pointed Apple-silicon Safari (WebKit/JSC, image macos-26-arm64) at the
same page: run 38297963941 prints OK Safari/JavaScriptCore produced the native digest, the same
15c1e8afโฆ. So this row is back to being about one thing, the export decision for mods, and what is
left of gate P1.5 is the three machines, not the engines.
OQ-43 (public-release hygiene. Licensing half decided 2026-10-11 โ MIT OR Apache-2.0
(LICENSE-MIT, LICENSE-APACHE, license in [workspace.package], and cargo deny now
license-checks the workspace members instead of skipping them), so the "no one may legally use it"
blocker is gone. Naming half still open โ the engine and game have no final names, so
publish = false stays),
OQ-44 (how is "a value a root depends on is integer math only โ no float, no transcendental"
enforced mechanically? Today it is a review rule with no lint. Phase 0's only digest,
lattice_hash(seed: u64, pos: &GlobalVoxelPos) -> u64, is integer-only by signature, so the rule
holds by construction there; the exposure started 2026-10-12, when crates/ledger landed the
BLAKE3 pass: its entry point is node_parts(kind, level, &[&[u8]]) โ bytes in, digest out, with no
type between a caller and a preimage to refuse an f32's bit pattern. The vector tests cover the
digests that exist today and cannot forbid the next one),
OQ-48 (which gate covers the other two hot-reload paths? assetc's watch mode is specified and
partly gated, but only the RON/text-asset half has one โ AS1 (doc 09) and W4 (doc 08). A WGSL shader
edit and a .wasm mod swap are the reloads the source actually promised and neither has a gate, so
while it stays unassigned the dev-profile promise โ "edit a file, see the change" โ is unmet, not
nearly-met).
The conversation states performance facts with confidence. They are hypotheses, each attached to the gate that will confirm or kill it.
| Claim | Confirmed by |
|---|---|
Naively evaluating 500 entities "instantly drops your framerate to single digits"; BVH gives an O(log N) skip |
R1 / S1 |
| "60 FPS with 500 culled entities in scene" โ our combined target; the source never states the pair | R1, S1 |
O(1) or O(log ฮt) time-jump for background automata via closed forms / matrix exponentiation |
S7, SM2 |
"O(1) instant evaluation" of a noise function at any tick |
C-gates + S1 |
| "sub-millisecond batch pass" for Merkle compaction over slabs | P1.3 |
| "GPUs can render millions of voxels per second" with zero disk reads | R1 |
| A voxel edit is a pointer-offset store that "takes nanoseconds" | P1.2 (our number, not theirs) |
| "Zero runtime memory fragmentation, zero GC pauses, absolute zero-copy execution" | P1.2, R2 |
| "Near-zero storage for unmodified chunks" | OQ-13, OQ-23 |
| World size "limited only by disk/hash storage, not RAM or CPU tick speeds" | OQ-23 |
| WASM runs at "~80โ90% the speed of native C/Rust" | S5 |
| A 10-agent swarm "speeds up writing code by 10xโ20x" | not measurable โ treat as rhetoric |
| An agent writes a 400-line SDF entity file "in minutes" vs 3 weeks for an artist | Phase 2 content throughput |
| A from-scratch raymarcher/allocator/compute pipeline costs "2โ3 months of pure low-level debugging" | ADR-026, S1 |
Until a gate measures one of these, it is a quoted opinion from an AI conversation, not data โ and several (single-digit framerates, sub-millisecond compaction) are exactly the kind of claim that dies on first contact with a GPU.
-
Before coding: find your subsystem's rows.
openโ write the decision, not the code. -
After deciding: move the row to
locked, add or amend the ADR, update the subsystem doc โ all in the same commit. -
New questions: append with the next free
OQ-NN. Never renumber or reuse an ID; IDs are cited across every doc and in commit messages. -
Weekly review picks the three highest-leverage
openrows and assigns owners. S7 (OQ-01) and OQ-02 stay on the list until closed.
AS5's mechanism landed (4befc6f), and the gate stays open on purpose. assetc <in.ron> <out.bin> --budget nodes=32,depth=8,evaluations=6144[,steps=96] reads the cost off the compiled records โ nodes,
depth, pool entries, and how many records are unbounded kinds that defeat the ray early-out โ and refuses a
shape on every clause it trips at once. A refusal is treated exactly like a compile failure: nothing is
written, the watch loop's fingerprint does not advance, so a later edit that fits recompiles rather than being
dismissed as unchanged. The per-ray ceiling is doc 04's cost law (steps x nodes), calibrated against the
renderer rather than the algebra: a 25-record tree marched as a real 32x24 frame measured 88 evaluations per
ray where the bound allows 2,400, so 27.3x of headroom in the safe direction. What is missing is the number:
a default budget is a frame-cost claim, and gate R1 has no GPU to price one on โ so AS5 remains open, now one
flag away from enforceable.
Opened 2026-10-13, by the correction to OQ-56.
Gate C1's frozen arithmetic keeps region cells in i64 and so does not bound them, while every coordinate
the renderer touches is an f32 that doc 02 keeps camera-relative and never hashes. An f32 near 2^30 has
a 64-unit ulp, so a cell origin that far out cannot name a voxel at all. The ordering fix in
crates/render/src/stream.rs makes the sampling exact for a camera inside its own cell โ the difference of
two comparable floats is representable โ and cannot make the origin itself finer. Measured consequence
before the fix: 1,280 of 4,096 snout pixels missing at cell 244,140; after it, bit-identical to the same cell
at zero.
Why it matters: R5 promises detail at 1e-6 world units, which is reachable only in a cell whose spacing is far finer than that. The demo's world, a peer's saved game, gate P1.5's browser target and eventually a server that places regions all inherit whichever answer is chosen โ and choosing generously is precisely the silent geometry-loss class of bug this repository has already caught itself shipping once.
The three ways out, none of them this module's call:
- (a) name a maximum render radius (a cell-index ceiling, or a constant in
contracts) and haveplanrefuse rather than draw a wrong frame; - (b) keep
i64cells and lift the far case's origin arithmetic tof64, which costs the hot path and needs its own cross-target story; - (c) declare the deep world out of scope for Phase 1 and record this measured failure as the reason.
The numbers the choice turns on are measured now (examples/deep_zoom.rs, printed by CI on all three
architectures and identical digit for digit): the frame is distance-invariant โ 4,096 of 4,096 hits and a
0.000000 depth delta at every cell from 0 to 2^28 + 1, world 1.0995e12 โ while placement is what fails. An
f32 origin stops being able to name a fraction of a voxel at 2^24 world units (cell index 4,096, ulp 2.0),
and from cell index 2^24 the index conversion itself rounds, wrong by a whole region at 16,777,217. Third
figure, for R5: an f32's ulp crosses 1e-6 near 16 world units, so the gate's tolerance only exists in
camera-relative arithmetic at all. The dangerous property is the silence โ a misplaced region renders
perfectly, so no test on the picture can catch it; only a contract number can.
(a) is a contract number and (b) is a doc-02 amendment, so both belong to the Architect.
Opened 2026-10-13, by two attempts and two reverts.
stream::render_budget spends a ceiling across region cells, measured in
Roadmap and Status. What it cannot do is hand the unfinished pixels to the next
frame โ refinement across frames, the streaming twin of raster::render_bnb_refined, which is what turns a
cropped picture into a transition. The demo's edit-to-picture claim depends on that.
Two findings came out of it, one of them a law rather than a defect:
-
A ceiling below one ray's worst case draws nothing, correctly. The cut is decided against
max_steps + 1before a ray starts, so a budget smaller than that affords a single ray and the frame stays entirely atNOT_DRAWN_*at zero charge. Consequence for callers: specify ceilings in ray charges, not as fractions of a frame โfull/8of a 32ร24 grid is less than one ray, and whoever picks that number will watch a renderer do nothing and conclude it is broken. That was this session's first false alarm. - The second failure is real and narrow. With a four-charge ceiling the refinement ran, and the depths converged while the pixel buffers did not โ which localises the defect to the resume path rather than to the march, and is why the code was reverted rather than shipped with a passing-but-weaker test.
What is missing is a diagnosis watched frame by frame: two frames at a four-charge ceiling in tile-major,
printing not_drawn and decided per frame, and checking whether the resume guard survives Frame's own
sizing (Frame::new hands back zero-length buffers, while the guard requires pixels.len() == rays * 3 and
depth.len() == rays). Reasoning at it has been tried twice; watching it has not.
Step 9b of the boot performs the carry: one streamed frame of the shipped three-cell world is refined at a
ceiling of one ray charge (max_steps + 1 = 97) until nothing is short of, and the settled frame is
asserted in the binary to equal the unbudgeted picture pixel-for-pixel. Measured: 813 frames, cumulative
charge 57,665 โ exactly the single pass's, with 2,260 rays proved empty along the way.
examples/refine_probe.rs turns that into a curve on a one-cell grid whose whole frame costs 876:
| ceiling (charges) | frames to settle | cumulative charge | pixels settled by frame one |
|---|---|---|---|
| sub-ray (0) | never | 0 | 0 |
| 1 | 125 | 876 | 22 |
| 2 | 9 | 876 | 37 |
| 4 | 3 | 876 | 93 |
| 8 | 2 | 876 | 198 |
| 16 | 1 | 876 | 3,072 |
Every settling row charges the same 876, asserted per row, so refinement never costs extra work โ the ceiling buys time to a complete frame, which is exactly what a demo is judged on, and never the cost. 96.0% of that grid is provably free, yet at one charge frame one holds 22 pixels of 3,072: the ceiling is charged before the reachability test, so a frame can only ever buy a couple of rays.
Measured since, which narrows the ask: completion does not depend on the order at all - refine_probe gets 57 frames at 1/8, 3 at 1/2 and 2 at 3/4 in both walk orders, because the cumulative charge is pinned to the single pass's 876 either way. So this is a first-impression question (22 pixels against 1,238 at the tightest ceiling) weighed against re-pinning the prefix property tests/budget.rs guards - not a performance question.
What is decided and by whom. Harvesting proved-empty rays ahead of the charge check is arithmetically
sound โ a ray charged zero cannot breach a ceiling โ and would move those 2,948 pixels into frame one at the
same total work. What it changes is which pixels are drawn at a given ceiling, the prefix property
tests/budget.rs exists to pin, and it must be done identically in raster::budget_walk and
stream::budget_in or budgeted and unbudgeted frames stop being comparable. That is a contract call, not a
refactor: option (a) harvest first and re-pin the prefix tests, or option (b) keep the present
contract โ a ceiling buys budget รท (max_steps + 1) rays, worst case, however cheap they turn out.
rust-toolchain.toml asks for channel = "stable", which is not a version. CI lints with whatever stable its
runner image carries (1.99 as of today); a workstation lints with its own (1.92). A newer clippy is a superset
of lints, and with -D warnings the gap becomes a build failure on the other machine. Twice on 2026-10-13 a
commit that passed every local check red-trunked clippy on all three architectures โ first a chunks_exact_mut,
then a constant-size chunks_exact(3) that the newer lint wants written as as_chunks::<3>().0. Neither was
reproducible locally, which is the definition of a discovery process nobody designed.
Nothing hashed depends on the answer: gates C2/C3/P1.5 are about cross-target behaviour, and the toolchain file says so out loud. So this is a process decision, and it has three shapes:
-
(a) pin an exact stable in
rust-toolchain.toml, so CI and every agent lint the same rule set, and raise it deliberately when it moves โ the only option that makes two machines comparable, and the one that touches the manifest'srust-version = "1.92"claim about the floor the project supports; -
(b) keep
stableand accept the lag, with the mitigation made procedural:tools/local_gate.shruns the trunk checks in CI's order and prints local rustc/clippy against the CI run's, so a differing pair of version lines warns the pusher that CI's lint set is larger; - (c) pin in the workflow and leave the file alone, which hides the difference instead of removing it.
Shipped so far is the procedural half of (b); the call between (a), (b) and (c) belongs to whoever owns the toolchain story, because the honest cost of (a) is that the MSRV line in the manifest stops being a claim about whatever the developer's machine happens to have.
RESOLVED 2026-10-14, owner-approved. The convention is GlobalVoxelPos (x, y, z) naming the half-open cube
[x, x+1) x [y, y+1) x [z, z+1), centre +0.5, and it is now written into doc 02's addressing table as the
single source renderer, collision and base terrain derive from (ADR-021). The measurement below is what made
it a ruling rather than a preference: a centred cube belongs to two regions at once where region_key() says
it cannot. What it unblocks is the edit-to-geometry step, which is still unbuilt.
The question was which world span a voxel coordinate names: the half-open cube [i, i+1) or one centred on
the integer. No doc says, and choosing inside whichever subsystem asks first is the AGENTS.md ยง9 escalation โ so
crates/render/examples/voxel_span_oq50.rs measures both candidates against arithmetic that is already frozen.
Over 28 probe indices covering every region boundary within ยฑ3 regions:
| convention | voxels touching more than one region | disagreeing with region_key()
|
|---|---|---|
A [i, i+1)
|
0 | 0 |
B [iโ0.5, i+0.5)
|
10 | 10 |
region_key() is i >> 12, and its own comment says region 0 is [0, 4096). A centred cube at every
i = kยท4096 therefore has half its volume in the region below the one its own index maps to โ not a style
objection but a contradiction with gate C1.
How visible is the difference? At 2 units a half-unit offset moves 85 of 3,072 pixels for an interior voxel and 365 for a boundary one, worst depth shift under 0.001 units with both frames reporting 484 surface rays; at 32 units it is 4 pixels, at 128 it is 0. So the choice is nearly invisible in the picture and decisive in the arithmetic.
The instrument names no winner: the ruling is the Architect's, and it binds doc 07's collision and any future base terrain as much as the renderer. It runs as a CI informational step โ verified from the runner's log, not the badge โ so the quoted numbers cannot rot.