Skip to content

Descent VTT — Quantity Registry

Descent VTT — Quantity Registry

Date: 2026-08-01 Mandated by: ADR-045 (Documentation Governance), clause 3 Status: This is the single source for every quantity in the system. Numbers in the whitepaper and in the revision documents cite a Q-ID rather than restating a value.

This table supersedes the scattered versions in W1_Tick_Budget_Revisions.md (Q-001…019) and W6_Governance_And_Closure.md (Q-020…035). Those tables are retained as the derivation record for their own rulings, but where the two disagree, this file wins.


Why this table was created three months after R2 closed, and what that fact means

ADR-045 requires in as many words that every quantity be consolidated into a single registry. What actually happened: W1 established Q-001…019, W6 appended Q-020…035, and the correction to Q-015 / Q-016 was written in a third document (Open_Items_S1_and_BENCH04.md).

Three documents, each internally correct, and the “single source” property never held.

This is exactly the shape of F-37 — whose cause was recorded as “numbers are consolidated but restating them in prose is permitted”, while what actually happened is more basic: the numbers were never consolidated at all; each ruling simply opened a new table inside its own file. The concrete consequence, found while building this table, is that W6’s closing table still claimed BENCH-04 decided Q-016 — when Q-016 had long since been settled by analytical recomputation and BENCH-04 demoted to a verification (corrected 2026-08-01).

A registry that is mandated to exist does not exist until a file actually bears that name.


Status markers

MarkerMeaning
InvariantAn architectural constant. Changing it requires an ADR, and overturns every entry derived from it
NormativeSet by a ruling. Changing it requires amending that ADR
DerivedComputed from other entries. The formula must be recorded — without one it is a normative value wearing the costume of a derived one
⊙ EstimatedDerived from an assumed unit cost and not yet measured. Must not be marked normative until the corresponding benchmark completes
ExistingCarried over from the source document; not re-examined by this round of rulings
PendingIdentified as needing a bound, with the value not yet decided. The existence of this row is itself the deliverable — it separates “undecided” from “unbounded”

Part 1 — Tick and simulation budgets (Q-001…019)

IDNameValueSourceStatus
Q-001TICK_PERIOD_MS50 (20Hz)§5.2 architectural constantInvariant
Q-002MAILBOX_OCCUPANCY_TARGET_MS28 per 50ms windowW1 Table A⊙ Estimated
Q-003TICK_MSG_P99_MS8 (Segment A ≤3, Segment B ≤5)W1 Table A; measured 2026-08-13 (BENCH-01, BENCH-03) — see “The measurement rig’s first results”Normative
Q-004REQUEST_GRAIN_METHOD_P99_MS5§4.4 (the tick is explicitly exempt, ADR-033)Existing
Q-005INTENT_AGGREGATE_MS15 per windowW1 Table A ← Q-008⊙ Estimated
Q-006HOUSEKEEPING_AGGREGATE_MS5 per windowW1 Table A⊙ Estimated
Q-007EFFECT_APPLY_MAX_PER_TICK1,000ADR-046 ← Q-003; measured 2026-08-13 (BENCH-01): 0.67 µs per Effect against an assumed ⊙3 µs, so 1,000 costs 0.670 ms of Segment A’s 3 msNormative
Q-008PLAYER_INTENT_RATE10/s sustained, 20 burstW1 / D-4 ← Q-005⊙ Estimated
Q-009RESIDENT_VISIBILITY_BYTES64 MiB per activationADR-048Normative
Q-010FOW_CHUNK_BYTES16 KiBDerived: (Q-011 chunk edge / cell edge)² × 2 bit = 256² × 2 / 8Derived constant
Q-011CHUNK_EDGE_M / CELL_M256 / 1§5.3Normative
Q-012DYNAMIC_OCCLUDER_MAX512 256 (lowered 2026-08-02)§5.3 (an authoring-driven hard cap that may refuse); the reason for the reduction is in the “Fog cadence ladder” sectionNormative
Q-013CHECKOUT_RATE / CHECKOUT_DEBOUNCE_MS1 per 2s per room / 400ADR-047Normative
Q-014MASK_STALENESS_SLO_TICKSp99 ≤ 1; > 3 triggers Degraded FidelityADR-033, W1 / D-1⊙ Estimated
Q-015aGEOMETRY_CEILING_MS_PER_ROOM_SEC300W1 Table B (an isolation ceiling — reaching it means the room is pathological; record and degrade)Normative
Q-015bGEOMETRY_EXPECTED_MS_PER_ROOM_SEC⊙3 refuted 2026-08-02Analytical recomputation (for capacity planning) → see the measured curve in the “Geometry cost” section⊙ refuted by measurement; the value awaits the reference room’s occluder count
Q-016ROOM_WEIGHTExpected cost by viewer count: 8 viewers ⊙0.02 cores, 50 viewers ⊙0.08 cores; admission applies a ⊙3× safety factor. Its COMPOSITION is refuted 2026-08-13 — see belowAnalytical recomputation (supplies the weight unit §4.4 lacked)⊙ composition refuted by measurement; the value awaits a measured fog-budget utilisation
Q-017SCRIPT_CPU_SOFT/HARD_MS, SCRIPT_HEAP_MB, SCRIPT_AGGREGATE_MS5 / 20 / 10 / 25§4.3Existing (host runtime configuration per §3.1; must not ship as an open-source constant)
Q-018INTERP_BUFFER_MS30 (fibre) – 150 (unstable)§8.3Existing
Q-019CHUNK_FLUSH_PER_ROOM_SEC50W1 Table B, ADR-035⊙ Estimated

The Q-015 / Q-016 correction, retained because it is the most important lesson in this table. The old definition treated GEOMETRY_POOL_MS_PER_ROOM_SEC = 300 as a capacity estimate and derived ROOM_WEIGHT_UNIT ≈ 0.85 cores per full-budget room from it. A budget ceiling is not a capacity unit — a ceiling is an isolation mechanism describing a room that will not occur. Using it as a capacity unit over-provisions hardware by roughly an order of magnitude and makes §10.2’s cost model unusable. See Open_Items_S1_and_BENCH04.md.

Q-016’s composition is refuted by measurement (2026-08-13, ADR-177), and its total is not re-derived. The recomputation that produced ⊙0.02 / ⊙0.08 cores stated that “Segment B accounts for ⊙95% of the expected cost in a 50-player room”. BENCH-03 measures Segment B at 0.675 ms per tick = 13.5 ms per room-second = 0.0135 cores at 50 viewers — which is 17% of the ⊙0.08 total, not 95%. Meanwhile the fog token bucket admits 6 advances per room-second at Q-075’s 44 ms, so the geometry term is bounded at 264 ms per room-second = 0.264 cores, nineteen times Segment B at its bound.

So the driver is geometry and the composition was inverted. That reverses §5.2’s sentence “The dominant cost is viewers, not world complexity … per-viewer snapshot assembly outweighs geometry by more than an order of magnitude” — measured, geometry outweighs per-viewer assembly by roughly the same margin in the other direction. Half of that reversal was already visible on 2026-08-02, when the fog-advance measurement refuted Q-015b’s ⊙3 ms and nobody re-read the sentence that rested on it; this round supplies the other half and the sentence is corrected through ADR-177 rather than in passing.

The TOTAL is deliberately not re-derived, and the reason is the one Q-015b already carries. The geometry term is an admitted allowance, not an expected cost: a room where nobody moves advances nothing, and a room at the bucket’s ceiling pays 264 ms. Turning that into an expected value needs a measured fog-budget utilisation from real play, which does not exist. Publishing a number derived from the bucket’s ceiling would repeat the exact error the paragraph above records — a budget ceiling is not a capacity unit — one correction later. Q-016 therefore keeps its ⊙ and loses its explanation.


Part 2 — Data lifecycle, security and frontend (Q-020…035)

IDNameValueSourceStatus
Q-020EVENT_PARTITION_COUNT64 (hash(room_id))ADR-036Normative (coupled to Q-021)
Q-021PROJECTION_SHARD_COUNTDerived: = Q-020 (deliberately aligned, so one shard’s rooms fall in the same partition)ADR-043Derived
Q-022WORKER_MIN_REPLICAS2ADR-043Normative
Q-023NEON_KEEPALIVE_INTERVALDerived: < autosuspend window / 2ADR-049Derived
Q-024EDGE_FETCH_MAX_BYTES / _REDIRECTS / _RATEPending / 2 / per-account token bucketADR-039Normative (the byte ceiling is pending)
Q-025DECODED_PIXEL_MAXPendingADR-039 (a decompression bomb has a small file size, so a byte ceiling does not catch it)Normative (pending)
Q-026PLUGIN_FRAME_AGGREGATE_FRACTIONPending (a fraction of the frame budget)ADR-040⊙ Estimated (awaiting S6)
Q-027PLUGIN_INTEREST_SET_MAXPendingADR-040Normative (pending)
Q-028LEASE_EXPIRY_TICKS / RENEW_INTERVAL_MS40 ticks (= 2s, expressed in tick sequence rather than wall clock) / 500ADR-050Normative
Q-029PEER_GRACE_BUFFER_MS150ADR-050 clause 3Normative
Q-030DICE_ANIMATED_MAX20ADR-041Normative
Q-031DICE_TRAJECTORY_VARIANTS_KPendingADR-041Normative (pending)
Q-032FONT_SUBSET_GLYPHS~3,000 (common zh-TW glyph set)W5 / D-2⊙ Estimated
Q-033VRAM_RESIDENT_BUDGET_BYTESPending (per profile)F-38 correction, §9.6.2⊙ Estimated (currently absent entirely — OPFS streaming does not reduce VRAM pressure, so no value here means the protection does not exist)
Q-034EXPORT_CONCURRENCY_PER_ACCOUNT = 1 (abuse bound, decided 2026-08-06, ADR-105) / _DAILYPendingADR-059, ADR-087Normative (pending, blocked on the product’s entitlement-tier model, not on measurement — see note below)
Q-035DIAGNOSTIC_SAMPLE_RATEPending (raised when a room is flagged)ADR-057Normative (pending)

Part 3 — Entries registered when this table was created (Q-036…050)

These numbers were always in the whitepaper but never carried a Q-ID. They fall into two classes: those with a definite value (Q-036…046), and those the document already claims are bounded while the value was never decided (Q-047…050). The second class matters more — listing them is the only way to separate “undecided” from “unbounded”.

With a value

IDNameValueSourceStatus
Q-036WORLD_EXTENT_M8,000 × 8,000§5.3Invariant (decides whether Q-043’s coordinate space is sufficient)
Q-037COORD_FORMATi32.16 fixed point; intermediates widened to i64/i128§5.3 Determinism Contract, ADR-017Invariant (changing it overturns bit-for-bit equality across hosts)
Q-038BRANCH_BUDGET_PER_ROOM16 (live branches, including the GC policy for unreferenced branches)§7.3, ADR-020Normative
Q-039EPHEMERAL_PUBLISH_RATE_HZ60 (≤ 6 peers) / 20 (≤ 16) / 10 (more)§9.6.3, ADR-012Normative (60Hz is a ceiling, not a constant — SFU fan-out is O(N) per sender and O(N²) per room)
Q-040OPFS_LRU_DAYS30§9.6.2Normative
Q-041ASSET_CHUNK_BYTES512 KBDerived: sized to §9.6.1’s frame budget rather than to convenienceDerived
Q-042SHUTDOWN_GRACE_S30§4.4Normative (must cover all three steps: flush → commit confirmation → snapshot write, ADR-037)
Q-043MICRO_BATCH_WINDOW_MS~50§7.1Existing (numerically equal to Q-001 and semantically unrelated — this is the window of events that may be lost under SIGKILL, not the tick)
Q-044TOUCH_TARGET_PT44§9.7.2Existing (platform HIG; drives the offset-proxy drag design, ADR-066)
Q-045C_COMPANION_VIEWPORT_IN⊙8 (effective viewport diagonal; below this is C-Companion)§9.7.1, ADR-066⊙ Estimated (awaiting measurement on real devices — the criterion is whether the tactical map and the character sheet can be shown at once, not the screen size itself)
Q-046CARTRIDGE_DEPRECATION_RELEASES2 (v2.9 deprecate → v3.0 remove)§7.5.4, ADR-071Normative

Claimed to be bounded, value pending

IDNameSourceWhy it must have a value
Q-047HW_DECODE_SESSIONS_MAX§9.4§9.6.3’s dice-animation cap (Q-030) cites in as many words “the same concurrency-cap pattern as §9.4’s hardware decode sessions” — and the value cited as the precedent has no value itself
Q-048ARCHIVE_OVERLAP_WINDOW§7.4, ADR-036Cold-tier migration “retains an overlap window before reclaiming locally”. The window length decides whether a failed verification is recoverable — too short and the source data is already gone when the checksum disagrees
Q-049CHECKOUT_CONCURRENCY_PER_ACCOUNT = 2 (abuse bound, decided 2026-08-06, ADR-105)§7.3, §10.3 face 6, ADR-047, ADR-087Listed explicitly as an EDoS face (“one account scripting tree traversal across fifty rooms”). Q-013 bounds the per-room rate and does not catch cross-room amplification
Q-050OFFLINE_QUEUE_MAX / _TTL§9.5 Guardrail 4, ADR-011 / 051“The offline queue is bounded in count and age; on overflow the client refuses further optimistic actions.” Without values that rule is unenforceable — and it exists to prevent accumulating a divergence too large to reconcile meaningfully

Added by the R3 rulings (Q-051…053)

IDNameSourceStatus
Q-051ROOM_DORMANT_AFTER_MSADR-072 (the grace period a room stays Active after the last client backgrounds)Normative (pending)
Q-052KEEPALIVE_AFTER_DORMANT_MSADR-072 (how long keep-alive continues after entering Dormant)Normative (pending)
Q-053CHECKOUT_CANDIDATE_TTL_MSADR-075 (how long candidate state is retained)Normative (pending)

Q-034 and Q-049 are blocked on a commercial decision, not on measurement (ADR-087). A convention organiser, a play-by-post organiser with fifty rooms, and an account systematically scraping campaigns produce identical telemetry. Set the bound where the organiser needs it and the scraper is unbounded; set it where the scraper stops and the organiser breaks mid-event. A quota that bounds legitimate high volume and abuse with one number encodes a compromise in a constant nobody will later recognise as one, so these two stay pending until the product’s entitlement-tier model exists. Cost containment does not wait: the existing per-account bounds continue to protect the bill. Where a bound must exist before the tier model does, it is recorded here as a cost bound and never as an abuse threshold — a bound doing one job must not be documented as doing two.

R4 rulings (Q-054…055) — added 2026-08-01, in English per the convention change

IDNameSourceStatus
Q-054ERASURE_COMPLETION_DEADLINEADR-078 clause 5 (time from an accepted erasure request to the last derived artefact being cleared)Normative (pending)
Q-055IDENTITY_BACKUP_RETENTION_MAXADR-078 clause 5 (retention ceiling for backups of the keystore and the linkage store)Normative (pending)

Enforcement-point ruling (Q-056) — added 2026-08-02

IDNameSourceStatus
Q-056SIGNALR_MAX_MESSAGE_BYTES32,768Measured 2026-08-02; see below

Q-056 was unset, and 32 KB was specifically rejected as a starting value. It bounds blast radius, not routing — size and routing are independent, and a 5 KB snapshot published through Clients.Group(...) fans out to every silo exactly as a large one would, so this is not the control that enforces §8.3. Its unblocking measurement was the per-viewer Full Snapshot size distribution, because a cap set below it would break the delivery path ADR-090 exists to protect.

That measurement now exists, and it returns the same number that was rejected. SnapshotSizeMeasurementTests, 2026-08-02: 2,000 actors over 400×400 units, 500 viewers, AOI radius 60 units.

measureentitiesbytesheadroom at Q-056
p501315,3086.2×
p991807,2684.5×
max1847,4284.4×
§5.2’s reference interest set2008,0684.1×

Re-measured 2026-08-09, when DisclosedEntity gained elevation and facing. The previous row set — 4,260 / 5,828 / 5,956 / 6,468, at 7.7× / 5.6× / 5.5× / 5.1× — was taken against a 32-byte struct and is superseded rather than wrong: same room, same seed, same 500 viewers, eight more bytes per entity. Q-056 itself did not move. The cap bounds blast radius and nothing in this measurement argues about that; what moved is the derivation underneath it, which is the part a reader builds on.

The rejection was still right, and this is the point of it. 32 KB was refused as an unmeasured guess; it arrives now as a measured one, and the two are different facts that happen to share a value. A cap adopted on the earlier reasoning would have been correct by luck, and nothing would have distinguished it from the version of that guess that was 4× too small.

Derivation. One DisclosedEntity is 40 bytes on the wire — Uuid (two ulongs, 16 B) plus four int32s (raw_x, raw_y, raw_z, rotation) plus a bool, padded to the struct’s 8-byte alignment — and FlatBuffers stores structs inline, so a snapshot is 68 + 40n bytes for n entities. Q-056 therefore admits 817 entities, or 4.1× §5.2’s reference interest set.

The struct was 32 bytes and 68 + 32n until 2026-08-09, when elevation and facing were added to the transform. The eight bytes were accepted into the tick payload rather than decoupled onto their own cadence, and that is a decision worth recording because ADR-092 makes the opposite call for attributes a few lines below. The test is cadence, not size: a facing changes exactly when a position changes — they are one transform, produced by one gesture — whereas a stat block changes when a stat changes and a fog chunk on its own ladder. Splitting a token’s orientation onto a second message would mean two packets per move, each arriving on a different tick, and a client interpolating a position toward one answer while rotating toward another. The 128× term ADR-092 excluded is not present here: this is 1.25×, it is bounded by the same n, and it does not grow with cartridge content.

This bound holds only because cartridge attributes are out of band. Ruled 2026-08-02: the tick-driven snapshot carries transform state only, and a cartridge’s MessagePack payload travels as a separate, event-driven message sent when a stat changes or when a client opens a sheet. Measured against the alternative, the reference room’s snapshot is 827,268 bytes — every entity carrying Q-060’s 4,096-byte ceiling — which at 20Hz is 16.5 MB/s per viewer and ends ADR-014’s bandwidth budget outright. §8.1 previously implied the payload rode in the packet; it does not. Q-056 and that ruling are one decision: raising the cap without re-examining what the packet carries would restore the 128× term it exists to exclude.

Enforcement point: Descent.Vtt.Application.Rooms.PerViewerSnapshot, whose wire form is schemas/snapshot.fbs, with the distribution pinned by SnapshotSizeMeasurementTests.

Still derived rather than measured, and the difference is recorded: the entity count is measured, but the bytes are computed from the schema’s layout because flatc is not yet wired into the build and no serializer exists to weigh. The figure becomes a measurement when it does, and it is not expected to move — the layout is specified, not estimated.

Q-055 must be ≤ Q-054, and this is the only coupling in this table that is a correctness constraint rather than a tuning one. A backup of the linkage store that outlives the erasure deadline can be restored, and restoring it resurrects exactly the record erasure destroyed. Set Q-055 above Q-054 and ADR-078’s entire mechanism becomes decorative while continuing to report success — the erasure job completes, the metric goes green, and the data comes back on the next restore drill. This is the least architectural clause in ADR-078 and the easiest to violate without noticing.

Q-051 and Q-052 must not be tuned independently. It is their sum that a user actually experiences as “how long you can step away before being penalised”: too short a Q-051 and everyone glancing at their phone at once interrupts the tick, too long and backgrounded rooms idle; too short a Q-052 and Neon resumes repeatedly, too long and ADR-049’s cost saving does not hold. They are registered as two entries because they act on different mechanisms, not because they can be decided separately.

Geometry pathfinding bounds (Q-058…059) — added 2026-08-02, in English per the convention change

Q-057 is intentionally unassigned; these two IDs were allocated by ruling.

These are the remaining two arguments of descent_geometry_evaluate (Q-012 is the third). §3.1 requires all three to be host-supplied runtime configuration and never fields a caller can set — a four-byte budget field in the request was a real defect, and the wire_codec fuzz target turned it into a request for four billion node expansions from a packet-sized input.

IDNameValueSourceStatus
Q-058PATH_EXPANSION_BUDGET4,096Measured; see derivation belowNormative
Q-059PATH_REGION_CELLS_MAX65,536Derived: one chunk = Q-011 256 × 256Derived

Q-058’s derivation, measured 2026-08-02 (GeometryCostMeasurementTests, release-profile crate at submodule 8b6a23c). The worst case for A* is an unreachable goal, because the search must expand everything it is allowed to before it can say so; a successful search stops early and says nothing about what an attacker can provoke. Over a full Q-059 region:

path_budgetWorst caseShare of a tick (Q-001)Outcome
4,0966.1 ms12%PATH_BUDGET_EXHAUSTED
16,38425.0 ms50%PATH_BUDGET_EXHAUSTED
65,53649.4 ms99%region exhausted before the budget bound

The two limits bound different things and neither is redundant. Q-059 is checked before any allocation and bounds the extent a request may name; Q-058 bounds the work actually done inside it. Leaving Q-058 at or above Q-059 makes it inert — the region is exhausted first — and a single query then costs a whole tick. 4,096 is chosen so the pathological case costs ~2% of Q-015a, which is what lets a rate limit on path requests be a bound on cost rather than a hope.

The gameplay cost is real and is the reason this may need revisiting. A maze-like region can exhaust 4,096 expansions on a route that genuinely exists, returning PATH_BUDGET_EXHAUSTED. That outcome is sound — it claims nothing about whether a path exists — but it is a refusal a player can feel, so this value trades responsiveness in complex terrain for a hard cost bound.

Fog cadence ladder and the Q-012 reduction — added 2026-08-02

Q-015a (300ms per room-second) was breached by a single observer at the old Q-012 of 512. This section records the measurement, the reduction, and the mechanism that now enforces the ceiling at runtime.

Measured cost of one fog advance over a 256 × 256 chunk (Q-010/Q-011), release crate at submodule 8b6a23c:

Occluders032128192256384512
ms0.269.3120.4225.1929.3736.1541.26

Derived cost model: cost ≈ 0.26 + 1.81·√n ms, fitting every point above to within 2%. The square root is the BVH traversal, not a curve fitted for convenience — which is why this may be used to interpolate an untested occluder count and the raw table may not.

Q-012 is reduced from 512 to 256. At 512 an advance costs 41.26ms, so one observer advancing every tick is 825ms per room-second — 2.8× Q-015a. At 256 an advance costs 29.37ms, which the ladder below can hold. Q-015a was deliberately not raised: it is an isolation ceiling describing a room that should not exist, and raising a ceiling to fit the load it was built to catch would leave the mechanism intact and the protection gone.

The room’s advance allowance is derived, not chosen:

Q-015a 300ms ÷ 29.37ms per advance = 10.2 → 10 advances per room-second, for the entire room with every observer combined.

At Q-001’s 20 ticks per second that is one advance every other tick. A single observer at full tick cadence costs 587ms and breaches the ceiling alone, which is why the ladder is not a tuning preference.

The ladder (§5.3’s mask cadence axis):

TierIntervalAdvances/secms/room-second
Focus — the acting party200ms (4 ticks)5.0146.9
Near — in the interest set500ms (10 ticks)2.058.7
Far — present, off-screen1s (20 ticks)1.029.4
Dormant — not moved recently2s (40 ticks)0.514.7

Checked against this table’s own reference room. It is 50 viewers but five mask sets — “four parties plus the Keeper” — not fifty. One focus + one near + three far = 10.0 advances/sec = 294ms, inside the ceiling. Fifty independent masks were never affordable at any tier, and the reference case only closes because masks are per party rather than per viewer.

The ladder is policy; the token bucket is the enforcement point. Four focus-tier parties each obey their 200ms interval and together cost 587ms. A per-room bucket admitting 10 advances per room-second is checked at dispatch, and an advance that cannot be afforded is skipped — the mask stays as it was and MaskStaleness grows, which §5.2 already designates as the correct degradation. Without it Q-015a is a row in a table that nothing checks. Enforcement point: Descent.Vtt.Application.Rooms.FogBudget, called from RoomGrain.AdvanceTickAsync before every dispatch.

Only the cadence axis of §5.3’s ladder is implementable in the host. The crate’s C ABI advances a fixed 256 × 256 chunk — PACKED_CHUNK_BYTES is a crate constant and there is no half-resolution query — so the granularity axis requires a change to Descent.Geometry, not to the backend. Recorded here because the two axes read as interchangeable in §5.3 and are not.

What the reduction costs. Q-012 is a authoring-facing hard limit, so halving it halves the dynamic occluders a chunk may hold. That is a gameplay-visible constraint on level authoring, not merely an internal bound, and it is the price of keeping Q-015a honest.

The ladder’s four intervals (Q-068…071) — registered 2026-08-02

The ladder above was ruled on 2026-08-02 but its four intervals were left in a table without ids, so they existed as prose here and as literals in FogCadence.cs and in no third place that either could be checked against. §9.2 then restated the cadence as “20Hz” for a further session because nothing bound the two together. These ids are assigned to values that were already ruled; no value changes here.

IDNameValueSourceStatus
Q-068FOG_CADENCE_FOCUS_TICKS6Re-derived 2026-08-04 against Q-075 (was 4)Normative
Q-069FOG_CADENCE_NEAR_TICKS15Re-derived 2026-08-04 against Q-075 (was 10)Normative
Q-070FOG_CADENCE_FAR_TICKS30Re-derived 2026-08-04 against Q-075 (was 20)Normative
Q-071FOG_CADENCE_DORMANT_TICKS60Re-derived 2026-08-04 against Q-075 (was 40)Normative

Registered in ticks, not milliseconds, because ticks are what the host stores and milliseconds are derived. The interval is ticks × Q-001 — Focus is 4 × 50ms = 200ms — so a change to Q-001 moves all four without any of them being edited. Registering the millisecond form as primary would have created four values that silently disagree with the tick period the moment Q-001 moved, which is the shape of defect Q-016’s original definition had.

The four are not independently tunable, because Q-015a is checked against their mix rather than against any one of them. A tier advances 1000 ÷ (ticks × Q-001) times per second, so its cost is that rate × 29.37ms — Focus is 1000 ÷ 200 = 5.0/s × 29.37 = 146.9ms. The reference room — one Focus, one Near, three Far — spends 146.9 + 58.7 + 3 × 29.4 = 294ms of Q-015a’s 300ms. The margin is 6ms. Halving Q-068 alone adds 146.9ms and breaches the ceiling on its own; so does promoting a second party to Focus. Any edit to one of these four requires recomputing the reference mix, and the token bucket is what makes the breach a skipped advance rather than a missed tick.

These bound presentation, and the presentation layer is where their cost is paid. Q-068 is the value §9.2’s fog-edge animation exists to hide: at 90Hz per eye the Focus interval plus ADR-033’s one-tick collection lag is about twenty-two frames of staleness. Raising Q-068 to buy room-second budget is therefore never free — it is paid in XR fog smoothness, and the two decisions have to be made together.

Enforcement point: Descent.Vtt.Application.Rooms.FogCadenceOptions, whose defaults are pinned against this table by FogCadenceOptionsTests.TheTierIntervalsAreTheValuesTheRegistryRecords.

Intentional leakage and permeability (Q-072…075) — added 2026-08-02, all pending

ADR-091 rules that audibility and leaked illumination are server-computed and dispatched pre-attenuated. Four quantities follow from it. All four are Normative (pending): the ruling exists, the values do not, and registering the rows now is what distinguishes “undecided” from “unbounded” — the point of this table’s Pending marker.

IDNameSourceStatus
Q-072ACOUSTIC_ATTENUATION_LEVELSADR-091 clause 3 (ordinal levels an audio attenuation may take)Normative (pending)
Q-073OPTIC_LEAKAGE_LEVELSADR-091 clause 3 (ordinal levels a leaked light intensity may take)Normative (pending)
Q-074LEAKAGE_EVENT_RATE_MAXADR-091 clause 4 (leakage events per concealed entity per room-second)Normative (pending)
Q-075PERMEABLE_ADVANCE_COST_MS44ADR-091 clause 7; measured 2026-08-04, see below
Q-076OPPOSED_CONTEST_RATE_MAX6ADR-094 clause 6; derived 2026-08-04, see below

Ecosystem bounds (Q-078…Q-081) — added 2026-08-05

Introduced by the Marketplace, Studio and ecosystem-module integration. All four are pending, and per §3.1’s rule the corresponding protection does not exist until each has a value — stated here rather than left to be discovered at the enforcement point.

IDNameValueSourceStatus
Q-078ENTITLEMENT_PRIMARY_FALLBACK_RATE_MAXPendingADR-097 enforcement; Marketplace_Architecture.md §2.2Normative (pending)
Q-079QUALIFIED_TAG_KEY_MAX_BYTESPending (measured against Q-077’s 64 on the qualified form)ADR-098 enforcement; Studio_Architecture.md §4.4Normative (pending)
Q-080STUDIO_SANDBOX_GRAIN_ROOM_WEIGHTPendingStudio_Architecture.md §7.2; input to ADR-002’s weighted placement director⊙ Estimated (awaiting measurement)
Q-081FINALIZE_MOVE_MIN_INTERVAL_MSPendingEcosystem_Module_Designs.md §3.3; input to §5.2’s per-player intent cap⊙ Estimated (awaiting measurement)
Q-083STUDIO_MODULE_CONTRACT_WINDOW_RELEASESPendingADR-109 enforcement; Studio_Architecture.md §1.5.2. How many releases a Studio module contract stays loadable — the reasoning of Q-046’s cartridge window: a window stated in releases can be planned against, one stated as “current” breaks peopleNormative (pending, blocked on an update-cadence decision, not on measurement)

Q-078 is an alarm threshold, not a limit. The fresh-read escape hatch is already bounded to one primary attempt per lookup by ADR-097 clause 5, which is a structural bound and is not pending. Q-078 is the rate above which the replica is declared unhealthy. Until it has a value the metric is observed and not alarmed, which is a monitoring gap rather than an open resource path — the distinction matters because the two are usually conflated and only one of them is urgent.

Q-079 is not a duplicate of Q-077. Q-077 bounds what a cartridge may declare; Q-079 bounds what a creator-namespaced key expands to after Studio prefixes it with a package id (@blood-studios/vampire-pack:boss). The authored suffix can satisfy Q-077 while the qualified form does not, and the qualified form is what is stored and transmitted. Measuring the wrong one is the failure this entry exists to prevent.

Q-080 and Q-081 are both inputs to budgets that already exist, not new budgets. Q-080 feeds the ADR-002 placement director, which is currently admitting an unmeasured load if authoring sessions are co-placed with live rooms; Q-081 feeds §5.2’s occupancy table, whose per-player intent caps are derived rather than chosen. Adding a per-second finalize from every seated player without deriving it again is the exact move that table exists to prevent.

Presigned asset-read URL lifetime (Q-089) — added 2026-08-08, ADR-123

IDNameValueSourceStatus
Q-089ASSET_READ_URL_TTL_SECONDSPendingADR-123; §6.2 download issuancePending

Pending rather than , and the reason is that the right value is a measurement nobody has taken. The TTL must be the shortest window that reliably completes a fetch of the largest asset a room can hold, on the slowest profile that must load it — a Profile C tablet on mobile data is the binding case, not a desktop. Publishing a guess would set a security parameter by convenience, and a TTL that is too short is a support ticket while one that is too long is a bearer capability living longer than its purpose.

It bounds the URL, not the asset. Once fetched, the bytes are in OPFS and this quantity has nothing further to say about them — the access control is at issuance (ADR-123), and §9.6.2’s LRU scavenger governs what happens after.

The Marketplace’s Q-M-034 is the same idea in the other bounded context and is deliberately not shared. Two systems mint URLs against different storage under different authority; one quantity spanning both would couple a VTT room’s liveness rule to a storefront’s checkout flow.

Per-account storage quota (Q-085…Q-088) — added 2026-08-08, ADR-119 / ADR-120 / ADR-121

IDNameValueSourceStatus
Q-085ACCOUNT_STORED_BYTES_FREE⊙ 200 MBADR-119; §6.2’s presigned-URL gate⊙ Estimated — benchmark: the custom-asset size distribution below
Q-086ACCOUNT_STORED_BYTES_PLUSPending (5 GB proposed)ADR-121 (Conditional)Pending — blocked on a recurring-billing surface, not on measurement
Q-087ACCOUNT_STORED_BYTES_PROPending (20 GB proposed)ADR-121 (Conditional)Pending — as above
Q-088EXEMPTION_DISTINCT_OWNERS_MINPendingADR-120Pending — the second half of the shared-copy rule

Q-085 is and the benchmark it names is real rather than a formality. 200 MB was chosen as a round number and not measured against anything. The measurement that would make it normative is the size distribution of custom uploads by class — an 8K 2D map and a single high-poly .stl are each plausibly the whole allowance, which would make the free tier “one asset” rather than “a small library”. That may be the intended product, but it must be a decision taken against real numbers rather than a consequence discovered by users.

It bounds STORED bytes in R2, not uploaded bytes. The two diverge under re-upload and versioning, and the stored copy is the one that is billed. Recorded because “quota” reads as either and the check in §6.2 predates any definition.

It is a cost bound, never an abuse bound (ADR-105). It scales with entitlement tier, which is exactly what ADR-105 permits for a cost bound and forbids for an abuse one. Nothing here defends against a hostile account, and no sentence elsewhere may cite it as though it did.

Q-086 and Q-087 are Pending rather than , and the distinction matters. Numbers are proposed (5 GB, 20 GB) but ADR-121 is Conditional: ADR-M-027 defers subscriptions from Marketplace v1, and the Marketplace is merchant of record, so these cannot be charged for today. They are blocked on a commercial mechanism, not on measurement — the same category ADR-087 uses for Q-034’s daily allowance, and for the same reason: publishing a value would imply an entitlement tier that does not exist.

Q-088 bounds the loophole, not the cost. ADR-120 exempts an asset from Q-085 only where the purchaser is not the publisher; Q-088 is the second guard, because two colluding accounts publishing for each other satisfy “purchaser ≠ publisher” trivially. It is Pending because the right value depends on catalogue behaviour nobody has observed — a threshold set too high denies the exemption to a genuinely niche pack with few buyers, which is the honest failure mode of setting it by guess.

Room chat bounds (Q-090…Q-092) — added 2026-08-09, chat.fbs

IDNameValueSourceStatus
Q-090CHAT_BODY_MAX_CHARS2,000§5.2’s rule that an authoring-driven ceiling may refuse outright; chat.fbsNormative
Q-091CHAT_SENDER_NAME_MAX_CHARS64chat.fbs; the server authors this field, so the bound is on the server’s own outputNormative
Q-092CHAT_MESSAGE_MAX_BYTES16,384Derived: see the formula belowDerived

Normative rather than , because neither estimates anything. A figure is a placeholder for a benchmark; these two are choices about what a room will refuse, and there is no measurement that would settle them. Q-090 is the same category as Q-012 (DYNAMIC_OCCLUDER_MAX): an authoring-driven ceiling, which §5.2 says may refuse rather than degrade, because a person who types 2,001 characters can act on being told so.

Enforcement point (ADR-045): ChatCodec.TryDecode refuses a body over Q-090 before allocating the string, and RoomGrain.SubmitChatAsync refuses it again. Two checks over one rule, for the reason RoomGrain.SetOccludersAsync records: the decoder guards the transport and the grain guards the room, and there is already a second caller planned for the grain (a system message authored by the ruleset) that does not arrive through a hub.

Q-092’s formula, stated because rule 3 requires it.

Q-092 = next power of two ≥ (Q-090 × 4) + (Q-091 × 4) + 256
= next power of two ≥ 8,000 + 256 + 256
= next power of two ≥ 8,512
= 16,384

The ×4 is UTF-8’s worst case per UTF-16 code unit, which is what both hosts count a “character” in — and it deliberately over-counts by 2×, because a 4-byte UTF-8 sequence occupies two code units rather than one. A ceiling is the one place where being conservative is free. The 256 is the FlatBuffers framing: two inline Uuid structs (32 B), the version, the kind, the timestamp, and the vtable and offsets.

Q-092 is exactly half of Q-056, and that is a decision rather than an arithmetic coincidence. Q-056 bounds any SignalR message at 32,768 as defence in depth; setting chat’s own ceiling at half of it means a chat frame can never be the message that meets the transport cap, so a Q-056 breach on this silo always names a different path. A ceiling that could touch the shared one would make the shared one useless as a diagnostic.

The Q-084 gap is pre-existing and its reason is not recoverable. Noticed while allocating Q-090: the registry runs Q-083Q-085 with no row, no ruling and no note, unlike Q-057, which line 192 records as intentionally unassigned. It is written down here rather than quietly closed by renumbering, because a gap somebody deliberately left and a gap somebody skipped by accident look identical afterwards, and only the record can tell them apart. This note is the record; the reason itself is marked unrecoverable rather than invented.

Ticket exchange bounds (Q-093…Q-094) — added 2026-08-09, ADR-130

IDNameValueSourceStatus
Q-093TICKET_REDEMPTION_TIMEOUT_MS2,000ADR-130; Marketplace §4.5. The silo’s bound on one redemption callNormative
Q-094TICKET_ACQUISITION_TIMEOUT_MS3,000ADR-130; Marketplace §4.5. The client’s bound on one acquisition callNormative

Normative rather than , and the distinction matters here more than usual. A figure is a placeholder for a benchmark that will replace it. Neither of these estimates a cost: they are choices about how long a connection attempt may spend waiting on another bounded context before giving up and retrying, and no measurement of the Marketplace settles that — a faster Marketplace does not make a longer wait correct. BENCH-M-03 measures the redemption round trip’s cost on the Marketplace side and is a different question in a different corpus.

Both are bounded above by a quantity this corpus does not own. The ticket’s own lifetime is ~15 seconds, and it is the Marketplace’s figure (§4.5), stated there rather than mirrored here — §8’s cross-context rule is explicit that Q-M-* is a separate namespace and that restating another context’s number is how the two come to disagree. What follows from it is the shape of these two: every millisecond either side spends waiting is a millisecond that cannot be spent acquiring a fresh ticket and retrying, and a retry is the only recovery a single-use credential has. A timeout approaching the credential’s life is therefore not “patient”, it is a guaranteed second failure.

Q-093 < Q-094 deliberately, and the asymmetry is not an oversight. Acquisition happens once per attempt on a client that has nothing else to do; redemption happens on the silo’s connection path, where a held request is server capacity. The client can afford to wait longer than the server can.

Enforcement point (ADR-045): HttpTicketRedeemer.RedeemAsync applies Q-093 on a linked cancellation token — not HttpClient.Timeout, which surfaces as the same exception a caller’s cancellation does, and the two must stay distinguishable or a deployment drain reports as a Marketplace outage. MarketplaceIdentityOptionsValidator refuses a non-positive value at startup. Q-094 is applied by an AbortController in auth-client.ts. TicketRedemptionTests.ARedemptionThatOutrunsQ093IsUnavailable asserts the first against a gated handler rather than a longer sleep, per §14.8.

Both ship a default, unlike Q-082, and the reason is which direction the absence fails. An unset intent rate is unbounded work for an attacker. An unset timeout falls back to HttpClient’s own 100 seconds — bounded badly and visibly, not unbounded. A host that wants a different value sets one; a host that sets none gets a working silo rather than one that refuses to start over a number it has no opinion about.

Cross-context input bounds (Q-095…Q-096) — added 2026-08-09, ADR-131

IDNameValueSourceStatus
Q-095MARKETPLACE_SUBJECT_MAX_CHARS128ADR-130/131; the subject a redemption establishesNormative
Q-096RESOURCE_URN_MAX_CHARS512ADR-131; vtt_entitlement_v1.resource_urnNormative

Both bound a string this context did not author, and neither is an estimate. They are the same category as Q-090 and Q-091 — a choice about what this side will refuse — rather than a placeholder for a measurement. What makes them worth registering is that both values reach a database key or an indexed comparison, and an unbounded externally-authored string arriving over a network is the shape every one of this corpus’s input rules exists to refuse.

Q-095 is retrospective. It has been in the code since ADR-130 as a bare MaxLength constant, uncited — recorded here rather than quietly left, because P5 is explicit that a number in code without a Q-ID is invisible to both the reader and the lint, and the fact that it predates this ruling does not make it less so.

Refusal, not truncation, in both cases. A truncated subject is a different principal that looks valid, and two subjects sharing a prefix would collapse into one linkage row — a disclosure defect wearing the costume of a formatting bug.

Enforcement point (ADR-045): MarketplaceSubject.TryCreate and ResourceUrn.TryCreate are the only constructors of either type, and both refuse rather than clamp; TicketRedemptionTests.AnOverlongSubjectIsRefused asserts Q-095 at the redemption boundary.

Cartridge load-context linger (Q-097) — added 2026-08-09, ADR-134

IDNameValueSourceStatus
Q-097CARTRIDGE_CONTEXT_LINGER_MS600,000ADR-134; §4.4’s reference-counted collectible unloadNormative

What it is. How long a cartridge’s AssemblyLoadContext stays loaded after the last room releases it. Ten minutes.

Why the value is not zero, which is what §4.4 reads as. Orleans deactivates idle rooms as a matter of routine — idle collection, rebalance, silo migration — so unloading the instant a reference count reaches zero makes a silo re-read, re-verify and re-JIT the same cartridge continuously under ordinary load. It is also worse than it first appears: AssemblyLoadContext.Unload is asynchronous, so a context is unloaded when requested and collected some time later, and rapid churn leaves several generations of one cartridge alive simultaneously. That is the duplicate-context waste ADR-134 rejected per-room contexts to avoid, arriving by a different route.

This is a chosen policy value, and it is Normative rather than because it is not derived from anything. No unit cost was assumed and no benchmark is pending, so marking it would imply a measurement is owed that is not. It is ten minutes because that is comfortably longer than an Orleans idle-collection cycle and short enough that a cartridge withdrawn from a silo stops occupying memory within one operator’s attention span. The evidence that would overturn it is a measured AlcCartridgeLoader reload rate on a silo under real room churn: a non-zero steady-state reload rate means the window is too short, and a resident set that never shrinks on a silo whose catalogue has changed means it is too long. Neither number exists yet, which is why this paragraph names them rather than a benchmark ID.

The sweep period IS this window, deliberately, so there is one number rather than two — CartridgeSweeper runs its PeriodicTimer at Q-097. The consequence is that a cartridge is reclaimed somewhere between one and two windows after its last room let go. Nothing depends on the exact moment, and a second configurable interval would be a second thing to get wrong for no behaviour anyone can observe.

Enforcement point (ADR-045): AlcCartridgeLoaderTests.ACartridgeStaysLoadedForTheWholeLingerWindow asserts both directions against a stepped TimeProvider — resident before the window elapses, swept after — and ARoomReturningInsideTheWindowReusesTheLoadedCartridge asserts the behaviour the window exists to produce. CartridgeLoaderOptionsValidator refuses a negative value at startup; zero is legal and means “unload as soon as nothing holds it”, which is what the collectibility test uses.

Volumetric visibility work budget (Q-102) — added 2026-08-11, ADR-155

IDNameValueSourceStatus
Q-102VOLUME_VISIT_BUDGET(no value)ADR-155; VolumeBvh::segment_visibility takes it as a required argumentPending

The existence of this row is the deliverable, and its value deliberately is not — the distinction rule 5 draws between undecided and unbounded. What ADR-155 decided is the unit; what nothing yet decides is the number.

The unit is node visits, and refusing milliseconds is the decision. The proposal that produced this quantity asked for a 300 ms budget on volumetric visibility, with a fallback to 2D projection when exceeded. That cannot be an input to this crate. Descent.Geometry exists to give bit-identical answers on two hosts (ADR-017), and a wall-clock reading is not a function of the inputs: two hosts running the same query on the same scene would cross a millisecond threshold at different moments and return different answers — 3D on one, projected 2D on the other. A timer is the single input that destroys the property the crate is for.

Q-058 is the precedent, not an analogy. PATH_EXPANSION_BUDGET bounds A* by expansions rather than by milliseconds for exactly this reason, and it was measured rather than guessed. This is its sibling in a different subsystem.

The host still gets its 300 ms, in the place a deadline belongs: outside the deterministic core, where ADR-033’s dispatch-now / collect-next-tick surface already owns wall-clock. A budget the tick may abandon and a budget the answer depends on are different things, and collapsing them is how a deterministic core stops being one.

What would make it Normative. The worst case is bounded and known — a BVH over Q-012’s 256 panels has at most 2n - 1 = 511 nodes, so a budget at or above 511 can never be exceeded and would make the mechanism inert (the same trap the registry records for Q-058 against Q-059). The useful value is the typical traversal cost on a real map, which needs the geometry cost measurement BENCH-04 covers, extended to volumetric scenes. That benchmark does not exist for 3D and is named here rather than assumed, per §4’s rule that a provisional number with no named benchmark stays provisional forever.

Until it has a value, the protection is the API rather than the number. VolumeBvh::segment_visibility takes a VisitBudget as a required argument with no Default, so a host that has not chosen one fails to compile — the same discipline Bvh::build applies to Q-012’s ceiling, and for the same reason: a silent default is how an unbounded traversal reaches production.

Enforcement point (ADR-045): VisitBudget has no Default and no constructor that supplies one, which is the structural half. volume.rs’s the_budget_outcome_is_a_function_of_the_inputs asserts the determinism the unit was chosen for, and an_ample_budget_answers_and_a_zero_budget_does_not pins both ends of the ladder so “exhausted” is neither unreachable nor universal.

SignalR inbound message bound (Q-101) — added 2026-08-11, ADR-154

IDNameValueSourceStatus
Q-101SIGNALR_MAX_RECEIVE_MESSAGE_BYTES32,796Derived: Q-056 + 28, the measured MessagePack envelope constant for the longest hub method taking a byte[]Derived

Q-056 and this are different quantities, and they had the same value. Q-056 bounds the payload — 32,768 bytes of FlatBuffers. MaximumReceiveMessageSize bounds the encoded message, payload plus envelope. SignalR’s default for the second is also 32,768, so the two read as one control and neither the code nor the corpus distinguished them.

Measured 2026-08-11 (dotnet run tools/bench-hub-envelope.cs), a payload at exactly Q-056 inbound as SubmitOccluders(byte[]):

protocolencodedoverheadagainst SignalR’s 32,768 default
JSON43,74710,979over
MessagePack32,79628over

Both. A Q-056-sized payload has never in fact been admissible on this hub: the transport refused it before the hub saw it, and the effective inbound payload ceiling was roughly 24.5 KB under JSON — a number nobody chose and nothing recorded. That is the defect Q-101 closes.

The derivation uses MessagePack’s constant and not JSON’s, and that is a decision. JSON stays registered so an unmigrated client can still negotiate (ADR-154), but sizing every connection’s receive buffer for base64 would inflate it by a third in order to admit a maximal payload from a legacy client — a case that has never worked and is not being made to work. A legacy client at the cap is refused loudly at the transport; a migrated client is not.

The 28 is a worst case rather than a convenient one. bench-hub-envelope.cs enumerates every hub method taking a byte[] and measures against the longest name, so adding a longer method later moves the constant and the benchmark says so.

Enforcement point (ADR-045): SignalRRegistration.MaxReceiveMessageBytes is the only place the value appears and AddDescentSignalR is the only caller; HubProtocolEnvelopeTests.TheInboundLimitAdmitsAQ056PayloadUnderMessagePackAndRefusesItUnderJson asserts both arms — the admission and the deliberate refusal — so a later widening to “fix” the JSON case cannot pass silently.

Asset chunk decryption (Q-098…Q-100) — added 2026-08-11, ADR-153

IDNameValueSourceStatus
Q-098ASSET_CHUNK_PLAINTEXT_BYTES524,288§6.1, ADR-060. Retrospective — the value has been in the whitepaper since the content-protection section was writtenExisting
Q-099CHUNK_DECRYPT_WEBCRYPTO_MS0.243tools/bench-asset-decoder.mjs, 2026-08-11⊙ Estimated
Q-100CHUNK_DECRYPT_WASM_SIMD_MS4.738tools/bench-asset-decoder.mjs, 2026-08-11⊙ Estimated

Q-098 is Existing rather than Normative, and the distinction is the honest one. §6.1 states 512KB and says it is sized “to the frame budget in §9.6.1 rather than to convenience”, but the derivation is not recorded anywhere and cannot be reconstructed — so marking it Derived would require inventing a formula (P9 forbids that) and marking it Normative would claim a ruling that does not exist. What the row buys is that descent-asset-decoder’s CHUNK_PLAINTEXT_BYTES and the Azure Worker’s splitter now cite one identifier instead of two copies of a literal.

Q-099 and Q-100 are although they were measured, and the reason is the environment rather than the arithmetic. Both figures come from a real run of a real decoder over a real 512KB chunk — nothing here is derived from an assumed unit cost. But the run was Node v24.18.1 on one x86-64 developer machine, and the claim the audit makes is about a browser on a player’s device. Those differ in ways that could move either number: a phone with no AES-NI narrows the gap from one side, and a browser’s crypto.subtle crossing a process boundary per call widens it from the other. Rule 2 applies unchanged — no normative sentence may rest on either without saying so, and ADR-153’s decision is written to survive both being wrong by a factor of three, because the finding it rests on is structural.

The benchmark that would make them normative is the same script run under Playwright against the Guardrail 7 device matrix, reporting a distribution rather than a best-of-five. It does not exist. Naming it is what stops these two becoming provisional forever (docs-and-adr-workflow.md §4).

What the two figures are for. Q-100 / Q-099 ≈ 19.5 is the ratio ADR-153 turns on: the WebAssembly decoder is an order of magnitude slower than crypto.subtle structurally, because WebAssembly has no AES instruction in any shipped or proposed extension, so RustCrypto’s aes 0.9.2 — which has no wasm32 backend and falls to the fixslice bitsliced software cipher — is the best any wasm build can do.

+simd128 is a real improvement and it is nowhere near enough, which is the finding. Measured against a build of the same source with the flag removed:

buildms/chunk (3 runs)artefactv128 opcodes
+simd1284.738 / 4.738 / 4.78042.9 KiB714 of 17,235
no simd1285.012 / 5.024 / 5.02154.9 KiB0 of 24,400

5.3% faster, and the spread inside each arm is under 1%, so the gain is real rather than noise. LLVM did autovectorise the bitslice — the census is dominated by V128Xor (129), V128And (30), V128Not (32) and the I64x2 shifts, which is exactly the boolean algebra fixslice is made of. It buys 5% against a 1,950% deficit.

Q-099’s own variance is much larger than Q-100’s and the row does not hide it: three runs gave 0.258 / 0.348 / 0.290 ms, a 35% spread, because at a quarter of a millisecond the measurement is substantially call overhead. The best observed figure is recorded, which makes the ratio a lower bound on crypto.subtle’s advantage rather than a flattering one.

Enforcement point (ADR-045): none, and that is correct. Neither figure bounds anything; they are evidence for a decision, and the decision’s enforcement point is decoder.test.ts › records the shipped preference as false, which pins WASM_DECODER_PREFERRED.

SDK client footprint (Q-082) — added 2026-08-06

IDNameValueSourceStatus
Q-082SDK_CLIENT_BUNDLE_BUDGET_KIBPendingADR-099 consequences; §12.1 (@descent-vtt/sdk is delivered to a browser client)Pending

Pending, deliberately not , and the difference is the whole reason this row exists. A figure is an estimate awaiting a benchmark — it has a number and a named measurement. This has neither: no budget has been decided and nothing has been measured, so publishing an estimate would invent the first half of a decision nobody made.

What the row records is that the SDK’s client-side footprint is currently unbounded, not merely untuned — the same distinction §3.1 draws for Q-033 and Q-050. ADR-099 removed the platform’s opinion about which validator a creator uses, which is correct for the contract and means the SDK can no longer bound this quantity on the creator’s behalf. What it can bound is its own footprint, and that is what this entry is for.

It is not a blocker for ADR-099: the decision’s safety rests on server-side re-validation (ADR-098), not on any size. It is a blocker for claiming the SDK is lightweight, and it exists so that claim is not made before there is a number behind it.

Attribute key bound (Q-077) — added 2026-08-04

IDNameValueSourceStatus
Q-077ATTRIBUTE_KEY_MAX_BYTES64ADR-095 clause 3Normative

Enforcement point: Descent.Vtt.Sdk.AttributeSchema.TryCreate, on every declaration, and on a prefix declaration additionally that it leaves room for a member.

This is not a bound that was missing; it is a bound that was not needed until ADR-095. Before prefix declarations, every writable key was spelled out by a cartridge, so key length was bounded by the cartridge’s own vocabulary — an enumerated, reviewed, finite list. A prefix family makes the suffix caller-supplied, which turns key naming into client-controlled input to a quota. That is the same shape §3.1 exists to prevent, arriving through a door ADR-095 deliberately opened.

It does not replace Q-060, and the two answer different questions. Q-060 bounds the payload in aggregate at 4,096 bytes; Q-077 bounds what any single key may take out of it. Without it, one 4,000-byte key is a legal payload that has consumed the whole budget — technically bounded, practically a denial of the actor’s own attributes.

Why 64. It must exceed the longest key any shipped cartridge declares, and BRP’s own cartridge-level bound is 32 bytes — so 64 is double the strictest consumer and leaves room for a ruleset with longer conventions. At 64, one key is 1.6% of Q-060, so a payload cannot be dominated by a single name. At Q-061’s ceiling of 128 keys the two interact predictably: 128 × 64 = 8,192, comfortably above Q-060, so Q-060 remains the binding constraint and Q-077 is the per-key floor beneath it rather than a second aggregate.

Q-072/Q-073 and Q-074 may not be decided independently, because the channel capacity is their product. A concealed entity leaks at most log₂(levels) × rate bits per second. Four levels at five events per second is ~10 bits/s — ample to locate something over a minute of listening — while four levels at one event per second is not. Neither factor is meaningful alone, and bounding one while leaving the other open produces a number that looks like a control and is not. This is the same coupling Q-051/Q-052 carry, with a product in place of a sum.

These are security bounds that will be argued as aesthetic ones. A muffled growl stepping between Q-072 levels sounds worse than one sliding continuously, and the request to raise it will arrive from sound design rather than from anyone who has read ADR-091. Recorded here so the next person to weigh it knows both hats are on the same head.

Q-075 blocks the re-derivation of Q-015a’s advance allowance and of Q-068Q-071. Today a ray terminates on its first blocking hit; accumulating attenuation means continuing through every permeable occluder on the path, so the measured 29.37 ms that 300ms ÷ 29.37ms = 10 advances rests on is not expected to survive. Until Q-075 exists, the ladder remains derived from the impermeable cost and permeability does not ship — shipping first would leave the token bucket admitting advances against a cost that no longer holds, and the symptom would be tick overruns in rooms with heavy permeable geometry, a long way from the cause. Its unblocking measurement is the existing fog-advance benchmark re-run with permeable occluders at Q-012’s ceiling.

Q-076 is coupled to ADR-094 clause 2’s outcome vocabulary for the same reason Q-072/Q-073 are coupled to Q-074. An opposed contest resolved under the default policy reports one of three outcomes — won, lost, tied — so each contest discloses at most log₂(3) ≈ 1.58 bits about the opposition’s hidden rating, and the recoverable total is that figure times the rate. Randomness does not hide the rating; it is what makes each contest an independent sample of it. Widening the vocabulary (reporting the margin, or the opposition’s degree) and raising the rate are therefore one decision, not two.

Its unblocking measurement is behavioural rather than performance. The question is how many contests against one opposition a legitimate scene actually contains — a stealth approach past a sentry is a handful, not fifty — and the bound is set above that and below the number at which a rating is recoverable to within a band. Until Q-076 exists, ADR-094 clause 6 is described and unenforced, and the default policy bounds the per-contest disclosure while nothing bounds the rate.

Q-076 does not apply to a contest the keeper has declassified (ADR-094 clause 7). Once a rating has been shown to the table, rate-bounding its re-display protects nothing. The exemption is per contest and recorded in the stream; it is not a property a room can acquire.

Q-075 measured — 2026-08-04

Value: 44 ms per permeable fog advance, at Q-012’s ceiling of 256 occluders. Enforcement point: permeable_advance_cost.rs in the geometry crate, printing the table below; permeable_bvh_equivalence.rs holds the property that makes the comparison meaningful.

The measured quantity is a ratio, and the absolute is derived from it. The benchmark box runs the impermeable advance at 12.99 ms where the 2026-08-02 measurement recorded 29.37 ms — it is roughly 2.2× faster, so its absolute numbers cannot be written into a ladder derived on the other machine. What transfers is the multiplier, because both arms run on the same box against the same geometry.

OccludersImpermeablePermeableRatio
163.09 ms3.36 ms1.1×
649.08 ms10.47 ms1.2×
1289.45 ms12.26 ms1.3×
25612.99 ms18.27 ms1.4×

Stable across runs at 1.4–1.5× at the ceiling. Q-075 = 29.37 ms × 1.5 ≈ 44 ms, taking the upper end because the conservative direction for a cost is the larger one.

ADR-091 clause 7 expected worse, and this is the headline. The clause anticipated that removing the early exit would break the derivation outright — a ray that must visit every crossing rather than stopping at the first looked like an order-of-magnitude change. It is 1.4×. The BVH prunes by the ray’s bounding box, and the overwhelming majority of rays cross very few occluders, so “no early exit” costs almost nothing on the rays where there was nothing to exit early from. The penalty is real and it is a constant factor, not a change of complexity class.

The comparison is like-for-like by construction. A permeable set whose members are all opaque reveals exactly what the impermeable advance does, asserted cell for cell. Without that property the measured difference would be part cost and part answer.

The permeable arm is deliberately not all-opaque. One occluder in four is opaque and the rest attenuate partially, because a wholly opaque field lets saturation at Fx32::MAX short-circuit the accumulation — which would understate exactly the cost being measured.

Q-015a’s advance allowance and the Q-068Q-071 ladder — re-derived 2026-08-04

ADR-091 clause 7 requires the ladder to be re-derived from Q-075 rather than assumed to have survived it. It did not survive unchanged.

The allowance falls from 10 to 6.

Q-015a 300 ms ÷ 44 ms per advance = 6.8 → 6 advances per room-second, against the 10 the impermeable cost bought.

The existing ladder breaches the ceiling under permeability. The reference mix — one Focus, one Near, three Far — runs at 10.0 advances/second, which at 44 ms is 440 ms against a 300 ms ceiling. A 140 ms overrun; the ladder as it stands does not hold.

Two ways to fit, and they are a product decision rather than an arithmetic one.

(a) Slow the ladder. Multiply every interval by the cost ratio. This is arithmetically exact — scaling every rate by 1/1.5 scales the mix’s total cost by 1/1.5 — and lands on the same margin the old ladder had:

IDWasBecomesCost in the reference mix
Q-068 Focus4 ticks (200 ms)6 ticks (300 ms)146.7 ms
Q-069 Near10 ticks (500 ms)15 ticks (750 ms)58.7 ms
Q-070 Far20 ticks (1000 ms)30 ticks (1500 ms)88.0 ms (×3)
Q-071 Dormant40 ticks (2000 ms)60 ticks (3000 ms)

Total 293.4 ms of 300 ms; margin 6.6 ms, against the old ladder’s 6 ms.

The cost of (a) is presentation, and it lands on a shipped decision. The Focus tier goes from 5 Hz to 3.33 Hz. §9.2 states the fog cadence as 5 Hz, and epic_presentation_smooth_fog.md fixes its temporal crossfade at exactly 200 ms with the invariant that the duration “must be strictly fixed and never adaptive to the magnitude of the mask delta”, because a delta-dependent duration is a timing side channel forbidden by ADR-034 clause 3. That invariant survives — the value becomes 300 ms and stays fixed — but the epic’s number changes, and the token/mask desync it already records as an open item grows from 250 ms to 350 ms.

(b) Cut Q-012 instead. Cost is ≈ 0.26 + 1.81·√n ms impermeable, so a permeable advance costs 1.5 × (0.26 + 1.81·√n). Setting that equal to the 29.37 ms the current ladder is built on gives √n ≈ 10.7, so Q-012 would fall from 256 to ~112 occluders. The ladder, §9.2’s 5 Hz and the epic’s 200 ms all survive untouched; what shrinks is how much dynamic geometry a room may contain.

Recorded rather than chosen, because the two trade different things. (a) costs every room a slower fog edge; (b) costs authored scenes their occluder budget and re-opens a Q-012 that was itself reduced from 512 only on 2026-08-02.

Ruled 2026-08-04: (a), and permeability ships

Q-012 stays at 256. A ~112-occluder ceiling was judged a fatal product constraint — modern maps are complex, and the limit would be met by authoring rather than by abuse, which is the wrong place for a DoS bound to bite. Slowing the Focus tier from 5Hz to 3.33Hz is an acceptable trade for a turn-based game.

Q-068Q-071 are updated in the table above and in FogCadenceOptions, and AdvancesPerRoomSecond falls from 10 to 6. Permeability is unblocked: ADR-091 clause 7’s condition is satisfied, the measurement exists, and the ladder has been re-derived from it rather than assumed to have survived.

The presentation consequence is owned rather than absorbed. The temporal crossfade in epic_presentation_smooth_fog.md becomes 300ms, and the epic’s invariant is unchanged — the duration stays fixed and never adaptive to the mask delta, which is what ADR-034 clause 3 forbids. The token/mask desync that epic already records as an open item grows from 250ms to 350ms. It was previously described here as covered by the client-side prediction work in a file named epic_frontend_prediction_proposal.md — no such document has ever existed in this repository (found 2026-08-06 by doc_reference_lint.py, which was written to catch exactly this). The desync is therefore not covered by anything: it is an open item against §9.6.4’s prediction path, whose scope is set by ADR-034 clause 3, and whose enabling work is the WASM geometry build in §11’s Phase 4 list. Recorded as uncovered rather than re-pointed at a plausible neighbour — epic_presentation_smooth_fog.md is a different epic and citing it here would reproduce the original defect with a name that happens to resolve.

Q-076 derived — 2026-08-04

Value: 6 contests against one opposition per room-second. Enforcement point: OpposedContestBudget.TryAdmit, a continuously-refilling token bucket keyed per opposition, capped at one second’s allowance.

Derived behaviourally rather than measured, as the pending row anticipated. The two ends:

  • Above legitimate play. A scene contests one opposition a handful of times — a stealth approach past a sentry is two or three rolls, not fifty. Six is roughly double the busiest honest case, so ordinary play never meets the bound.
  • Below recovery. Estimating a hidden win-rate to within ±5 points needs on the order of a hundred samples. At six per second that is ~17 seconds of uninterrupted automated contesting against one target — outside any scene, and long enough to be seen.

Per opposition, not per room. A party contesting six sentries once each learns almost nothing about any of them; one player contesting one sentry six times is sampling a single distribution. A room-wide limit confuses the two and is paid for by the first.

The coupling to ADR-094 clause 2 holds at this value. Three outcomes is log₂(3) ≈ 1.58 bits per contest, so the channel is ~9.5 bits/second against one opposition. Widening the outcome vocabulary raises the first factor and therefore requires this number to be revisited — the BRP suite asserts ContestOutcome has exactly three values, naming Q-076 in the failure message, so the two cannot drift apart silently.

What this bound does not do, recorded because a rate limit invites the assumption that it does. It makes the automated case infeasible; it does not stop patient grinding at one contest every few seconds for an hour. What covers that is not arithmetic — it is that a hundred consecutive Hide attempts against the same sentry happen in a room with a keeper in it. The bound exists to make the fast case impossible and the slow case visible, and it should not be described as more.

Cartridge attribute payload bounds (Q-060…062) — added 2026-08-02

Descent.Vtt.Domain’s Actor stores rule-specific state in a schema-less payload that only the owning cartridge understands. A schema-less payload with no bound is an unbounded allocation reachable from a client message — the same shape as the defect §3.1 exists to prevent, where a four-byte budget field in a geometry request asked for four billion node expansions. These three bound it.

IDNameValueSourceStatus
Q-060ACTOR_ATTRIBUTES_MAX_BYTES4,096Ruling 2026-08-02Normative
Q-061ACTOR_ATTRIBUTES_MAX_KEYS128Ruling 2026-08-02Normative
Q-062ACTOR_ATTRIBUTES_MAX_DEPTH3Ruling 2026-08-02Normative

Enforcement point: Descent.Vtt.Sdk.AttributeBag.TryCreate, and again in AttributeBag.TryApply on the result of every change. Checking only the opening payload is how a bound gets defeated by a sequence of individually reasonable updates, so the second check is not redundant. Refusal is a return value rather than an exception because the input arrives from the wire, where a malformed payload is expected rather than exceptional.

Q-060 measures content, not serialized length, and the formula is recorded here because rule 3 requires it. Per entry: the UTF-8 byte count of the key, plus the value’s size — 0 for null, 1 for a boolean, 8 for an integer, the UTF-8 byte count for text, and the sum of the elements for a list or nested bag. It deliberately does not measure the JSONB encoding, which depends on a serializer the contract layer does not name and must not: the SDK is a public package a cartridge compiles against separately, and pinning the bound to a serializer version would make the same payload legal on one host and illegal on another. The stored JSONB is larger than Q-060 by its own framing, which is the persistence layer’s budget to hold and not this one.

Q-061 counts keys across the whole structure, not the top level. Counting only the outer bag would let a payload hide a thousand keys one level down and still satisfy the check that exists to stop exactly that.

These three bound a cartridge, and cartridges are trusted code. §4.4 and §12.2.6 are explicit that an AssemblyLoadContext is a versioning and unload boundary and not a security boundary — a loaded .NET cartridge runs in-process with full host privileges. So these bounds are not a sandbox. They exist because the payload’s contents originate from a client, and because a first-party cartridge with an honest bug should degrade to a refusal rather than to an allocation the host cannot survive.

Interaction with ADR-071 — ruled 2026-08-02: history is exempt. ADR-071 gives attribute definitions a retired tombstone state and makes removal a two-release deprecation (Q-046), so a long-lived campaign accumulates attributes in its stream that no current definition covers. An event that was valid when written remains valid forever: tombstoned and historical attributes do not count against Q-060 or Q-061 during hydration. These two quotas guard the active, writable surface — what a client can cause to be allocated now — and history is not that surface, because it was validated on the way in. Without this exemption a campaign that satisfied every bound when its events were written could simply stop rehydrating, and the cause would be a limit it never violated.

Q-062 is not exempt, because depth is not a quota. It bounds recursion — fingerprinting and serialisation both walk the structure — so an unbounded historical depth is a stack overflow rather than a large allocation. No stream can legitimately hold one: the limit has never been looser than it is now.

Enforcement point: AttributeBag.TryRehydrate and AttributeBag.TryApplyFromHistory, used only on the replay path; AttributeBag.TryCreate and TryApply remain the live path and keep the quotas.

Known gap, recorded rather than left to be found. The write path cannot yet distinguish a tombstoned key from an active one, because ADR-071’s attribute-definition registry does not exist. An actor already over the live quota is therefore read-only today: every proposal against it is refused. That is the right fail-safe direction — a refusal, not an unbounded allocation — but it is not the end state, and it is pinned by HistoricalAttributeTests.AnActorOverTheLiveQuotaIsCurrentlyReadOnly so the behaviour changes deliberately when the registry exists.

Hydration retry and abandonment bounds (Q-063…067) — added 2026-08-02

§7.4’s hydration saga made one attempt and stopped; only a reactivation retried, so a room whose database blinked stayed unusable until something happened to address it again. It now retries inside the activation, and these five quantities bound how hard and for how long.

They are host-supplied runtime configuration (RoomHydrationOptions, Descent:Rooms:Hydration), like the §3.1 geometry limits and for the same reason: the values below are the shipped defaults and a ruling, not a compile-time constant a caller can observe or set.

IDNameValueSourceStatus
Q-063HYDRATION_MAX_ATTEMPTS12Ruling 2026-08-02Normative
Q-064HYDRATION_RETRY_CAP_MS30,000Ruling 2026-08-02Normative
Q-065HYDRATION_RETRY_BASE_MS500Ruling 2026-08-02Normative
Q-066HYDRATION_ABANDON_AFTER_MS120,000Ruling 2026-08-02Normative
Q-067KEEPALIVE_AFTER_ABANDONED_MS30,000Ruling 2026-08-02Normative

Full jitter is part of the specification, not an implementation detail. The delay before attempt n is a uniform draw over [0, min(Q-064, Q-065·2ⁿ)) — the whole interval, not a band around the cap. Rooms do not fail independently: a database going down is one event that every room notices at nearly the same instant, so an undithered backoff preserves that alignment perfectly and delivers the entire fleet as a single spike onto a server that is trying to recover, which re-synchronises the herd for the next round. Jitter added around a deterministic delay narrows the herd rather than dispersing it — the same spike with soft edges. A conforming implementation that drops the jitter still satisfies every other line of this table and is still wrong.

Q-063 and Q-066 bound different failure shapes and neither is redundant — the same relationship Q-058/Q-059 have. Whichever is reached first abandons the room:

Store failure modeWhat bounds itWhy the other does not
Fast (connection refused)Q-063 — 12 attemptsExpected elapsed is ~91 s, inside Q-066
Slow (connection timeout)Q-066 — 120 s12 attempts × a 30 s timeout is ~6 min of waiting

The ~91 s figure is the sum of the expected draws, which under full jitter is half each cap: (0.5 + 1 + 2 + 4 + 8 + 16)/2 + 5 × 30/2 s. Raising Q-063 far enough that its expected elapsed exceeds Q-066 would make it inert against a fast-failing store; lowering Q-066 below that figure would make Q-063 inert against every store. Neither may be tuned without recomputing the other.

Q-067 is not a state-preservation window, and this is the distinction that makes it necessary. ADR-072 holds a Dormant room’s activation to protect T0 and avoid replay. An abandoned room has no T0 to protect. Q-067 exists so that re-activation does not clock a fresh hydration attempt per inbound call — without it, callers addressing an unreachable room drive the retry rate instead of the room, and the backoff these quantities describe is bypassed entirely from outside. Q-067 at zero does not disable a safety margin; it deletes the backoff.

What is ruled versus what is still assumed. The backoff shape — exponential, full jitter, Q-065 base doubling to a Q-064 ceiling — is the ruled part and the part the tests enforce. Q-063 and Q-066 are a policy choice about how long a database outage is tolerated before a room stops trying, not a measurement, and two minutes is chosen to ride out an ordinary failover without retrying an unreachable store forever. They are marked Normative because a ruling set them, per this table’s own status definition — not because an outage-duration distribution has been measured. The measurement that would confirm or move them is observed failover duration in production, and there is no production yet.

Enforcement: HydrationBackoff.CapFor/Next (the draw, asserted as a distribution in HydrationBackoffTests rather than observed as a timing); RoomGrain.HydrateAsync (the two abandonment conditions); RoomHydrationOptionsValidator (a policy that could not run is refused at start-up).

Geometry cost — Q-015b’s ⊙3 refuted by measurement (2026-08-02)

Rule 2 of this table says entries are falsifiable claims, published in that form deliberately. This one has now been falsified, which is the mechanism working rather than failing.

Measured per fog-of-war advance over one 256 × 256 chunk (Q-010/Q-011), converted to this table’s units by Q-001 (20 advances per room-second, one observer advancing one chunk every tick):

Dynamic occludersPer advancems / room-secondvs Q-015a = 300
00.27 ms5.4ok
3212.14 ms242.8approaching
12820.01 ms400.2over
512 (Q-012 ceiling)41.71 ms834.22.8× over

For comparison, on the same host: one LOS ray is 0.4 µs at any occluder count, a 2,048-ray batch is 0.57 ms, and a 121 × 121 A* is 0.58 ms. (“At any occluder count” was re-measured on 2026-08-13 and is withdrawn — a full-traversal ray is 2.8 ns at zero occluders and 871 ns at Q-012’s 256. The comparison this sentence draws survives it by four orders of magnitude either way; see “The geometry crate re-measured”.) Fog advance is the only geometry operation whose cost is architecturally significant, and it is the one Q-015b was estimating.

Q-015b is not simply replaced with a number, because the quantity is configuration-dependent and one input is missing. The reference room is defined in this table as 50 viewers with 200 entities per interest set; it does not state a dynamic occluder count, and the table above shows the answer moves by two orders of magnitude across that axis. Deciding the reference room’s occluder count is a product question, not a measurement, and until it is answered Q-015b has a measured curve rather than a measured value.

What is already settled is that ⊙3 is wrong by roughly two orders of magnitude, and that Q-015a’s isolation ceiling is breached at 128 occluders by a single observer — well below Q-012’s permitted 512. Either the mask cadence ladder in §5.3 is not optional for ordinary rooms, or Q-012 is too permissive for per-tick advancement, or Q-015a is set too low. That is a ruling, not a measurement, and it is now the blocking one.

ADR-033 is what kept this from being an incident. Because geometry is dispatched fire-and-forget and consumed a tick later, a room carrying 41.71 ms of advance per tick was measured with a 28 µs tick body and zero overruns — the shortfall appeared as MaskStaleness, exactly as §5.2 specifies. The estimate being wrong by 278× did not stall a single tick.


Reference-point table (readability mitigation)

ADR-045 acknowledges that citing a Q-ID instead of a number costs skim-readability, and requires a reference-point table to mitigate it. These are the five most frequently cited scenarios, with the numbers and IDs side by side:

ScenarioKey numbers
Tick cadence50ms / 20Hz (Q-001, Invariant); mailbox occupancy target 28ms (Q-002); tick message p99 8ms (Q-003)
Reference room (50 viewers, 200-entity interest set per viewer)Segment B measured 0.675ms against a 5ms budget (2026-08-13, BENCH-03 — the ⊙3.04ms derivation was 4.5× high); marginal cost 12.43µs per viewer (Q-110); geometry isolation ceiling 300ms (Q-015a); Q-015b’s expected ⊙3ms was refuted by measurement on 2026-08-02 (a single fog advance at Q-012’s ceiling is 41.71ms = 834ms per room-second); Q-016’s ⊙0.08 cores keeps its ⊙ and loses its explanation — it gave Segment B ⊙95% of the total and Segment B measures at 17% of it
Visibility memory64 MiB per activation (Q-009); chunk 16 KiB (Q-010) = 256m × 1m cells (Q-011); the 50-viewer reference case (four parties plus the Keeper, ~16 resident chunks) is about 1.3 MiB
Lease protocolExpiry 40 ticks = 2s (Q-028, counted in tick sequence); renewal 500ms; peer grace buffer 150ms (Q-029)
Profile CC-Companion threshold ⊙8 inches (Q-045, awaiting measurement); touch target 44pt (Q-044); asset tiers are decided by ADR-005’s 2D bake obligation

Rules of use

  1. No bare numbers in the whitepaper — cite a Q-ID. A CI lint detects them by grep (allow-list: version numbers, years, and this table’s reference-point section).
  2. A ⊙-marked entry must not be cited as normative. They are falsifiable claims, published in that form deliberately — a concrete number that can be overturned is more useful than an unfalsifiable qualitative allocation. Until the corresponding benchmark completes, any normative sentence depending on one must state that dependency.
  3. A derived entry must record its formula. A “derived” value without one is a normative value wearing the costume of a derived one — which is exactly the shape of the error in Q-016’s original definition.
  4. Register a new number here before writing it into a document, not the other way round. The other way round is the cause of the entire Q-036…050 section.
  5. A pending entry has a deadline for disposition. A bound that is pending forever is operationally identical to no bound at all — the only difference is that the document looks more responsible.

Outstanding inventory

CategoryEntriesWhat it blocks
Awaiting measurement (⊙ → normative)Q-002, Q-005, Q-006, Q-008, Q-014, Q-016, Q-019, Q-026, Q-032, Q-033, Q-045, Q-080, Q-081, Q-085 — 14. Q-003 and Q-007 left this row on 2026-08-13: BENCH-01, BENCH-02, BENCH-03 and BENCH-05 ran (ADR-177)The rig existstests/core/Descent.Benchmarks, core/Descent.Geometry/benches, apps/vtt-frontend-client/bench, run weekly by .github/workflows/perf.yml. Nine of the fourteen are not blocked on a BENCH item at all and each now names what it is actually blocked on: Q-002/Q-005/Q-008 on Orleans message dispatch, which needs a live silo and a load generator (Q-112); Q-006 on the housekeeping messages other than the lease sweep; Q-014 on nothing measurable — it is an SLO rather than a cost; Q-016 on a fog-budget utilisation from real play; Q-019 on I/O; Q-026 on S6; Q-032 on a zh-TW corpus and a subsetter, neither of which is in this repository; Q-033 on real hardware — the Guardrail 7 low end, an integrated-GPU laptop and a mid-range tablet on a shared memory pool, since no web API reports resident VRAM; Q-045 on real devices, because inches need a device’s DPI; Q-080 on Studio; Q-081 on a product decision; and Q-085’s benchmark is not a BENCH item — it is the custom-asset size distribution, measurable from production upload telemetry rather than from a rig
Measured, awaiting a rulingEmpty as of 2026-08-06, and this row was stale for two days. Q-015b’s question — which of the mask cadence ladder, Q-012 or Q-015a had to move — was ruled on 2026-08-04: option (a), the ladder slows (Q-068Q-071 ×1.5), Q-012 stays at 256 because a ~112-occluder ceiling would be met by authoring rather than by abuse, and permeability ships. The row survived because the ruling was written into the Q-015b section and not back into this inventory
Pending values (normative but valueless)Q-024 (bytes), Q-025, Q-027, Q-031, Q-034, Q-035, Q-047, Q-048, Q-049, Q-050, Q-051, Q-052, Q-053, Q-054, Q-055, Q-072, Q-073, Q-074, Q-078, Q-079, Q-082, Q-086, Q-087, Q-088, Q-089 (added 2026-08-08) — 25. Q-075 and Q-076 left this row: both were measured/derived on 2026-08-04 (44 and 6) and are Normative. Q-026, Q-033, Q-080 and Q-081 carry both markers and are counted once, in the ⊙ row aboveThe enforceability of each owning ADR. Q-056 was measured on 2026-08-02 and has left this row (32,768; see §Q-056). The absence of Q-033 and Q-050 means the corresponding protections do not in fact exist, which ranks them above the rest. Q-051 / Q-052 decide what “stepping away briefly” actually feels like, and the two are coupled. Q-075 blocks ADR-091 from shipping — permeability must not go live before it is re-measured, or the token bucket admits advances at a cost that no longer holds. While Q-076 is undecided, ADR-094 clause 6 is “described but unenforced” — the default policy bounds only how much a single opposed roll discloses, and bounds the rate not at all
CI lints (ADR-045 Enforcement)Bare-number lint, ADR Status / reverse-link lint, normative-sentence enforcement-point lintAll three are implemented in tools/ (2026-08-01). Recording them as “blocked until project CI exists” was a misreading — all three checks defined in ADR-045’s Enforcement field operate on the whitepaper text; what needs a build is running them automatically per commit, not writing them

Re-measured 2026-08-06, and the figures below are the 2026-08-01 baseline rather than the current state. The lint now reports 129 unit-carrying quantities, 26 in a paragraph or table citing a Q-ID, and 22 distinct Q-IDs cited — against 114 / 1 / 5 when this note was written. So the uncited count is 103, not 113, and the number of distinct IDs actually appearing in the whitepaper has gone from five to twenty-two. The improvement was real and nobody recorded it, which is why docs/README.md was still quoting 113 five days later. The disposition question below is unchanged; only its size is.

What the bare-number rule actually measures (tools/bare_number_lint.py, 2026-08-01 — historical). Rule 1 above requires the whitepaper to cite a Q-ID throughout. In fact: the document carries 114 quantities with units, of which 113 sit in a paragraph or table citing no Q-ID at all; only 5 distinct Q-IDs appear anywhere (Q-020, Q-032, Q-051, Q-052, Q-053) against this table’s 55 entries. The rule has never been applied to the whitepaper body.

Rewriting all 113 is not obviously the right disposition: 33 of them sit inside §5.2’s Table A / Table B, whose entire purpose is to show their own arithmetic (0.25ms + 0.40ms + … = 3.04ms). Replacing the intermediate values of a derivation with Q-IDs would make the derivation impossible for a reader to check — that is, executing the rule literally would destroy the very auditability it exists to protect.

This is therefore a matter awaiting a ruling, not a cleanup. There are at least three possible dispositions: (a) require citation only for normative and ⊙ entries, exempting derivation tables and explanatory arithmetic; (b) require each paragraph that introduces a quantity to cite a Q-ID at least once, with repetitions exempt; (c) keep the rule as written and accept a large-scale rewrite. Until it is ruled on, the lint defaults to measurement mode (always exits 0) and only --strict blocks.

Q-034 and Q-049 split into two bounds — 2026-08-06 (ADR-105)

ADR-087 left both pending because “a quota bounding legitimate high volume and abuse with one number encodes a compromise in a constant nobody will later recognise as one”. That objection was correct and one step short: it is equally a reason to have two numbers.

Abuse boundCost bound
Protectsother tenants, and the disclosure surfacethe bill
Derived fromthe shape of legitimate behaviourmeasurement, then commercial choice
Scales with entitlement tierneveryes
Blocked on the tier modelnoyes
  • Q-049 = 2 concurrent checkouts per account, across all rooms. Normative, decided — not ⊙, because it estimates nothing. A checkout is Keeper-driven with a preview to read (ADR-075 builds the candidate outside the refusal window precisely so a human can look at it). Two covers one person running two tables; fifty-room scripted traversal — §10.3 face 6 — is infeasible. Q-013 bounds the per-room rate and cannot see across rooms.
  • Q-034 = 1 concurrent export per account. Normative, decided. An export is an offline job delivered by a short-lived link; there is no user-visible benefit to two at once, and concurrency is the term that turns a daily allowance into a burst against the shared pool.
  • Q-034’s daily allowance stays Pending, and its absence now blocks nothing but a price list. With concurrency at 1 the burn rate is bounded by the offline pool’s own capacity regardless of the daily figure.

An abuse bound that money can raise is a cost bound wearing the wrong label, and the tenant it protects is not the one paying. That request will be made; this row is the answer to it.

Knowledge corpus and CDC bounds (Q-103…Q-105, Q-107) — added 2026-08-12, ADR-168 / ADR-169

IDNameValueSourceStatus
Q-103KNOWLEDGE_SEARCH_LICENCES_MAX32ADR-168; one entitlement read per named licence against the Marketplace replicaNormative
Q-104SEARCH_TENANT_TOKEN_TTL_SECONDS300ADR-168; the window in which a revoked licence stays searchableNormative
Q-105KNOWLEDGE_CHUNK_TEXT_MAX_CHARS8,192ADR-168/169; one outbox payload, and therefore one write-ahead-log recordNormative
Q-107KNOWLEDGE_INDEX_BATCH_MAX512ADR-169; chunks held by the replication consumer before a flushNormative

All four are refusal choices rather than estimates, the same category as Q-095 and Q-096: each says what this platform will not accept, not what something is expected to cost. None is and none is waiting on a benchmark.

Q-104 is the one that is not a bound but a lag, and calling it a TTL hides that. The search engine validates a token; it does not re-ask whether the licence is still held. So this is the interval for which a revoked entitlement remains searchable — a hole ADR-168 bounds rather than closes, and the honest way to read the row is “how incomplete a takedown is allowed to look”. Shortening it costs one token mint per interval per active searcher; lengthening it widens the hole. ADR-097’s rule that nothing may be cached beyond a room’s lifetime is not violated and is not satisfied either — the token is a cache, and this number is its size.

Q-103 bounds amplification, not correctness. Each named licence is one keyed read on the replica (§2.5 grants a lookup, never an enumeration), so an unbounded list turns one authenticated request into a burst against the database the whole of §2.5 exists to protect. Refused rather than truncated: a silently shortened list mints a token omitting a licence the caller does hold, which surfaces to a player as “my rulebook is missing from search” and is undiagnosable from the client.

Q-105 reaches further than it looks. It bounds the chunk, therefore the outbox row, therefore one write-ahead-log record, therefore the memory one replication message costs to decode. Refused rather than truncated because §7.2 promises “exact text citations” and half a chunk is a citation that stops mid-sentence.

Q-107 only binds a backfill. Ordinary traffic flushes at its transaction’s commit, which is also the only point the replication slot is advanced; this ceiling exists so that one INSERT … SELECT over a whole corpus cannot be held entirely in the consumer’s memory before anything is indexed. Flushing early costs at-least-once delivery of the early part if the consumer then dies, which the index tolerates because an upsert is keyed on ChunkId.

Enforcement (ADR-045): KnowledgeSearchOptionsValidator and KnowledgeCdcOptionsValidator refuse a host that has not set Q-103 or Q-107, and MeilisearchOptionsValidator refuses a non-positive Q-104 — none of the three ships a default, following §3.1’s rule for the geometry limits. Q-105 is enforced in KnowledgeChunkCodec on both sides: Encode throws and TryDecode refuses, so a payload written by an older version or by hand cannot put an unbounded string into the index. KnowledgeChunkCodecTests.Text_over_Q_105_is_refused_on_the_way_in_and_on_the_way_out and its ..._at_exactly_Q_105_is_accepted partner pin the boundary in both directions.

AI session bounds (Q-106, Q-108, Q-109) — added 2026-08-12, ADR-170

IDNameValueSourceStatus
Q-106AI_SESSION_HISTORY_TURNS_MAX64ADR-170; turns retained in a session’s persisted stateNormative
Q-108AI_PROPOSAL_HIT_POINT_DELTA_MAX9,999ADR-170; the largest magnitude a decoded proposal may carryNormative
Q-109AI_PROPOSAL_TEXT_MAX_CHARS512ADR-170; one prompt, one ability name, one narrationNormative

Q-106 counts turns and not exchanges, so one ask and one answer are two. The session refuses at the ceiling rather than evicting its oldest turn, and the direction is the decision: silent eviction changes what the model was told without the game master knowing, so the same question starts producing a different answer because context they remember giving is gone. A full session says so, and starting a new one is a visible act.

Q-108 and Q-109 bound a language model’s output, which is the least trustworthy input in this system. Both are refusals rather than clamps: a model that emits a delta of two million has misunderstood the request, and clamping it to Q-108 would show a game master a plausible proposal derived from an implausible one. Q-109 also bounds what a turn costs to persist, since the history is grain state written on every deactivation.

What these do NOT bound, stated because the gap is the important part: there is no rate limit and no token quota. §10.3 names per-room and per-player daily consumption quotas as the defence against prompt spamming. One in-flight turn per session and Q-106 together bound concurrency and conversation length; neither bounds how often a player may ask. With the offline mock provider the cost of that gap is CPU. With a real provider it is money, and ADR-170 records the quota as something that must exist before one is configured.

Enforcement (ADR-045): AiSessionOptionsValidator refuses a host that has not set Q-106; AiSessionGrainTests.A_session_at_Q_106_refuses_rather_than_evicting_its_oldest_turn asserts both halves — the refusal, and that nothing was dropped to make room. Q-108 and Q-109 are enforced in AiCombatActionCodec.TryDecode, with A_delta_at_exactly_Q_108_is_accepted_in_both_directions pinning the sign as well as the magnitude.

The measurement rig’s first results (Q-110…Q-112) — added 2026-08-13, ADR-177

BENCH-01, BENCH-02, BENCH-03 and BENCH-05 have run. They were outstanding from W1 and this table has named them as the blocker on sixteen rows since it was created. What follows is what they measured, what it settles, and — at greater length, because it is the part that matters — what it does not.

The machine, stated first, because every absolute below is a property of it. AMD Ryzen 7 7735HS, 8 physical cores, Windows 11 26200, .NET 10.0.10 (x86-64-v3 RyuJIT), Release, BenchmarkDotNet 0.15.8. tools/perf-baseline.json gates ratios and publishes absolutes for exactly the reason Q-075’s entry gives: the multiplier transfers between machines and the milliseconds do not.

§5.2 Segment B, term by term (50 viewers, 200-entity interest set)

TermDerivedMeasuredvs derived
AOI query + per-viewer delta comparison, every slot changed⊙0.65 ms0.342 ms0.53×
— the comparison alone, nothing changed⊙0.40 ms0.092 ms0.23×
FlatBuffers assembly, one packet per viewer⊙2.00 ms0.354 ms0.18×
cartridge MessagePack sub-payload, shared (D-3)⊙0.30 ms0.034 ms0.11×
per-silo grouping (ADR-032)⊙0.02 msnot built
per-viewer digest hash (§10.4)⊙0.02 msnot built
outbound allowlist recomputation (§9.6.3)⊙0.05 msnot built
Segment B — the four implemented terms⊙3.04 ms0.675 ms0.22×
Segment B — what AdvanceTickAsync’s own body runs0.332 ms

The pre-registered criterion was “within 0.5×–3× of the analytical prediction counts as confirmation; > 3× requires reallocating Table A’s budget”, and the result is outside it on the side the criterion did not anticipate. 0.22× is below the 0.5× floor. The budget holds — 0.675 ms against 5 ms is 7.4× headroom — and the derivation it holds by is wrong by a factor of four and a half. Recorded as a miss rather than as a pass, because the band was two-sided and only one side was given an action, and a criterion checked only in the direction somebody feared is not a criterion.

Three of the seven terms measure nothing because the work does not exist. Verified 2026-08-13 by search across apps/vtt-backend-silo and modules/: RoomSnapshotDispatcher sends per viewer to that viewer’s own connection with no bucketing step, there is no XxHash3 and no digest field anywhere in the silo, and §9.6.3’s pairwise mask is not implemented. Their derived total is ⊙0.09 ms, 3% of Segment B. They are named and left unmeasured rather than approximated (P7): a stand-in would publish a figure describing code nobody has written, and the substitution would be invisible in the row above.

Two Segment B figures, because Table A budgets MAILBOX OCCUPANCY and the derivation costed the WORK. RoomGrain.ReplicateToViewers ends at SendDeltas/SendSnapshots, both _ = …Async(…); the wire encode happens inside the transport sink and the cartridge payload rides ADR-092’s decoupled path. So the tick’s own frame runs 0.332 ms of the 0.675. Whether the fire-and-forget continuation still occupies the activation is NOT settled here — Orleans resumes a grain’s continuations on its own scheduler, and how much runs on the caller’s stack depends on whether the transport’s ValueTask completes synchronously. That needs a live silo, and it is recorded as open rather than assigned to a segment by assumption.

Q-110 — Segment B’s per-viewer marginal cost

IDNameValueSourceStatus
Q-110SEGMENT_B_MARGINAL_US_PER_VIEWER12.43 µs (as derived) / 6.06 µs (mailbox body), at a 200-entity interest setMeasured 2026-08-13, SegmentBBenchmarksNormative

This is the number §5.2 needed and did not have. The whitepaper’s 50 seats are a derivation premise, and §5.2 says in as many words that raising the count is a trade — “Raising the seat count therefore requires lowering the per-player rate or making intent handling cheaper — the trade is explicit rather than discovered under load” — and then never fixes the seat count, while §4.4’s admission control weighs rooms by viewer count against a quantity that did not exist.

Measured across four seat counts, so the line is checkable rather than assumed. At 1 / 8 / 25 / 50 viewers Segment B is 61.5 / 152.7 / 355.1 / 674.7 µs. The secant from 8 to 50 gives 12.43 µs per viewer; a least-squares fit over all four gives 12.46 µs with an intercept of 49.3 µs and predicts the 1-viewer point to within 0.4%. The relation is linear over the measured range, which is what makes the next entry arithmetic rather than extrapolation.

The fixed term is the interesting half. ~49 µs of Segment B does not scale with viewers at all — it is WorldDeltaAssembler.Project, one pass over the room per tick — and it is why a one-viewer room costs 61 µs rather than 12.

Q-111 — the seat count Segment B’s own budget admits

IDNameValueSourceStatus
Q-111SEGMENT_B_ADMITTED_SEATS / _ENTITY_BOUND398 seats / 200 entitiesDerived: see the formula belowDerived

Registered as a pair, because a seat count with no entity bound is not a bound. Segment B scales with viewers and with what each viewer is shown; 398 is the figure at §5.2’s 200-entity interest set and it means nothing detached from it.

The formula, because rule 3 requires one:

Q-111 = 8 + (5,000 µs − 152.709 µs) ÷ 12.4287 µs per viewer
= 8 + 390.0
= 398 seats, at 200 entities per interest set

It is NOT the room’s seat ceiling and must never be cited as one. It is what one term’s budget admits. Three constraints sit between it and a real ceiling and none was measured by this round:

  • Orleans message dispatch. Q-112 measures the application half of an intent and not the grain call around it. At 398 seats and Q-008’s 10/s that is ~3,980 messages per second into one single-threaded activation, and nothing here says that is possible.
  • Bandwidth. Q-056’s derivation puts §5.2’s reference interest set at 8,068 bytes, so 398 viewers at Q-001’s 20 Hz is ~64 MB/s per room, against a budget ADR-014 owns and this round did not evaluate.
  • Room size against interest-set size. WorldDeltaAssembler.Assemble iterates every slot the room has ever assigned, with the AOI test inside that loop rather than in front of it — so the delta pass is O(viewers × room) where the derivation assumed O(viewers × interest set). The two agree only when the room is the interest set, which is the shape of the reference room and therefore the shape this figure was taken on. A room holding 2,000 entities of which each viewer sees 200 is not covered by any number here.

The seat-count / intent-rate relation, carried here so the next person can see the price rather than rediscover it. §5.2’s trade is seats × intent rate × per-intent cost ≤ Q-005. With Q-112 measured at 0.394 µs, the application half of that product at 398 seats and 10/s is 78 µs against Q-005’s ⊙15 ms — four orders of magnitude of slack, on the half that was measured. The trade §5.2 describes is therefore not paid in intent handling at any seat count this rig can reach; it is paid in whatever the unmeasured dispatch costs.

Q-112 — what one player intent costs in the application layer

IDNameValueSourceStatus
Q-112INTENT_APPLICATION_US0.394 µs (decode 0.363 + lease 0.021 + move + release)Measured 2026-08-13, IntentPathBenchmarks (BENCH-05)Normative as a lower bound — see below

D-4 assumed ⊙0.3 ms per intent and the application half is 0.394 µs — 760× cheaper. The decode dominates it: 363 ns of the 394 is SubmitIntentCodec.TryDecode, and the whole lease protocol is 21 ns to grant or renew and 7 ns to refuse. Lease contention is cheaper than granting, because a refusal finds the blocking grant and stops.

Normative as a lower bound, which is a status this table has not used before and needs. An intent is a grain message and its true mailbox occupancy is this plus Orleans’ own dispatch — deserialisation, the activation’s scheduler, the turn boundary. Standing a TestCluster up inside a BenchmarkDotNet harness measures the test host rather than a deployed silo, so that half is not measured and not estimated. Marking the row Normative outright would claim a completeness it does not have; marking it would claim nothing was measured. The bound is real, and it is a bound.

What would close it is the load-generation tool §14.8’s T4 tier is for: a real silo, real connections, Q-008’s rate at Q-111’s seat count, and tick jitter observed rather than computed. It does not exist. Named here rather than left implicit, per docs-and-adr-workflow.md §4.

BENCH-01: Q-007’s ceiling of 1,000 holds, with 4.5× to spare

Effects in the batchpremise validation + event constructionapplyper Effect
1471 ns238 ns709 ns
10036.0 µs26.4 µs624 ns
1,000 (Q-007)382.0 µs288.4 µs670 ns

The criterion was Q-007; if > ⊙3 µs, the ceiling of 1,000 must be lowered”. Measured 0.67 µs, so it is not lowered. At the ceiling a full batch costs 0.670 ms against Q-003’s Segment A budget of ⊙3 ms — 4.5× headroom, on the same order as Segment B’s.

The batch does not amortise, and that is worth knowing before someone raises Q-007. The unit cost is 709 / 624 / 670 ns across three orders of magnitude of batch size, so the two staging dictionaries and RoomAuthorisedSchema.Compose are not what a batch is paying for — the cost is per proposal and scales cleanly. Q-007 can therefore be reasoned about linearly, which the derivation assumed and nothing had checked.

The number this row does NOT contain is the channel write. W1 defines the unit as “premise validation, event construction, channel write”, and the third is RoomGrain’s _outbox — a List<DomainEvent> add behind a capacity check, inside a grain a BenchmarkDotNet harness may not host for the reason Q-112 gives. An add to a grown list is nanoseconds against a 670 ns unit, so the omission is small; it is stated because “small” is a judgement and the row would otherwise read as complete.

The allocation is the part that should be watched. A full 1,000-Effect batch allocates 1.66 MB and lands 9.8 Gen1 collections per thousand batches. Q-007 is a per-tick ceiling, so a room that reaches it is asking for 33 MB/s of garbage from Segment A alone, on top of Segment B’s 28 MB/s. Neither figure had ever been taken.

What BENCH-02 decided about D-2, and how close it came to the other answer

Viewerscontiguous arrayDictionary of reference-typed transformsratio
1678 ns2,456 ns3.62×
85.12 µs19.69 µs3.84×
2516.03 µs62.55 µs3.90×
5032.24 µs147.38 µs4.57×

D-2 stays normative, and the margin is 21%. The pre-registered criterion is explicit: the derivation predicted a 5×–25× gap, and “if the gap is < 3×, W1/D-2’s normative requirement can be demoted to a recommendation”. Measured 3.62×–4.57×: above the demotion threshold, below the predicted band. Both halves are recorded because both are true — the rule survives, and the number it was written on was optimistic by roughly a factor of two.

What the ratio buys, in Segment B’s own units: at 50 viewers the dictionary layout costs 115 µs more per tick, which is 2.3% of the 5 ms budget rather than the “delta pass alone consumes the whole segment” the derivation predicted. That prediction assumed ⊙20–50 ns per comparison against ⊙2 ns; the measured contiguous pass is 32.24 µs for 10,000 slot comparisons = 3.2 ns, and the dictionary is 14.7 ns. The per-comparison figures were the right shape and the dictionary end was 2–3× pessimistic.

The dictionary arm is built to be as good as a dictionary gets, and that matters for a result this close to a threshold: the same EntityId key the production slot map uses, pre-sized to its final capacity so neither arm pays a rehash, entries allocated in slot order. What it does not get is the half of ADR-023’s rule that says “or boxed values” — the transform is a class, so a comparison is a hash lookup and then a pointer chase. A real deployment accumulates those entries as players join over a session, so the heap layout gets worse rather than better.

Enforcement point (ADR-045): Descent.ArchitectureTests forbids the production baseline from using a dictionary; BaselineLayoutBenchmarks is where the rejected design is allowed to exist, and tools/perf-baseline.json gates the ratio at ≥ 3.0 — the demotion threshold itself — so a change that closed the gap turns the T4 job red rather than quietly making a normative rule pointless.

D-3 measured: what sharing the cartridge payload actually saves

Viewersshared once per entity (D-3)re-serialised per viewerratio
135.0 µs35.3 µs1.01×
835.0 µs298.9 µs8.55×
2533.7 µs852.5 µs25.3×
5033.6 µs1,701.6 µs50.7×

The shared arm is flat and the other is linear, which is the measurable form of “O(entities), not O(viewers × entities)”. §8.2 states that as a normative requirement; this is the first time it has had a price. At 50 viewers D-3 saves 1.67 ms per tick — a third of Segment B’s entire budget — which is more than every implemented term of Segment B put together.

It is also the rule most likely to be broken by accident, because breaking it looks like moving one call inside one loop. The ratio is gated in tools/perf-baseline.json for that reason.

The geometry cost §5.2 Table A has no row for

ADR-033 moved geometry out of the mailbox and §5.2 budgets the consequence in Table B, per room-second. Table A gives the tick nothing at all for geometry, and that is not quite right: RoomGrain.AdvanceTickAsync crosses the P/Invoke boundary’s near side twice on every tick — Stage 2 copies the occluder set into a job and enqueues it, Stage 3 copies Q-010’s 16 KiB mask out of the published slot. Neither runs native geometry; both are on the mailbox.

0 occluders256 occluders (Q-012)
dispatch + read the published mask (on the mailbox)487 ns517 ns
— the 16 KiB mask read alone (Q-010)217 ns161 ns
full FFI round trip, dispatch → published (off the mailbox)0.264 ms24.24 ms

The tick pays ~0.5 µs per observer dispatch, and it is not worth a Table A row — but the absence should be a stated absence rather than an oversight. 0.5 µs against Q-002’s 28 ms is 0.002%, and it is flat in Q-012 because the copy is dominated by the fixed 16 KiB rather than by the occluder set.

The round-trip row is a LATENCY, not a cost, and nothing in Table A may be compared against it. ADR-033’s whole point is that the room does not wait for it. It is here because it is the first figure taken through the P/Invoke boundary the silo actually uses, and because it corroborates the crate’s own numbers from the other side: 0.264 ms at zero occluders against the registry’s 0.26 ms, and 24.24 ms at Q-012 against the 29.37 ms taken on the 2026-08-02 box — the same shape, on a machine Q-075’s entry already records as faster.

This row is here because the first version of it was wrong and plausible. The round-trip arm originally waited for any published mask rather than the one it had just dispatched, so it returned on the previous iteration’s result and reported 440–515 ns — within a hundred nanoseconds of the near-side row above. Two benchmarks measuring different things do not agree to that precision, and that is what gave it away rather than any assertion. Corrected before anything was published; recorded because a plausible-looking number is exactly what this rig exists to stop the corpus acquiring.

The allocation figure nobody had, and it is the one worth watching

Segment B’s four implemented terms allocate 1,418,208 bytes per tick at 50 viewers, which at Q-001’s 20 Hz is 28.4 MB/s per room. The mailbox body alone is 937,808 B per tick, 18.8 MB/s.

was never the only thing missing from these rows; a garbage rate was. Nothing in §5.2 budgets allocation, and a Gen0 collection landing inside a tick is exactly the jitter Table A’s ≥22 ms Reserve row exists to absorb — so the figure belongs beside the timings rather than in a footnote. Recorded rather than acted on: whether 28 MB/s per room matters depends on rooms per silo, which is Q-016’s question and is not settled here.

The two largest contributors are both known and both fixable in principle. PerViewerSnapshotCodec.Encode constructs a fresh 8 KiB FlatBufferBuilder per call where WorldDeltaCodec reuses one, and WorldDeltaAssembler.Assemble allocates three List<>s per viewer per tick. Neither is a defect today and neither has a Q-ID; they are named so that a future change has somewhere to start.

The geometry crate re-measured, and what reproduced — 2026-08-13, ADR-177

Four figures in this table were taken once, by hand, on one machine, and have been load-bearing ever since. core/Descent.Geometry/benches/ re-takes all four under criterion, on the pinned 1.97.1 toolchain, with confidence intervals. Three reproduce. Two do not, and one of those is a claim rather than a number.

Same box as the .NET figures above — AMD Ryzen 7 7735HS, --release, rustc 1.97.1. It is the same machine class Q-075 was measured on, which is the first thing the run established rather than assumed: Q-075’s entry records the impermeable advance at 12.99 ms where 2026-08-02 recorded 29.37 ms, and this run measures 13.065 ms. Two independent measurements of the same quantity, 0.6% apart, taken eight days and one harness apart.

Reproduced

QuantityRecordedRe-measured
Impermeable fog advance at Q-012 = 25612.99 ms (Q-075’s box)13.065 ms0.6% apart
A* worst case at Q-058 = 4,096, region exhausted6.1 ms5.21 mson a box this run shows to be ~1.2× faster
ADR-143’s WebAssembly slowdown, compute-bound5.32×5.47×2.8% apart

ADR-143’s ratio is the one that most needed this. It declined the universal WebAssembly geometry core — and with it the deletion of the C ABI, the [LibraryImport] surface and the only AllowUnsafeBlocks project in the tree — on a single measurement of 5.32×. It now has a confidence interval and a weekly re-run. Both workloads are reported because they differ: compute-bound is 5.47× (native 2.68 µs, wasmtime 14.67 µs) and call-bound is 5.76× (20.9 ns against 120.4 ns). Q-015a’s budget is compute, so 5.47× is the figure that divides it; the call-bound arm exists because a 20 Hz dispatch loop pays that cost on every query and reporting only the first would hide a real term.

The two hosts are asserted to AGREE before any ratio is reported. ADR-017 makes a disagreement the defect rather than the benchmark’s problem, and a speed comparison between hosts that answer differently means nothing.

Not reproduced, and both are worth acting on

1. The fog-cost model’s coefficients do not transfer between machines, and its intercept is not the zero-occluder cost. This table publishes cost ≈ 0.26 + 1.81·√n ms and says it “may be used to interpolate an untested occluder count” because the square root is BVH traversal rather than a convenience fit. The shape reproduces and the coefficients do not. On this box:

Occluders032128256
measured0.215 ms6.475 ms9.377 ms13.065 ms
published model, rescaled to this box0.115 ms4.64 ms9.17 ms12.90 ms

A fit over this box’s own points gives cost ≈ 2.68 + 0.633·√n ms, within 5% at every point — and its intercept is 2.68 ms against a measured zero-occluder cost of 0.215 ms, an order of magnitude apart. The published model set its intercept to the zero-occluder reading and fitted only the coefficient, which happened to work on one machine.

The consequence is narrow and should be stated narrowly: interpolate within a machine, never across one. The √n shape is confirmed where it matters — 128 and 256 land within 2% — and the low end does not follow the same line here. Nothing derived from this model changes, because everything derived from it (Q-012’s reduction, the Q-068Q-071 ladder) used the 256-point measurement directly rather than the interpolation.

2. Q-075’s permeable multiplier re-measures at 1.25×, not 1.4×.

Occludersimpermeablepermeableratiorecorded 2026-08-04
163.177 ms3.170 ms1.00×1.1×
648.829 ms9.657 ms1.09×1.2×
1289.336 ms11.612 ms1.24×1.3×
256 (Q-012)13.604 ms17.052 ms1.25×1.4×

The ladder is NOT re-derived on this, and the refusal is the decision. Q-075 = 44 ms was set as 29.37 ms × 1.5, “taking the upper end because the conservative direction for a cost is the larger one”, and on that basis Q-068Q-071 were slowed by half — which cost §9.2’s fog cadence, moved epic_presentation_smooth_fog.md’s crossfade from 200 ms to 300 ms, and grew a recorded token/mask desync from 250 ms to 350 ms. At 1.25× the allowance would be 300 ÷ 36.7 = 8.2 → 8 advances per room-second rather than 6, and the ladder could be given a third of that presentation cost back.

Two measurements of one ratio, 12% apart, is not enough to move a shipped ladder, and the direction of the disagreement is the safe one: 44 ms is conservative, so nothing is currently under-budgeted. What would settle it is repetition, which now existsperf.yml re-takes this weekly and tools/perf-baseline.json bands the ratio at 1.10–1.60, so a run outside that is a finding rather than a shrug. Recorded here rather than acted on, because re-deriving a ladder on a single run is the failure mode Q-058’s entry warns about in the other direction.

The claim that is refuted rather than re-measured

§5.2 Table B says LOS is “0.4 µs per ray” and — in this table’s own geometry-cost section — “one LOS ray is 0.4 µs at any occluder count”. The independence claim does not hold:

Occluders0256 (Q-012)
one unblocked ray crossing the whole chunk2.8 ns871 ns

311× apart, so “at any occluder count” is false, and the published 0.4 µs sits between the two ends rather than describing either. The measured ray is the worst case by construction — unblocked, so the traversal runs to completion, where a blocked ray short-circuits on its first hit and measures the early exit. examples/wasm_vs_native.rs already draws that distinction for its own two workloads; the LOS claim never did, and that is most likely where 0.4 µs came from.

Nothing rests on it, which is why this is a correction and not a finding with consequences. The figure appears only in the sentence that calls LOS and A* “not significant by comparison” with fog advance, and at 871 ns against 13 ms that comparison holds by four orders of magnitude whichever end is quoted. The claim of independence is what is withdrawn.

Q-099 / Q-100 in a real browser — 2026-08-13, ADR-177. Still , and now for a different reason

The registry named the benchmark that would make these normative: “the same script run under Playwright against the Guardrail 7 device matrix, reporting a distribution rather than a best-of-five”. apps/vtt-frontend-client/bench is the first two-thirds of that sentence — a real Chromium, a real crypto.subtle, the real wasm artefact, and a distribution.

Two runs, headless Chromium on this box, 100 batched samples each:

minp50p95maxspread
crypto.subtle (Q-099)0.171 ms0.194 / 0.215 ms0.40 / 0.47 ms0.88 / 1.05 ms366% / 410%
wasm decoder (Q-100)4.74 ms4.94 / 7.44 ms8.7 / 9.9 ms8.8 / 10.2 ms80% / 73%

The ratio holds and is larger in a browser than in Node. At the minima it is 27.7× against Node’s 19.5×; at the p50s, 23×–35×. ADR-153’s finding is structural and this strengthens it: WebAssembly has no AES instruction in any shipped or proposed extension, so RustCrypto’s fixslice software cipher is the best any wasm build can do, and a browser’s crypto.subtle is not slower than Node’s in a way that closes the gap.

They stay , and the residual is now precise rather than general. The old reason was “the run was Node and the claim is about a browser”. That is answered. What is left is two things and both are in the table above: the spread, which is 366–410% on the fast arm and makes a p50 unstable between runs on one machine (4.94 ms and 7.44 ms are the same benchmark twenty minutes apart), and the device matrix, which is Guardrail 7’s and does not exist. The minima are the stable statistic and are what the ratio above is taken at.

Timer quantisation was the first thing this measured, and it is worth recording. A browser clamps performance.now() to roughly 100 µs unless the page is cross-origin isolated — a Spectre mitigation — so the first run reported crypto.subtle at exactly 0.100 / 0.200 / 0.300 / 0.400 ms: four values, all multiples of the clock, and a “distribution” describing the timer. The suite now times batches of 100 and divides. tools/bench-asset-decoder.mjs’s Node figures are not affected — Node’s performance.now() is not clamped — which is one more way the two environments differ that the ⊙ note did not anticipate.

Q-033 is not advanced and the probe explains why rather than producing a number. What it recorded on this run is the point: navigator.gpu is present and requestAdapter() returns null, and WebGL2 reports ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device ...)) — a software rasteriser whose “VRAM” is host RAM. No web API reports resident GPU memory at all, deliberately, because it is a fingerprinting surface. Q-033 can only be measured by allocating until something breaks on the hardware the answer is about, and that hardware is Guardrail 7’s low end: an integrated-GPU laptop and a mid-range Android tablet on a shared memory pool. A figure taken under SwiftShader would describe the runner and would be wrong in the direction that looks safe.