Skip to content

Descent VTT — Architecture Decision Rulings, Round 1 (R1)

Descent VTT — Architecture Decision Rulings, Round 1 (R1)

Date: 2026-07-31 Basis: the R1 round of adversarial review of Descent VTT Enterprise System Architecture Whitepaper.md (38 findings) Status of this document: the six gate decisions = Accepted; the second batch of ADRs = Proposed (written in full within their respective revision waves) This document does not modify the whitepaper. The whitepaper and its zh-TW parallel version are revised per these rulings in W1–W6.

Writing convention (as originally recorded): ADR field names, identifiers, technical terms and API/library names always remain in English; the body of the argument was written in Traditional Chinese. The §11 Index Line at the end of each ADR is an index sentence ready to paste directly into §11 of the English whitepaper. This document has since been translated in full; the convention is kept here as a record of how it was authored.

⚠️ Finding number namespaces: Audit_Angles_Tracker.md records that the Phase 1 review produced and fixed F-01 through F-47. This round’s (Phase 2) findings are also numbered from F-01, so the two overlap and refer to different defects. The formal namespace for this round’s findings is R2-F-01R2-F-48; every bare F-nn in this document and in W1–W6 always means R2-F-nn. Cross-round references MUST carry the prefix. (R2-F-40R2-F-48 are the results of Design_Docs_Conflict_Scan.md scanning the peripheral design documents.)


0. Ruling Summary

GateTopicRulingADR producedFindings affected
G1Tick delivery topologyconnection registry + targeted silo forwarding; sticky routing demoted to an optimizationADR-032 (supersedes ADR-024)F-01
G2Tick execution modelpipelined, mask one tick behind; mailbox discipline extended to the tick itselfADR-033 (supersedes ADR-018)F-02, F-10
G3Client prediction scopeprediction narrowed to own movement over disclosed geometry; client visibility demoted to presentationADR-034 (amends ADR-017)F-03
G4FOW explored authoritypromoted to first-class T0 state, persisted, branch-keyed, copy-on-writeADR-035 (amends ADR-016, ADR-020)F-04
G5Event cold tiering unitimmutable hash(room_id) partitions; archived per roomADR-036 (supersedes ADR-019)F-19
G6Durability orderingsnapshot seq may never exceed the committed seq; cached snapshots are seq-validated copiesADR-037 (amends ADR-016)F-06

ADR format extension (ruled at the same time): the current format has only Supersedes / Superseded-By. G3, G4 and G6 all “narrow a single clause of an ADR that remains correct”; recording that as supersession would retire a decision that is still right, and not recording it would lose the modification. The format therefore gains an Amends / Amended-By link type (see ADR-045).


ADR-032 — Targeted Silo Forwarding for Tick Delivery

Status: Accepted · Date: 2026-07-31 · Supersedes: ADR-024

Context

ADR-024’s diagnosis is correct and is retained here: a RESP backplane’s group publish sends every message to every silo to be filtered individually — 500 rooms × 20Hz ≈ 10,000 publishes/sec received and deserialized by all 8 silos, the overwhelming majority irrelevant to the receiver, with a single cache container becoming the bottleneck.

Its prescription is not implementable: “sticky-route by RoomId to the silo hosting that RoomGrain” requires the edge layer to know grain placement. Two mutually independent facts defeat it:

  1. ACA replicas sit behind a managed ingress and cannot be addressed individually by an application-level key — the very property §10.1.1 uses to conclude that Cloud Run cannot form a multi-silo cluster.
  2. Even with perfect routing, Orleans placement is dynamic (idle deactivation, scale-in, rebalancing), and the weighted placement director of §4.4 decides destinations independently. Any mapping held by the edge layer is stale by construction.

The consequence as currently written: one grain migration silently switches off the 3D world for an entire room of players, while the grain is healthy, the tick has not overrun, and every HTTP 200 is normal.

Decision

Tick delivery is a two-hop targeted operation within the cluster.

  1. Connection registry: for each RoomId, record the set of SiloAddresses currently holding at least one connection for that room, plus each silo’s connection list. The registry is owned by that room’s own activation (room-scoped state, not a new global service), updated on connect/disconnect/reconnect.
  2. Targeted send: each tick, the RoomGrain produces the per-viewer payloads it was going to produce anyway (§8.2), groups them by owning silo, and sends one message per silo involved, carrying only the bytes for that silo’s viewers.
  3. Local delivery: each silo’s local dispatcher writes the bytes to its local hub connections. No group semantics, no backplane.
  4. Sticky routing is demoted to a pure optimization: sticky ingress by RoomId is retained as a latency and bandwidth optimization, and correctness MUST NOT depend on it. On a hit it is one hop; on a miss or after a grain migration it remains deliverable, at the cost of one intra-cluster forward.

The cost is O(number of silos holding viewers for this room), bounded by min(#viewers, #silos); the worst case for a 50-player room spread across 8 silos is 8 messages per tick, rather than “every message of every room in the cluster received and filtered by all 8 silos”.

Alternatives Considered and Why Rejected

  • A targeted backplane channel (one channel per silo): workable, but it puts Garnet back on the correctness path for room state delivery, violating rule (1) of §10.1.1(2) — “no correctness property may depend on it”. A Garnet restart (which §10.1.1 explicitly calls a routine patching event) would become a room-splitting incident.
  • A self-built addressable gateway inside the cluster owning ingress: solves addressing, but introduces a new always-on layer with its own cold start chain (§9.5 Guardrail 6) and independent failure domain, to solve a problem solvable within the existing cluster.
  • Keep ADR-024, disable placement elasticity, and pin rooms to silos: directly kills the load-aware placement of §4.4, whose reason for existing is real (count-based placement packs every heavy room into the newest replica after a scale-out).

Consequences (including negative)

  • One extra intra-cluster hop in the non-sticky case, which MUST be measured and accounted for in the tick-to-glass p99 budget.
  • Silo↔silo TCP reachability within an ACA Environment is promoted from “a release verification item” to a hard load-bearing dependency. §10.1.1 already lists it as release-gating; now, if Spike S1 shows it is unreachable, Orleans multi-silo itself is unusable, the deployment form degrades to a single silo, and the entire elasticity argument of §10.1 must be rewritten.
  • The connection registry is new state, but MUST NOT be persisted — it is derivable from live connections (each silo re-declares on reconnect), and persisting it would make it a stale fourth authority.
  • Grouping and message construction for 50 viewers now falls explicitly on the tick path, and the snapshot assembly row of §5.2 must absorb that cost (see ADR-033 and W1).

Enforcement

  • Integration test: force a grain migration mid-session and assert every viewer keeps receiving ticks.
  • ArchitectureTests: types on the tick path MUST NOT reference hub group APIs.
  • Runtime metrics: TickDeliveryHops, TickDeliveryFanoutSilos.
  • Extend the existing room-level liveness health check of §10.1.1 to cover the cross-silo case (the current version asserts only that “two sessions in the same room exchange a tick”, which passes within a single silo).

Open

Spike S1.

§11 Index Line

ADR-032: Tick delivery by targeted per-silo forwarding over a non-persisted room connection registry; room-affine ingress demoted to a latency optimisation that no correctness property may depend on. (Supersedes ADR-024.)


ADR-033 — Pipelined Tick Execution; Mailbox Discipline Extended to the Tick Itself

Status: Accepted · Date: 2026-07-31 · Supersedes: ADR-018

Context

ADR-018 correctly drove three classes of work out of the mailbox, while exempting the one that consumes the most mailbox. §4.4 states explicitly that “an activation processes one message at a time — including across await points”; and the three stages of §5.2 are serially dependent (the snapshot’s visibility filtering consumes the mask geometry produces). AdvanceTickAsync therefore monopolizes the mailbox for the duration of its await on the geometry pool: roughly 35ms of every 50ms within budget, approaching 100% under pool contention — player intents cannot be processed, and the symptom is “nobody’s actions respond, but tokens keep interpolating”.

At the same time, §4.4’s “any Grain method with p99 > 5ms is treated as a CI-tracked regression” is violated by the tick method itself by a factor of 4–9, so that rule cannot possibly be enforced by CI as things stand.

Decision

  1. Geometry is fire-and-forget, with results written into a sequence-numbered double buffer. The tick never awaits it.
  2. Tick N uses the most recently completed mask (usually tick N−1’s). Every snapshot carries transformTick and maskTick; §8.2 is revised to explicitly permit maskTick to lag transformTick by one tick (more under Degraded Tick Mode, in which case the current rate MUST be pushed down — see W1/F-36).
  3. AdvanceTickAsync consists of two non-blocking segments (validation + event generation; snapshot assembly + grouping), each with its own budget, and is explicitly exempted from the 5ms Grain method rule with its own SLO instead. The 5ms rule continues to apply to every Grain method on the request path.
  4. ADR-018’s three prohibited work classes are retained verbatim, with a fourth constraint added: no Grain method may await a dedicated pool from inside the mailbox. Dispatch, hold a flag, reply immediately — the tick included.

Alternatives Considered and Why Rejected

  • Inline geometry on the Grain thread and lower the world ceilings: this worsens rather than improves the mailbox occupancy problem, and would require pushing the §5.3 ceilings down far enough to break the established 50-player room target.
  • Split into SimGrain + StateGrain: introduces a second in-memory representation of the same logical entity, violating §2.1.1’s “no fourth source of truth”, and replaces a scheduling problem with a cross-grain consistency problem.
  • Lengthen the tick period to accommodate serial geometry: §5.2 declares 20Hz the derivation basis for the interpolation buffer and the script budget, so this voids the constant underlying three subsystems for the sake of a scheduling problem.

Consequences (including negative)

  • Visibility may lag the transform by up to one tick (50ms), and this is game-visible semantics: an entity may be drawn one tick before the fog that hides it. The mitigation is a rendering rule rather than a netcode rule — the client MUST NOT disclose entities not yet covered by maskTick, i.e. entities are gated on the older of the two ticks. This rule MUST be written into §8.2 and §9.2, and it is the real cost of this decision.
  • The 15ms released is not headroom, but the home for the omitted work listed in W1/F-10 (allowlist recomputation, digest hashing, Effect application), and must be entered into the table alongside.
  • Overrun semantics change: geometry falling behind now degrades into reduced mask freshness rather than blocking the broadcast — strictly better, but “geometry is late” no longer manifests as a tick overrun and needs its own metric, MaskStaleness.

Enforcement

  • ArchitectureTests: the tick path in the Grain assembly MUST NOT await geometry/sandbox pool types.
  • The MaskStaleness metric (in ticks) with an SLO.
  • A CI benchmark asserting the p99 of each AdvanceTickAsync segment.
  • A client integration test asserting the entity gating rule (MUST NOT disclose entities not covered by maskTick).

§11 Index Line

ADR-033: Pipelined tick — geometry is dispatched fire-and-forget and consumed one tick later; snapshots carry transformTick and maskTick; no Grain method may await a dedicated pool from inside the mailbox, the tick included. (Supersedes ADR-018.)


ADR-034 — Prediction Scope Limited to Client-Known Geometry; Client Visibility Is Presentation-Only

Status: Accepted · Date: 2026-07-31 · Amends: ADR-017

Context

The determinism contract of §5.3 is sound as a property of a function, but §9.6.4 expands it into a claim about outcomes (“bit-exact, producing no correction at all”), and the latter requires identical inputs. The authority rules of §9.2 deliberately withhold undisclosed entities and occluders from the client. LOS/FOW is a function of the occluder set, so wherever hidden geometry exists, the same crate necessarily gives different answers on the two hosts — and the resulting correction is an observable, repeatable side channel: a fully compliant client can map an undisclosed secret door to metre precision merely by watching the fog retract. geometry_parity.json is structurally incapable of detecting this, because what it compares is the agreement of the two hosts on identical inputs.

Decision

  1. Client-side LOS/FOW is presentation-only. The client may smooth, upsample, antialias and animate the authoritative mask (the role §9.2 already assigns to WebGPU compute), and MUST NOT disclose cells the server has not disclosed. The same rule now governs both the WASM path and the WGSL path; visibility has exactly one producer, and it is on the server.
  2. Movement prediction is retained: only for the acting client’s own entity, only over geometry that client legitimately holds, and only while holding an Ephemeral Ownership Lease (§5.1.1). Fixed-point determinism remains necessary — it guarantees that “the predicted move” and “the server’s validation of that same move” agree. ADR-017’s determinism clause is narrowed accordingly: what it guarantees is agreement given identical inputs, and the platform guarantees identical inputs only for “one’s own entity moving over disclosed geometry”.
  3. Correction timing MUST NOT carry information. Divergence between prediction and authority is applied at a fixed cadence with uniform visual treatment, so that “how fast the fog retracts” cannot distinguish “something is there” from “nothing is there”.
  4. §12.2.3 is narrowed: any query whose result depends on geometry the caller may not hold MUST NOT return predicted. Vision queries are authoritative-only throughout; calculatePath returns predicted only within areas already disclosed to the caller, and is otherwise authoritative-only.

Alternatives Considered and Why Rejected

  • Full client FOW prediction + disclosure delay: reduces the side channel’s bandwidth without closing it, and trades responsiveness for an object the player does not interact with frame by frame. Fog latency is not input latency.
  • Give hidden occluders to the client and cull client-side: already rejected by §9.2 for the right reason (“a culling shader is one line away from being disabled”), and it upgrades an inference channel into a direct disclosure.
  • Abandon client prediction entirely: discards the input latency property the whole dual-hosted Descent.Geometry design exists to buy, for a problem that only affects visibility.

Consequences (including negative)

  • Fog and lighting boundaries now update at the authoritative cadence (20Hz, plus ADR-033’s one-tick mask lag) rather than at frame rate. This is perceptually acceptable for fog (a slow-moving boundary); for the dynamic light flicker of §5.4.6 it MUST be separately verified, and will very likely need to move wholesale into the presentation layer with the server pushing parameters rather than state.
  • The shared memory arena (Guardrail 3) loses its largest consumer and MUST be re-argued against its remaining consumers (own-movement collision/path preview, mask handoff to the render worker, atlas input), or the toolchain cost Guardrail 3 pays (+atomics builds, F-21’s two wasm artefacts, a hard dependency on cross-origin isolation) is out of proportion to its use. This re-argument is a mandatory deliverable of W5, not an optional review.
  • The headline claim of §9.6.4 (“produces no correction at all”) is withdrawn and replaced with the restricted version; the framing of WASM geometry in §5.3 and §13 Phase 4 MUST be narrowed in the same revision.
  • ADR-017 remains Accepted: one implementation, one language, fixed-point, dual-hosted. Only its “prediction converges” justification is narrowed.

Enforcement

  • Add an adversarial case class to the parity corpus: identical viewer pose, occluder sets differing by exactly one undisclosed occluder; assert the client build refuses to answer rather than giving a different answer.
  • At the type level: vision query APIs expose no predicted member.
  • A red-team test as a release gate: attempt to map a secret door by observing correction timing.

§11 Index Line

ADR-034: Client geometry predicts own-entity movement over disclosed geometry only; client LOS/FOW is presentation-only smoothing of the server mask, and correction timing is scheduled so it carries no information. (Amends ADR-017.)


ADR-035 — Explored FOW Mask Is First-Class T0 State: Persisted, Branch-Keyed, Copy-on-Write

Status: Accepted · Date: 2026-07-31 · Amends: ADR-016, ADR-020

Context

§5.3 forbids masks from entering snapshots on the grounds that they are “recomputable”. Visible genuinely is recomputable; Explored is not cheap — it is a function of the room’s entire reveal history, and its recomputation is bounded by “events since the campaign opened” rather than “events since the last snapshot”, in direct contradiction with the O(1) cold start of §7.1. At the same time, §5.3’s “cold chunk storage” implies masks are persisted, while the branch-keyed derived artefact list of §7.3 omits FOW entirely — so a fork inherits the parent timeline’s explored map, permanently disclosing regions the new timeline never explored.

Decision

  1. The explored mask is T0 state with a persistent representation, keyed (RoomId, BranchId, ChunkId) with its corresponding SourceEventSeq, written in the same transaction as the room snapshot that references it (see ADR-037).
  2. “Recomputation from occluder geometry plus reveal events” is retained as the audit and rebuild path, executed in CI over a fixed corpus — it is the proof that “T1 remains sufficient”, not a runtime mechanism.
  3. A fork is chunk-granular copy-on-write: TimelineForked records the base (BranchId, seq); a chunk is materialized on its first write in the new branch. Branch GC (the 16-branch budget of §7.3) MUST reclaim chunk rows.
  4. Time travel within a branch restores only the relevant chunks, from the nearest chunk snapshot plus that chunk’s reveals, rather than the whole world.
  5. FOW chunk storage joins the branch-keyed list of §7.3, and joins the archival unit of §7.4 (chunks migrate with the room, see ADR-036).
  6. Visible remains purely derived and is never persisted.

Alternatives Considered and Why Rejected

  • Keep masks non-persisted and accept reveal replay: a campaign with 200,000 reveal events would pay tens of seconds on every warm room start, landing on a wait state §9.5 Guardrail 6 labels “a few seconds” and §7.4 reserves for already-archived rooms.
  • Put masks into the room snapshot payload: 1,024 resident chunks reach 16MB per visibility set; the snapshot becomes a large blob whose write cost falls on the deactivation path, and chunk-granular time travel becomes impossible.
  • Treat chunk storage as a pure cache: this is exactly the current ambiguous reading, and exactly the one that produces the fork bug — a cache without a branch dimension silently shares across timelines.

Consequences (including negative)

  • A new storage class and a write path close to the tick (chunk dirty flush). It MUST be batched and budgeted; it is not free, and §5.2 MUST either account for it or explicitly assign it to the geometry pool’s completion handler.
  • Branch storage amplification (already acknowledged by §7.3) now includes mask chunks. The default of 16 branches MUST be re-derived against the 64MB resident ceiling and the multiplicative constraint of F-09 in the same revision.
  • “Explored” now has two representations (chunk rows and reveal events). This is permitted only when one of them is declared derived and verified in CI; the equality check is a release gate, not a comment.

Enforcement

  • CI fixed corpus: rebuild the explored mask from events and assert bit-equality with the persisted chunks.
  • ArchitectureTests: no chunk query may omit BranchId.
  • Fork integration test: assert that on “a branch forked before a region was revealed”, that region reads as Unexplored.

§11 Index Line

ADR-035: Explored FOW masks are persisted, branch-keyed, copy-on-write T0 chunk state written in the snapshot transaction; recomputation from reveal events is retained as the CI-verified rebuild path, not a runtime mechanism. (Amends ADR-016, ADR-020.)


ADR-036 — Room-Scoped Archival on Immutable Hash Partitions

Status: Accepted (in principle) / Proposed (specific mechanism, pending S2) · Date: 2026-07-31 · Supersedes: ADR-019

Context

ADR-019’s reasoning about “why not delete and dump into object storage” is correct, and is retained here in full in spirit: re-inserting events below the async daemon’s high-water mark means they are never projected; inserting above it applies the whole campaign twice.

Its mechanism is not implementable: a PostgreSQL partition key is an immutable attribute of the row, whereas “room activity epoch” is a mutable per-room attribute. If the epoch is fixed at write time, a long-lived room’s events spread across multiple epochs and DETACHing any partition takes away part of a still-active room’s history; if the epoch changes, the partition key must be UPDATEd, which is the DELETE+INSERT ADR-019 exists to avoid.

Decision

  1. mt_events and the Yjs blob table are partitioned on an immutable key: hash(room_id) into a fixed count (starting at 64; that count is a migration-visible constant registered in the quantity registry).
  2. Cold tiering is per room, not per partition: an idle room’s events are batch-relocated to a room-scoped cold structure (a separate table or a partition set on a resident cold tablespace), and that relocation (a) preserves seq_id, (b) verifies read-after-write, (c) retains an overlap window, and (d) is executed by the projection/lifecycle worker (ADR-043), never by a request-layer instance.
  3. Because the relocation does not re-insert rows into the segment of the active logical table below the daemon’s high-water mark, and rehydration restores the same rows with the same seq_id, ADR-019’s invariant survives: every room’s rebuild remains possible forever, and the global high-water mark never becomes invalid.
  4. Rehydration continues to use the per-room projection cursor and directed replay (§7.4) rather than the global high-water mark. Because two progress mechanisms coexist, every projection handling a rehydrated room MUST be idempotent on (stream, version) — previously an implicit requirement, now normative.
  5. FOW chunk rows (ADR-035) and per-viewer diagnostic retention (§10.4) are archived together with the room.

Alternatives Considered and Why Rejected

  • Per-room partitioning: tens of thousands of partitions inflate query planning time and cause catalogue bloat; ATTACH/DETACH on the parent table takes SHARE UPDATE EXCLUSIVE, which conflicts with itself, so concurrent cold-room wakeups serialize.
  • Time partitioning + filtering by room at query time: cheap, but “cold” never corresponds to a detachable unit, and the FinOps goal (moving abandoned campaigns out of hot storage) is entirely unachievable.
  • Marten’s built-in archival/tenant partitioning (if it covers this need): preferred if S2 confirms it, because a first-party mechanism avoids diverging from the Marten daemon’s own assumptions. This ADR is Accepted on the principle of “per room” and Proposed on the specific mechanism.

Consequences (including negative)

  • Batch relocation is real I/O (a COPY-grade operation), and ADR-019 claimed to avoid it. The “no COPY required” claim is withdrawn; what is retained is the stronger property (no re-insertion into the active sequence range), with the cost moved to an offline worker and a verified overlap window.
  • hash(room_id) partitioning offers no locality benefit for cross-room queries; none of the hot paths are cross-room, but reporting queries MUST be re-reviewed.
  • Idempotent projections become a hard requirement, constraining how projections are written (no non-idempotent side effects, such as maintaining a counter by increment).

Enforcement

  • Review gate/ArchitectureTests: every projection declares and tests its idempotency.
  • An archive → rehydrate → rebuild round-trip test asserting projection results are equal.
  • The partition count and hash function are pinned in the quantity registry.

Open

Spike S2.

§11 Index Line

ADR-036: Event archival is room-scoped over immutable hash(room_id) partitions, executed offline with read-after-write verification and preserved seq_id; projections handling rehydrated rooms must be idempotent per (stream, version). (Supersedes ADR-019.)


ADR-037 — Durability Ordering: A Snapshot May Never Lead the Event Log

Status: Accepted · Date: 2026-07-31 · Amends: ADR-016

Context

§2.1.1 declares that “T0 MUST be exactly reconstructible from T1” while providing no mechanism. §7.1 flushes events to Marten in ~50ms micro-batches, while the Grain independently writes a snapshot carrying SourceEventSeq, with no ordering constraint between them. A SIGKILL inside that window leaves a snapshot claiming state at seq X while the log reaches only X−k: the room revives at X, while every rebuild/replay/export produces X−k, and clients have meanwhile been force-corrected to X−k by the DurableSeq handshake — a permanent, silent divergence, in exactly the direction that handshake does not cover, because the server does not know what it forgot.

Separately, the §10.1 topology diagram places “MemoryPack room snapshots” on Garnet, which §2.1.1’s “no fourth source of truth” forbids unless it is strictly a verified copy.

Decision

  1. A snapshot’s SourceEventSeq MUST NOT exceed the highest committed event sequence for that stream. Implemented as: the snapshot is written in the same Marten transaction as the batch that produced its seq, or after that batch’s commit is confirmed.
  2. The deactivation order is normative: flush pending batch → confirm commit → write snapshot → release activation. The 30-second grace period of §4.4 must budget for all three together.
  3. Garnet’s room snapshots are declared a read-through cache of the T1 snapshot, keyed including (BranchId, SourceEventSeq). On reading the cache, an activation validates the seq against the event store’s stream version, and any mismatch or miss falls back to Marten. Cache contents are never written directly from Grain memory.
  4. DurableSeq (§4.4) is restated as bidirectional coverage: a client is never told an action is final before commit, and the server never revives from state it cannot prove was committed.

Alternatives Considered and Why Rejected

  • Have the projection/lifecycle worker build snapshots from the stream instead: eliminates the ordering risk entirely and is architecturally cleaner, but places cold-start readiness behind an asynchronous worker’s lag — a room deactivated and immediately re-entered would find no current snapshot. Retained as the fallback if option (1) proves too costly on the deactivation path.
  • Shrink the batch window to reduce the race: shrinking a race is not eliminating it, and that window varies with load.
  • Accept the window and detect divergence after the fact: no detector is available — the room and the log are each internally consistent.

Consequences (including negative)

  • Deactivation becomes slower and can fail (a failed commit now blocks the snapshot write). A failed snapshot MUST be a non-fatal and metered outcome (the next activation replays a few more events), not a lost room.
  • Snapshot cadence is coupled to batch commit cadence; the interval for periodic snapshots MUST be expressed as “committed sequence distance” rather than wall-clock time.
  • Cache misses after a Garnet restart now always fall through to Marten, so how warm the Neon branch is on the activation path matters more (interacting with F-14/§10.1.1(3)).

Enforcement

  • Property test: kill the host at random points inside the batch window; assert the snapshot seq ≤ committed seq, and that “replay from snapshot” equals “replay from stream”.
  • ArchitectureTests: the snapshot writer type MUST NOT reference the cache client.
  • The SnapshotSeqLag metric (committed seq − snapshot seq), which MUST never be negative.

§11 Index Line

ADR-037: A snapshot's SourceEventSeq may never exceed the committed event sequence; deactivation order is flush → confirm → snapshot; cache-resident room snapshots are seq-validated read-through copies of T1, never written from Grain memory. (Amends ADR-016.)


Second-Batch ADR Ledger (written in full in their respective waves)

The ruling direction for each item below is settled; the complete record is written within its owning revision wave, so that it can be reviewed together with that wave’s section changes.

Completed: ADR-039, ADR-040 and ADR-044 have been written in full in W4_Security_And_Abuse_Revisions.md, with status Accepted (the two sub-items of ADR-040 clause 2 pending Spike S6). The rest remain Proposed.

W1 additions (unforeseen when this ledger was written; written in full in W1_Tick_Budget_Revisions.md, status Accepted):

  • ADR-046 — Effect Premise Validation (Amends ADR-033): seams automatically record (entityId, fieldVersion) premises; a mismatch atomically invalidates the whole Effect buffer and is reported to author and player. Resolves F-11.
  • ADR-047 — Timeline Checkout as a Bounded Out-of-Mailbox Saga (Amends ADR-020, ADR-033): out-of-room construction + atomic swap + token bucket/debounce; node-tree browsing reads T2 rather than replaying. Resolves F-12.
  • ADR-048 — Resident Visibility Memory as a Single Budget (Amends ADR-035): four independent ceilings become one budget + a Visibility Set Grouping Policy + an ordered degradation ladder. Resolves F-09.

The ADR-033 × ADR-037 conflict is resolved: ADR-033 rule 4 forbids awaiting I/O inside the mailbox, and ADR-037 requires snapshots to be written after commit confirmation — literally mutually exclusive. The resolution is “capture on the Grain thread, write out of room”, with the deactivation path as an explicit exception (see W1/D-5). Both ADRs must be cross-annotated.

The §4.4 room weight unit gap is filled: Q-016 (1 room weight = a full-budget room ≈ ⊙0.85 cores, see W1 Table B). This gap was not listed as an R1 finding, but ADR-032’s weighted placement actually depends on it.

W3 additions (written in full in W3_Data_Lifecycle_Revisions.md, status Accepted):

  • ADR-042 — Scope-Typed Query Keys (Amends ADR-020): four scopes expressed as separate repository interfaces, replacing “always require BranchId”. Resolves F-20.
  • ADR-043 — Projection Worker Availability & Sharded Leases (Supersedes ADR-021): minReplicas: 2 + per-shard leases on hash(RoomId) (aligned to the ADR-036 partition count) + blocking lagging writes during a rebuild. Resolves F-26. Mechanism pending S7.
  • ADR-049 — Database Autosuspend Driven by Room-Activation Liveness (new; supplies the ADR §10.1.1(3) lacked): the suspend condition is bound to room liveness rather than DB traffic, the liveness signal travels via the never-suspending clustering branch, and the daemon backs off adaptively so it cannot become an accidental keep-alive. Resolves F-14’s cost-model contradiction (the user-visible symptom is resolved separately by W2).

The partial withdrawal of ADR-019 must be annotated: ADR-036 withdraws its “no COPY required” claim, but retains its core reasoning and attributes it to the correct mechanism (no re-insertion into the active sequence range). It MUST NOT be misread as ADR-019 being rejected in its entirety.

F-25 moves from W5 into W3: multiplied with ADR-037 it produces a hole R1 did not foresee — seq validation cannot detect shape drift. The ruling is to eliminate the second cache shape (the cache stores a byte-identical copy of the T1 payload); see W3/D-6.

W2 additions (written in full in W2_Authority_And_Ephemeral_Revisions.md, status Accepted):

  • ADR-038 — Ephemeral Payload Content Rule (Amends ADR-014): payloads may contain only the sender’s input-derived values and the transforms of entities it holds a lease for, and MUST NOT contain any value computed from the sender’s occluder/visibility set. GM vision cones and lighting previews become local or server-issued. Resolves F-18.
  • ADR-050 — Lease Protocol (Amends ADR-016, ADR-014): timing moves to tick sequence numbers (no clock synchronization needed), grants are pushed immediately + grace buffering, the handback guarantee becomes “no gap, no rewind” and is decoupled from persistence, PeerDriven carries staleAnchor, and lease holders publish coalesced advisory positions. Resolves F-07, F-08, F-39.
  • ADR-051 — Offline Intent Eligibility (Amends ADR-011): a fail-closed offline whitelist; offline spatial movement becomes a local planning layer; long-lived offline leases are forbidden (snapshot suppression would freeze that entity for the whole room). Resolves F-33.

🔴 New finding F-39 (surfaced during W2 design; not one of the original 38): §9.6.3 claims the allowlist is recomputed every tick “so that a token dragged behind cover stops being transmitted within one tick” — the server does not know the in-drag position, because §7.1 routes it entirely around the backend. The mechanism the existing protection describes does not exist. Resolved by ADR-050 clause 7.

The ADR-050 × ADR-037 boundary must be cross-annotated: broadcasting the current T0 ≠ reviving from uncommitted state after a crash. The two rules govern different moments.

§7.1/ADR-012’s “0 requests hit the backend” is partially qualified: one coalesced advisory position uplink per room per tick is a deliberate exception, and it is the premise on which §9.6.3’s allowlist claim becomes true.

W5 additions (written in full in W5_Frontend_Topology_Revisions.md, status Accepted):

  • ADR-052 — Geometry WASM in a single worker + private memory, the shared arena retired (Amends ADR-017, ADR-034; supplies the ADR Guardrail 3 lacked). ADR-034 removed the arena’s largest consumer (FOW), and the remaining consumer is a low-frequency, small-data request/response relationship. This dissolves F-21, F-27 and Spike S3 along with it, and reduces cross-origin isolation from a hard dependency to a frame-skew quality lever.
  • ADR-053 — Authoritative UI state reaches the main thread without the Render Worker (Amends ADR-013): the Network Worker is the single arrival authority and fans out to two consumers; one latch, two sources. Makes Guardrail 1’s Recovering state genuinely achievable. Resolves F-15.
  • ADR-041 — Per-die trajectory index dice (Supersedes ADR-028): a CI-verified offline trajectory table + non-overlapping tray slots, with no seed inversion and no runtime physics solve. Resolves F-22.
  • ADR-054 — Client update contract (Amends §9.6.6; interacts with Guardrail 6): build-id cache keys, the app shell never Cache-First, a pre-handshake version check, and a bounded forced update that first flushes the offline queue and Yjs. Resolves F-23.
  • ADR-055 — Room-level cartridge set compatibility admission (Amends ADR-020): a named warning before the upgrade, pinning to a compatibility environment on a separate ClusterId, and ruleset read-only rather than activation failure at window expiry. Resolves F-24.

§6.1’s “four subsystem degradations” MUST be revised down to one (ADR-052 clause 5). The routing split and CI checks are retained, but the magnitude of their justification must be honestly revised downward.

W6 additions (written in full in W6_Governance_And_Closure.md, status Accepted):

  • ADR-045 — Documentation Governance: adds the Amends/Amended-By link type; every normative sentence MUST name its enforcement point, and one that cannot is demoted to explanatory text; the quantity registry is the single source, and bare numbers MUST NOT appear in the whitepaper.
  • ADR-056 — Parity must verify correctness rather than mere agreement (Amends ADR-017): introduces a test-only high-precision oracle, because identical wrapping arithmetic produces “identically wrong” results that pass a hash comparison. Resolves F-28.
  • ADR-057 — A client-side per-tick applied-state digest (Amends ADR-031), which MUST cover predicted and authoritative state separately, or ADR-034-class divergence remains invisible. Resolves F-32.
  • ADR-058/059/060/061promoted from W3/D-6, W4/D-1, W4/D-2 and W5/D-1 (see below).

The first application of the governance rule caught four gaps in itself: of the 24 section instructions across W1–W5, four in fact modified an existing ADR while being written only as instructions (cache shape → ADR-004; export → ADR-022; key escrow → ADR-022; visual consistency → criterion (b)). These have been promoted to ADR-058 through 061. The other 20 “implement an existing ADR” and correctly remain at the instruction level.

F-37 is closed by the quantity registry; the substance of F-38 is Q-033 (a per-profile VRAM residency budget, currently absent from the entire document) — OPFS streaming does not reduce VRAM pressure, and §9.6.2 has no byte ceiling of the kind §5.3 gives FOW.

§11 has been rewritten as a complete index including Status (see W6): of the 31 pre-existing, 4 superseded and 14 amended; 30 added (032–061).

Open-item dispositions added (in Open_Items_S1_and_BENCH04.md):

  • ADR-062 — Silo-Per-App Topology, status Conditional (in force only on S1-FAIL, otherwise permanently Proposed). One ACA app per silo (single replica, same image, still consistent with ADR-002) obtains a stable internal FQDN; an explicit room→silo assignment table is written by the placement director and read by edge routing, so placement and routing share one source of truth — F-01’s “two components each owning placement” is structurally eliminated. ADR-032’s targeted forwarding is retained as the safety net for the reassignment window.
  • S1 no longer blocks the project: the fallback is fully ruled.
  • ⚠️ W1’s Q-015/Q-016 have been corrected: they originally treated a budget ceiling as a capacity unit, overestimating per-room cost by roughly 40× (an 8-player room expects ⊙0.02 cores rather than 0.85; 500 rooms ≈ ⊙10 cores rather than 425). Two architectural consequences follow — (1) the dominant capacity cost is viewer count rather than world complexity, and the budget weights of the current §5.2 table are the opposite of the actual cost drivers; (2) geometry is cheap as a result of ADR-048’s grouping policy, an ADR that solves memory and computation at once.
  • BENCH priority adjusted: BENCH-04 is demoted to a verification; BENCH-02/03 are promoted to first priority (Segment B’s 5ms budget is the actual bottleneck).
ADRTitleRuling (summary)LinksFindingsWave
ADR-038Ephemeral Payload Content RuleEphemeral payloads may contain only state that is “the sender’s own and already disclosed to every recipient”. Exporting shapes derived from the GM’s privileged geometry (vision cone previews, ruler results snapped to hidden walls) is forbidden. Per-recipient identity filtering cannot, in principle, filter derived values.Amends ADR-014F-18W2
ADR-039External & Player-Supplied Content Is a Trust Tier; Isolated Edge Fetch ServiceThe §6.4 trust ladder gains a P4 — External / Player-Supplied Input row (that table currently states explicitly that “there is no fourth row”; this decision is the amendment to that sentence). The Edge Proxy becomes an egress-only worker with no managed identity and no internal VNet reachability, admitting on “the resolved IP must fall in the public unicast range” (re-resolved after every redirect), with enforced size ceilings, a per-account token bucket, and image decoding performed inside that worker under resource ceilings.Amends ADR-010; new tier rowF-05W4
ADR-040Client Plugin Resource BudgetsMirrors §4.3’s budget model onto P2: a per-plugin QuickJS runtime, per-frame time slicing implemented with an interrupt handler, a per-runtime heap ceiling, disabling on overrun with the offending plugin named, and plugin:deferred reporting.Amends ADR-010F-17W4
ADR-041Per-Die Trajectory Index DiceAbandons “one seed determines the whole tray”. The server picks a trajectory index per die from a pre-built table (die type × face × K verified trajectories) and assigns non-overlapping landing points; the wire carries the trajectory index and the landing index. No physics inversion, no combinatorial explosion.Supersedes ADR-028F-22W5
ADR-042Scope-Typed Query KeysReplaces “always require BranchId” with Scope = Campaign(BranchId) | TimelineIndependent | Licence(sourceId). The repository layer requires every query to declare its scope. Resolves the contradiction that flattened Yjs text and licensed corpora cannot carry a BranchId.Amends ADR-020F-20W3
ADR-043Projection Worker Availability & Sharded LeasesminReplicas: 2 (one active, one standby); projections are sharded into independent lease units so a rebuild can hold some shards exclusively without starving real-time projection on the rest; rebuilds are rate-limited and pausable; during a rebuild, affected read models take a write lock rather than merely displaying a lag marker.Supersedes ADR-021F-26W3
ADR-044Verifiability Scope for Commit–RevealWhat is publicly verifiable is the raw random byte stream (anybody can recompute it from serverSeed + nonce); the bytes→game-outcome mapping is published by the server as data in the audit record (input bytes, thresholds, outputs), so a dispute can be checked by a third party without publishing the algorithm. serverSeed rotates per roll.New (§4.1 previously had no ADR)F-29W4
ADR-045Documentation Governance: Amends Link Type, Enforcement-Point Rule, Quantity Registry(1) The ADR format gains Amends / Amended-By; (2) every normative sentence MUST name its enforcement point (ArchitectureTests / CI lint / runtime metric / release checklist — one of four), and one that cannot MUST NOT be written into the document; (3) the quantity registry: all quantified numbers are consolidated and given IDs, the text cites only IDs, and a CI lint detects bare numbers.New (meta)F-37 and document-wide symptomsW6

Remaining items revised directly by their waves, requiring no standalone ADR: F-07, F-08, F-10, F-11, F-12, F-13, F-14, F-15, F-16, F-21, F-23, F-24, F-25, F-27, F-28, F-30, F-31, F-32, F-33, F-34, F-35, F-36, F-38.


ADRs Left Unchanged (explicitly untouched)

The following decisions were judged correct on review, are not modified this round, and the alternatives they rejected MUST NOT be quietly revived in later waves:

ADRWhy it stands
ADR-002Modular Monolith rejecting microservices/K8s. ADR-032’s targeted forwarding holds within a single horizontally scalable deployment and is not grounds for microservices.
ADR-010“Third parties deliver only data, never code reaching the main thread”, plus the explicit declaration that Shadow DOM is not a security boundary. ADR-039/ADR-040 only extend its ladder and resource dimensions, leaving the core rule untouched.
ADR-017One geometry implementation, fixed-point, dual-hosted. Only its prediction convergence argument is narrowed, by ADR-034.
ADR-023AOI entry requires a per-entity baseline; every “Full Snapshot” is per-viewer filtered.
ADR-026Yjs must have a server relay fallback (user-authored content MUST NOT depend on a single transport). W2/F-16 only supplies the carrying component and rate limits, leaving the principle untouched.
ADR-029Rejects WebTransport, demoting it to a Phase 5 measurement spike. All three reasons hold.
ADR-031Day-2 operability requirements. W6/F-32 only supplies the client-side digest counterpart it lacked.

Spike List (blocking items)

IDExperiment/investigationBlocksIf the result is negative
S1silo↔silo TCP reachability inside an ACA Environment; whether ACA ingress offers any per-replica addressing. The specification and pre-registered criteria for all four outcomes are in Open_Items_S1_and_BENCH04.mdFinalization of ADR-032§10.1 is no longer rewritten: instead, ADR-062 is activated (silo-per-app, one stable internal FQDN per silo + an explicit room→silo assignment table). Only if the control group also fails (S1-FAIL-HARD) does the platform choice need re-examining
S2Marten’s current support for custom partitioning/archival of mt_eventsThe specific ADR-036 mechanismSwitch to a custom schema, and verify the interaction with the daemon’s high-water mark detection ourselves
S3two workers executing the heaviest FOW simultaneously over the same shared linear memoryDissolved: ADR-052 removed the mechanism (single worker + private memory), so there is nothing left to verify
S4The intermediate widening strategy on the Descent.Geometry hot path; comparison against a high-precision reference implementationF-28 / W5The parity corpus is upgraded to “agreeing and correct”, requiring an independent reference implementation
S5FontFace.load() support inside a worker on Safari/FirefoxF-35 / W5A new Guardrail 7 row, whose absent behaviour is world-space labels falling back to the §9.6.5 DOM path
S6The trigger granularity of QuickJS-in-WASM’s interrupt handler; whether per-runtime memory ceilings are exposed through the chosen bindingADR-040 clause 2Replace with “a per-frame invocation budget + after-the-fact detection and disabling”, and acknowledge in the document that this is a weaker guarantee
S7Whether Marten’s async daemon supports multiple instances within one database, scoped to subsets of streams and each advancing independentlyADR-043 clause 2Implement sharding as “N daemon processes with stream filters”, or degrade to “a single daemon + independent leases for the remaining sweeps” (which still resolves the availability gap and sweeps blocking one another, but does not resolve rebuild isolation)

S1 and S2 block finalization of W1/W3; S3–S5 block finalization of W5; S6 blocks finalization of ADR-040’s preemption mechanism (its other clauses are unblocked). The rest of W4 is blocked by no spike.


Traceability Matrix (finding → ruling → wave)

FindingSeverityResolved byWave
F-01🔴ADR-032
F-02🔴ADR-033
F-03🔴ADR-034landed in W2
F-04🔴ADR-035landed in W3
F-05🔴ADR-039W4
F-06🔴ADR-037landed in W3
F-03🔴ADR-034 + the W2 landing instruction tableW2 ✅
F-07🟠ADR-050 clauses 4/5W2 ✅
F-08🟠ADR-050 clauses 1/2/3/6W2 ✅
F-18🟠ADR-038W2 ✅
F-33🟡ADR-051W2 ✅
F-39 (new)🔴ADR-050 clause 7W2 ✅
F-09🟠ADR-048W1 ✅
F-10🟠The §5.2 Table A/Table B rewrite + D-2, D-3, D-4W1 ✅
F-11🟠ADR-046W1 ✅
F-12🟠ADR-047W1 ✅
F-36🟡D-1 (renamed Degraded Fidelity Mode; the 20Hz snapshot cadence is no longer variable)W1 ✅
F-13, F-17, F-29, F-30, F-34🟠/🟡Text revision (ADR-040, ADR-044)W4
F-04🔴ADR-035 + D-1, D-2W3 ✅
F-06🔴ADR-037 + D-4W3 ✅
F-14🟠ADR-049 (the cost model) + W2 (the user-visible symptom)W3 ✅ / W2
F-19🟠ADR-036 + D-3W3 ✅
F-20🟠ADR-042W3 ✅
F-25🟠D-6 (eliminating the second cache shape)W3 ✅
F-26🟠ADR-043W3 ✅
F-15🟠ADR-053W5 ✅
F-21🟠ADR-052 (mechanism retired, finding dissolved)W5 ✅
F-22🟠ADR-041W5 ✅
F-23🟠ADR-054W5 ✅
F-24🟠ADR-055W5 ✅
F-27🟡ADR-052 clause 3 (mechanism retired, finding dissolved)W5 ✅
F-31🟡D-1 (semantic consistency replacing visual consistency + a new matrix row)W5 ✅
F-35🟡D-2 (a two-stage subset font + a new matrix row; pending S5)W5 ✅
F-16🟠W3/D-5 (the relay carrier + authorize once, relay many times + rate)W3 ✅
F-28🟡ADR-056 (introducing an oracle so parity verifies correctness; details pending S4)W6 ✅
F-32🟡ADR-057W6 ✅
F-37🟡ADR-045 quantity registry + CI lintW6 ✅
F-38🔵Motivation restated + Q-033 per-profile VRAM residency budgetW6 ✅

Next Actions

  1. Start W4 and S1–S5 immediately and in parallel (W4 has no gate dependencies; spikes have lead time).
  2. G1/G2 settled → W1 can begin (the §5.2 budget table rewrite is the single largest change).
  3. G3 settled → W2 and W5 can proceed in parallel; W5 MUST include the “re-argument of the residual value of the Guardrail 3 arena” that ADR-034 requires.
  4. G4/G5/G6 settled → W3.
  5. W6 closes out: completing the ADR format (not one entry in the current §11 index has a Status field), the quantity registry, and zh-TW synchronization.
  6. To be confirmed: if Audit_Angles_Tracker.md is the existing audit-angle tracking table, this round’s 38 findings and six gates should be registered in it, so the next round does not re-review the same angles.