docs(meta): body-map-viewer workshop — rounds, measurements, outcomes, as-built briefing

The complete workshop record: four round-1 positions, five round-2 syntheses
(incl. Troblum's adversarial pass with addendum + final scorecard — all seven
findings resolved), both lead interviews, Qatux's round notes and the 8-section
workshop-outcomes.md (the lakes message-crossing documented as process
history), measurement ⑥ (set_pixel/c1) + the population-survey and chunk/S2
addenda in the measurement docs, the brief's appendix updated through ⑥, and
architecture-briefing-final.md — Jeroen's outline written back as-built
(six-level ladder, lakes, ~9MB resident global tier). README row: Complete.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 10:57:45 +02:00
co-authored by Claude Fable 5
parent b613443d51
commit 29c22cb728
20 changed files with 8689 additions and 3 deletions
+1 -1
View File
@@ -36,4 +36,4 @@ docs/workshops/
| [v0.1 Content Scoping](v01-content-scoping/) | — | Complete (2 rounds + closing, 38 tickets created) |
| [Art Direction & Mood Board](art-direction-mood-board/) | — | Complete (3 rounds + closing + technical session, D-019 amended, D-043-D-052 confirmed) |
| [Control & Interaction Scheme](control-interaction/) | — | Brief ready |
| [Body Map Viewer](body-map-viewer/) | — | Brief signed off (2026-07-25); round 1 gated on measurements T-1177/T-1178/T-1154/T-1179 |
| [Body Map Viewer](body-map-viewer/) | — | Complete (2 rounds + 2 lead interviews + post-ratification review; D-255 confirmed + 12 amendments; [outcomes](body-map-viewer/workshop-outcomes.md)) |
@@ -0,0 +1,309 @@
---
title: "Body Map Viewer — Araminta Round 1"
description: "Named-feature encoding, per-gridunit payload schema, and encoding continuity across steps — argued from the measured wire-size table (T-1179) and hydrology cliff proposal (T-1177)"
workshop: body-map-viewer
round: 1
author: Araminta
status: complete
---
# Araminta — Round 1
Answering my three questions from the [workshop
brief](body-map-viewer-workshop-brief.md#participants). Grounded in T-1179's
measured wire table, T-1177's cliff representation proposal, the current
`DistrictWindowLayer` struct (`server/src/atlas/layer_proxy.rs`), and the
carrier three-way rule (`docs/architecture/river-courses-t1170.md`, Ruling
1c). Keep clean, layer detail later — but the wire contract is exactly the
kind of "invisible until it's wrong" decision that deserves the extra pass
now, because every renderer downstream inherits whatever we lock here.
---
## 1. Named-feature encoding (THE wire decision)
**Verdict: don't treat this as one decision. It's three, because rivers,
settlements/POIs, and roads don't share a truth model — the carrier
three-way rule already sorted them, and my job is to encode each correctly,
not force a single scheme across all of them.**
### Settlements + POIs + their names → ids-with-lookup, whole-body family (rule i)
This is *already built and already correct* — I want to name that explicitly
so the workshop doesn't accidentally relitigate a solved problem. Names live
in `atlas_city_names` (D-223), a names-only pool with no geometry or
population, read once per body via `handle_city_names_request`
(`atlas_data_proxy.rs`). Settlement *geometry* (footprint aggregates:
density, dominant type/zoning) rides the whole-body `SettlementLayer`
family (D-226 T-1112/T-1119) — rung-independent, computed once, filtered
client-side by zoom.
This is rule (i) by construction: settlements are discrete, finite,
rung-independent, valid forever once placed. **Encoding: a small integer id
per settlement, resolved against the whole-body name pool the client already
holds.** Never inline the string into every gridunit that happens to be
near a city — that's paying string bytes on a per-cell raster for something
that changes at most a few hundred times per body. POIs (once they exist,
per the outline's "later" scope) follow the identical shape: id + whole-body
lookup, not per-gridunit inline text.
**What the per-gridunit payload carries instead: presence, not identity.**
A gridunit near a settlement doesn't need to know the settlement's *name*
it needs to know *"a settlement occupies me"* so the map-art function can
render footprint fill/color, and the **id** so the client can join against
the lookup it already has cached (for label placement, click-through, the
sidebar). See §2 below for the exact field.
### Rivers → sparse feature list, windowed payload, ids (not inline names)
Rivers are rule (iii) — rung-indexed invention (T-1170, Ruling 1c) — which
already settled the carrier (`DistrictWindowLayer.courses: Vec<RiverCourse>`,
outside the windowed-family ceiling, per the same echo key as the six dense
arrays). I have nothing to add to the *geometry* carrier — Tyre's ruling is
correct and I'm not reopening it. My scope is the **naming** half, which
Ruling 1c's `RiverCourse` struct doesn't currently carry at all (`edge_id`,
`class`, `points`, `terminus` — no name field).
**River names: same ids-with-lookup shape as settlements, one level indirect.**
`edge_id` is already the stable identity (`river_course::pack_cell_id`,
stable across every window/rung shipping that edge, per the doc). Do not
inline "Kelvin's Run" into every `RiverCourse` polyline on every window that
happens to cross it — a trunk river crosses dozens of windows over its
length, and the string would repeat dozens of times for zero new
information (the same argument PNG encoding wins for the dense fields:
don't pay bytes for something the client can dereference). Instead: name
assignment is a whole-body, rung-independent property of the **edge-id
graph** (rivers don't get renamed between gridunits), so it belongs in a
names-only whole-body lookup exactly like `atlas_city_names`
`river_names: BTreeMap<u32 /* edge_id or trunk-root id */, String>` fetched
once per body, joined client-side against the `edge_id` already on every
`RiverCourse`. This is additive to the existing whole-body atlas-names
request path, not a new mechanism — same shape, second table.
One nuance rivers have that settlements don't: a single named river usually
spans many `edge_id`s (a trunk plus its class-2 tributaries share a name;
"unnamed" streams don't get an entry at all). That's a **naming-assignment
generator concern** (which edges share a name), not a wire-encoding concern
— the wire only needs "this edge_id resolves to this name-table key or
none," so I'm not blocking on it; flagging it so Dudley's naming generator
(whenever that lands) knows the wire already expects a many-edges-to-one-name
join, not a strict 1:1.
### Roads → not in this workshop's scope, but the encoding falls out for free
Roads aren't measured or built yet (outline: "later, ... roads, railroads,
etc will also be in scope"), but worth stating now since it's free: roads
are graph-like exactly like rivers, so they inherit rule (iii) + the same
ids-with-lookup naming shape without a new design pass when they land. Not
claiming this as decided — just noting the pattern generalizes and nobody
needs to re-litigate named-feature encoding a third time.
### Why not dense classification rasters for any of these
The brief posed "dense classification rasters + sparse feature list vs
inline identity vs ids-with-lookup" as the open question. A dense raster
(e.g. a `settlement_id: Vec<u32>` per-gridunit array, one entry per cell)
is the wrong shape for anything that is **sparse and discrete** — settlements
and named rivers occupy a small fraction of gridunits in any canvas. T-1179's
own per-field RLE numbers make this concrete: `morphology`/`vegetation`
(genuinely dense, every cell has a value) still only manage 6 runs at 330K
cells because they're spatially coherent — a settlement-id raster would be
mostly a single "no settlement" sentinel with a handful of small filled
regions, which is exactly what a sparse feature list (a handful of
`{id, footprint}` entries) encodes in a few hundred bytes instead of
330K8.3M raster cells that are 99%+ one repeated sentinel. **Dense rasters
are for continuous fields (rule ii territory — biome, elevation, moisture);
sparse feature lists + ids-with-lookup are for discrete named things (rule
i/iii territory). Don't blend them — the moment "sparse feature" bleeds into
"raster," we're back to paying for information nobody asked for.**
---
## 2. The per-gridunit payload schema
**Baseline: the six fields `DistrictWindowLayer` ships today are exactly
right and I'm not adding classification vocabulary — I'm adding the fields
the outline's own example promised and the cliff proposal requires.**
Jeroen's outline literally specified the payload: *"biome: grassland,
settlement: null/'name here', river: null/'name here/unnamed', frozen:
true/false, flooded: true/false, sea: true/false."* Reconciling that
against what's measured and what's shipped:
| Outline's ask | Wire field today | My call |
|---|---|---|
| biome | `morphology: Vec<u8>` (17-zone D-239 §6) | Carries it. "Biome" in the outline reads as morphology zone — keep the existing vocabulary, don't add a second one. |
| — | `elev_q: Vec<u8>` | Keep (height, per outline's separate ask). |
| frozen | `glaciation: Vec<u8>` (5-grade) | Carries it — a boolean collapse of glaciation is a client-side *derivation of the same data*, not a new field. Client reads `glaciation > 0` if it wants a bool; server shouldn't duplicate the same fact two ways on the wire. |
| flooded, sea | `morphology` (has water zone classes) + hydrology's settled state | See "map time axis" note below — this is where the outline's "sea"/"flooded" distinction needs a decision the workshop, not me, should rule on (Q1 to Jeroen in the brief). I'm not duplicating a boolean the morphology vocabulary can already express; if the workshop wants a fast client-side "is this wet" check without decoding the full morphology enum, that's a **cheap additive derived bit**, not new server-computed information — flag it, don't block on it. |
| settlement: null/name | **new**, see below | Presence + id, not inline name (§1). |
| river: null/name | `courses: Vec<RiverCourse>` + **new** name lookup | Already ships geometry; name resolves via the new whole-body table (§1). |
| — | `moisture_q`, `vegetation` | Keep — not in the outline's own example list but already shipped and load-bearing (vegetation climate law, D-239 §8). |
**New field: `settlement_id: Vec<u32>` (0 = none), per-gridunit dense array,
same shape as `morphology`.** This is the one genuinely new dense field my
schema adds. It's dense (not sparse-list) because "which settlement, if any,
covers this gridunit" is a per-cell lookup a renderer needs at paint time —
exactly the same shape argument as `morphology` itself (a classification
raster, not a feature list, because every cell needs an answer, most of them
"none"). This is different from the settlement *name* (§1's ids-with-lookup)
— the id here is cheap (u32, near-constant runs, same RLE profile as
`morphology`/`vegetation` in T-1179's per-field table — settlements are
rare and spatially coherent, so this field costs almost nothing extra under
PNG encoding). The **name** join happens client-side against the whole-body
lookup; the wire never repeats the string.
**Cliff/vertical structure: adopt T-1177's proposal as the payload
shape, gated on frequency, not on principle.** Dudley's measurement is
unambiguous: gorge carving is *rare* (zero carved cells across all three
production-scale benches; genuine carving needs a narrow two-basin-saddle
geometry that's uncommon at continental working-grid resolution). That
changes my answer from "add three dense fields" to "add them as
sparse, not dense":
- `channel_depth: Vec<u16>` and `cliff_edge: Vec<bool>` **as dense per-gridunit
arrays would be wasteful** given near-zero occupancy — RLE would crush them
to a handful of runs, but PNG already wins on the honestly-dense fields, so
paying for two more mostly-zero dense arrays is not free even if cheap.
- **My call: fold the cliff case into the same sparse-feature-list shape as
settlements/rivers, not a seventh/eighth dense array.** A
`cliffs: Vec<CliffSegment>` field (`{ cell: (u16,u16), channel_depth: u16 }`,
`cliff_edge` implied by list membership) parallel to `courses` — present
only where it's true, empty in the overwhelming majority of windows T-1177
measured. This keeps the common case (no gorge in this window) at zero
bytes and matches the carrier three-way rule's own logic: this is
rung-indexed invented detail (rule iii — hydrology solves once per body but
the *carved representation* is a windowed-rung concern the same way course
geometry is), so it belongs on the windowed payload as a sparse list, not
baked into the dense sentinel arrays.
- Elevation stays single-valued (`elev_q`) for every gridunit, including
carved ones — it's the rim/dominant height per T-1177's own reasoning. The
channel floor is a derived value (`elev_q - channel_depth`) the client
computes only for gridunits that appear in the sparse `cliffs` list. No
change to the existing single-height contract for the 99%+ common case.
**What the client is allowed to do with these fields — restated plainly,
because "map art function, not data function" needs a hard line:**
- Color/style pixels from the classification fields (morphology, glaciation,
vegetation, moisture) — yes, that's the entire job.
- Interpolate/smooth/tween the color *presentation* between adjacent
gridunits or between step-crosses (cosmetic) — yes, per the brief's premise
3 investigation item.
- Invent geometry, decide where a river bends, decide whether a cell is
flooded, decide whether a cliff exists — **no, never.** Every one of those
is a server-derived fact already on the wire (courses' points, morphology's
water classes, the new `cliffs` list). If the client ever needs to guess
at content the schema doesn't carry, that's a missing field, not a license
to invent — send it back to this schema, don't let it leak into client-side
derivation logic. This is the same discipline D-227 already enforces
server-side; it just needs restating as a *client* rule now that the
client receives finished-content data instead of raw geometry it used to
interpolate itself.
- Resolve names via the whole-body lookup tables (§1) — yes, a join, not a
derivation.
---
## 3. Encoding continuity across steps
**One colorizer family, confirmed — and the wire schema I'm proposing is
exactly what makes that cheap to guarantee, because every step ships the
same six-plus-two field shape at a different spacing, never a different
vocabulary.**
T-1143 §6's rule (one colorizer spans every rung, cited already for
temperature in D-226 T-1124 §2 — *"deliberately not a separate district-tier
quantization... one temperature colorizer spans both zoom levels"*) already
set the precedent I'm extending to the whole schema: **every step's payload
uses the identical field set, identical enum vocabularies
(`MorphologyZone`'s 17 zones, `VegetationClass`'s 7 including `Marine`,
`GlaciationGrade`'s 5 grades), at every step from global down to the deepest
gridunit spacing.** The only thing that changes between steps is *sampling
density*, never *meaning*. This is what makes the between-step magnification
red flag (Tyre's #1) tolerable — a magnified hold of step-N's canvas for one
step interval is showing the *same* classification vocabulary at coarser
spacing, not a different color language snapping in, so the discontinuity is
a resolution jump, never a palette jump.
**"Average-back across step boundaries" — confirm the mechanism, name the
limit.** For continuous quantized fields (`elev_q`, `temp_dc`, `moisture_q`)
average-back is safe and already implicit in how a coarser step's cell is
itself computed (it's a spatial mean over finer terrain, same as today's
Region/District relationship). For **categorical** fields
(`morphology`, `vegetation`, `glaciation`), averaging is meaningless — you
cannot average "grassland" and "forest" into a third zone. R3 in the
delivered ladder design doc already flags this as an open risk
("average-back unverified for categorical morphology gates") and T-1179's
own per-field RLE table gives the concrete reason it matters: `morphology`
and `vegetation` are the two fields that compress to near-nothing (6 runs
at 330K cells) *because* they're genuinely piecewise-constant zone
classifications, not smoothly-varying scalars — averaging them at a step
boundary would destroy exactly the property that makes them cheap and
legible. **My call: coarser steps must derive morphology/vegetation/
glaciation as a fresh classification decision at that step's own spacing
(the dominant-mode rule D-226's `dominant_district_type`/`dominant_zoning`
precedent already uses for settlement aggregates — pick the plurality
class, don't blend), never as a numeric average of the finer step's discrete
values.** This is consistent with the whole "each step is a derivation
sampled at gridunit resolution" framing (Tyre's D-166 corollary repoint) —
a coarser step doesn't downsample a finer step's raster, it re-derives at
its own spacing, and for categorical fields that re-derivation is a mode/
plurality pick, not an arithmetic mean.
**"Different classes of content at lower zoom" (forest → clearings/ponds) —
this is new vocabulary, not more density, and it needs to be scoped now so
it doesn't sneak in unbounded.** The outline names this directly: *"at low
enough zoom, a green forest biome may start showing clearings and ponds or
such — to be determined and tinkered with."* My read: this is **sub-zone
detail-scatter within an existing classification**, the same category as
`voxel_relief`/`voxel_mosaic`'s invented detail-scatter (D-227, T-1154's
octave-cutoff family) — not a new top-level vocabulary entry on
`morphology`. A "forest with a clearing" is still `Forest` morphology at
the district level; the clearing is deeper-step invented texture *within*
that classification, resolved the same way finer steps already resolve
everything else (fresh derivation at that step's `min_wavelength_m` cutoff).
**Concretely: this does not need a new wire field or a vocabulary change at
all** — it falls directly out of `min_wl_m` already being echoed per window
(the octave cutoff field `DistrictWindowLayer` carries today) and the
deeper step simply deriving at a smaller cutoff, which — per T-1154's
measurement — is trivially affordable (17 ms parallel at the deepest
realistic 83K-cell canvas). The one thing I'd ask the synthesis round to be
disciplined about: keep this as "existing classification, finer octave
detail," not "a growing zoo of sub-biome enum values" — that way it never
threatens the one-colorizer-family guarantee, it just adds spatial texture
underneath a class that was already decided at the coarser step. If a
future pass wants clearings/ponds to carry *distinct* semantic meaning
(e.g. a pond is walkable water, not decoration), that's a scope question
for whoever owns block/tile classification next, not a step-continuity
question — flagging the boundary, not answering past it.
---
## Summary for the wire contract / tagged-envelope synthesis (feeding Q2/Tyre)
For whoever writes the synthesis-round wire contract:
- **Per-step payload = the existing six dense fields (`morphology`,
`elev_q`, `temp_dc`, `moisture_q`, `vegetation`, `glaciation`) + one new
dense field (`settlement_id: Vec<u32>`) + two sparse feature lists
(`courses` — already shipped — and new `cliffs`) + the existing `courses`
name resolution now needs a whole-body `river_names` lookup alongside the
existing `atlas_city_names` settlement lookup.**
- **Encoding: PNG-per-field for every dense array** (T-1179's unambiguous
winner — smallest and fastest at every measured size, no per-field
hand-tuning needed since DEFLATE already captures the compressibility
spread the per-field RLE table exposed). Sparse lists (`courses`,
`cliffs`) stay MessagePack-native — they're already small (~12 KB
typical per T-1170) and don't benefit from raster encoding.
- **This payload is categorically larger than the 30 KB windowed-query cap
at every measured size (21×–563×, T-1179)** — confirms the brief's own
read that the tagged-envelope migration is not avoidable by a smarter
encoding choice. I have nothing to add to that call beyond confirming the
numbers hold for the schema I'm proposing (it's the same six fields T-1179
measured, plus one more near-free dense field and two near-free sparse
lists — doesn't change the order-of-magnitude verdict).
- **Vocabulary stays frozen across every step.** No per-step schema
variants, no new enum arms below the district-level vocabulary already
governed by D-239. Deeper detail is resolution and octave cutoff, never a
new field.
@@ -0,0 +1,952 @@
---
title: "Body Map Viewer — Araminta Round 2 (Synthesis)"
description: "Final per-gridunit payload schema with TTL-split planes, opener/Region rung correction, label de-dup rule at step seams, final wire schema, lakes gap fix (morphology-fold + outflow-course endorheic cue)"
workshop: body-map-viewer
round: 2
author: Araminta
status: complete
---
# Araminta — Round 2 (Synthesis)
Four items, per the coordinator's slice. Grounded in
[lead-interview-1.md](lead-interview-1.md)'s rulings (sparse `CliffSegment`
adopted, Phase-4 scope, current-state-via-TTL-split map time, viewport
canvases + envelope ratified), my own [round-1
position](araminta-round1.md), and the other three round-1 documents
([dudley-round1.md](dudley-round1.md), [stig-round1.md](stig-round1.md),
[tyre-round1.md](tyre-round1.md)) plus [dudley-round2.md](dudley-round2.md)
for the envelope mechanics. **All four items are now fully closed — no
open cross-checks remain.** (b) region-as-step-0 and the cliff wire shape
closed three-way (Araminta + Tyre + Jeroen's ruling); (d)'s envelope-framing
question closed by Dudley's `StepCanvasResponse` design (§(a) of his round-2
document): one flat tagged message, my default assumption confirmed without
qualification.
---
## (a) Final payload schema — TTL-split planes, concretely
### The full field set, ruled and unified
Round 1 (mine) proposed six existing dense fields + one new dense field
(`settlement_id`) + two sparse lists (`courses` existing, `cliffs` new).
Jeroen's lead-interview-1 ratified the sparse `CliffSegment` shape and
Phase-4 scope outright ("Cliffs — sparse `CliffSegment` feature list,
Phase-4 scope... Aligns the rarity finding, Araminta's encoding logic, and
Tyre's scope ruling"). Nothing about the field *set* changes here — this
section's job is the thing round 1 didn't yet have to answer: **which of
these fields is static geometry vs. sim-state, and what does that split
cost on the wire.**
### The TTL-split — field-to-plane assignment
Jeroen's ruling (lead-interview-1, ruling 2): *"static geometry cached
indefinitely-fresh (determinism), frozen/flooded carried as
separately-cached short-TTL planes re-requested as sim time advances."*
This is a **wire-framing** decision, not just a cache-policy one — if
frozen/flooded ride inside the same dense array as permanently-fresh
fields, the client cache can't apply two different freshness rules to one
byte blob. So the planes have to be **physically separate fields**, not a
policy applied after the fact to a merged payload.
**Static-geometry plane (cached indefinitely-fresh, D-227 determinism
guarantee — same seed/body/position → same bytes forever, no TTL, no
staleness check, only storage-budget eviction per Jeroen's amendment):**
| Field | Type | Why static |
|---|---|---|
| `morphology` | `Vec<u8>` | Classification zone — a pure function of terrain derivation, D-239 §6 vocabulary, no sim-time input. **§(e) note:** the existing `Lake`/`OpenOcean` discriminants gain a hydrology-basin-membership data source (post-ratification fix, CLOSED) — no wire change, same field, better-sourced values. |
| `elev_q` | `Vec<u8>` | Height — geology, not weather |
| `moisture_q` | `Vec<u8>` | Climate baseline (region-computed-once-inherited per D-243 §3), not a live sim tick value |
| `vegetation` | `Vec<u8>` | Derived from climate baseline + morphology, same determinism class |
| `settlement_id` | `Vec<u32>` | Settlement placement is a generation-time fact, not sim state |
| `courses` | `Vec<RiverCourse>` | River geometry is invented deterministically (T-1170), never moves. **§(e) note:** an overflow lake basin's outlet edge extends into this list once the D8 sourcing fix (§(e)) lands — the same field, no new carrier. |
| `cliffs` | `Vec<CliffSegment>` | Hydrology settled equilibrium (T-1177) — a pure function of seed, computed once per body, not re-solved on a clock |
**Sim-state plane (short-TTL, re-requested as sim time advances — the
concrete instantiation of D-226's clock-bound region water-height model,
`governance/decisions/architecture.md` §"`Water` is a dynamic depth
state... a region property computed once per phase"):**
| Field | Type | Why sim-state |
|---|---|---|
| `glaciation` | `Vec<u8>` | 5-grade freeze state — this is the "frozen" half of the outline's frozen/flooded pair. Glaciation as *permanent ice geology* (a glacier that doesn't melt) is static; glaciation as *seasonal frost* is sim-state. Per D-226's clock model, both are the same field sampled at different clock buckets — the wire doesn't need two fields, it needs this one field re-requested on the seasonal clock-bucket rollover. |
| `flooded` | **new**, `Vec<bool>` or folded bit — see below | The outline's explicit ask (`flooded: true/false`) that today's `morphology` water-zone classes don't cleanly separate from permanent sea/lake morphology. This is the region water-height-vs-elevation comparison D-226 already specifies as the mechanism — a derived boolean per gridunit, computed at request time from the current region water-height clock state, not stored geometry. |
**`temp_dc` is the genuinely ambiguous one, and I'm ruling it static, not
sim-state, for a specific reason worth stating.** Temperature has a
diurnal/seasonal clock term per D-226's model too, so it looks like a
sim-state candidate at first read. But the Atlas map (this workshop's whole
scope) is not a live weather display — it's a navigational reference the
player consults at any moment, and the *baseline* climate classification
(what makes a zone "arctic" vs "temperate" for morphology/vegetation
purposes) is the region-computed-once-inherited value D-243 §3 promises,
not the live diurnal offset. Treating `temp_dc` as static keeps it
consistent with `moisture_q`/`vegetation`, which are already climate
*baseline* values, not live samples — and it avoids a wire/cache
inconsistency where morphology (static, climate-baseline-derived) and
`temp_dc` (if sim-state) disagree about which clock reading produced the
zone you're looking at. **If a future pass wants a live "current
temperature" readout as a distinct overlay** (a genuinely different
feature — a weather-report reading, not the map's base classification),
that's an additive sim-state field on top of this, not a reclassification
of the existing one. Flagging this reasoning explicitly since it's the one
plane assignment that isn't obvious from the field name alone — Jeroen's
"map time axis" ruling names frozen/flooded specifically, and I'm reading
that specificity as intentional (temperature wasn't named alongside them),
not an oversight.
### The staleness axis, stated per field — why each side, explicitly
The coordinator flagged this precisely: the plane split isn't "things that
sound weather-related vs. things that don't," it's a real distinction each
field needs its own stated reason for, because two fields with
superficially similar names (`glaciation`, `temp_dc`) land on opposite
sides and a reader shouldn't have to infer why.
- **`glaciation` → SIM-STATE, because "frozen" is seasonal, not
geological.** The field's own doc comment already frames it as a
"5-grade" classification, and D-226's clock model treats freeze/thaw as
exactly the kind of clock-bucket-rollover term (diurnal/seasonal
phase-stepped, `RegionPhase` memoized per bucket) the whole
"current sim state" framing in Jeroen's ruling is about. A gridunit that
reads `glaciation: 4` (heaviest grade) in winter and a lower grade in
summer is not lying about the world's geology — it's answering "what
does this look like right now," which is precisely what a short-TTL,
re-requested-on-clock-rollover field is for. **Permanent ice sheets are
not a separate wire concept** — a body-wide-permanently-glaciated
region still reads a high grade at every clock sample, so the sim-state
framing costs nothing for the "always frozen" case; it only pays off
(shorter TTL, re-fetch on season change) for the seasonal case, which is
exactly where the map needs to be honest about "right now" rather than
"always."
- **`flooded` → SIM-STATE, for the identical reason on the water axis.**
D-226's own water-height model states this almost verbatim: *"Depth is
time-varying via a deliberately cheap, deterministic, clock-bound
water-height... floodplain / tidal-flat / seasonal-river emerge rather
than being placed."* `flooded` is a derived comparison
(region-water-height-at-current-clock-phase vs. local elevation), not
stored geometry — it cannot be static by construction, because the same
gridunit legitimately answers `true` at high water and `false` at low
water within one static-geometry payload's entire cached lifetime.
- **`temp_dc` → STATIC, because the *map's* temperature reading is a
climate-baseline classification, not a live weather sample.** This is
the one that needs the explicit contrast: `temp_dc` *could* have a
diurnal/seasonal clock term exactly like `glaciation`/`flooded` do (the
same D-226 clock model covers temperature too — "one clock model drives
temperature, flooding, tides, snow..."), so the distinction here isn't
"does this field have a clock term at all" (glaciation, flooded, AND
temp_dc all do, in the underlying sim). The distinction is **what the
Atlas map is using the field for.** `temp_dc` on this payload feeds the
*zone classification* — it's read alongside `morphology`/`vegetation`
to answer "what kind of place is this," a determinism-class question
(D-227: the same seed always classifies this gridunit as arctic
tundra), not a "what's the weather doing this afternoon" question. If
`temp_dc` rode the sim-state plane, a player who opens the Atlas in
winter vs. summer would see the *classification itself* flicker between
TTL refreshes even though the underlying terrain and biome haven't
changed — that's a worse experience than the actual live-diurnal-shift
case (frozen/flooded), where the flicker *is* the honest answer to
"what does this look like right now." **`glaciation`/`flooded` sim-state
because their live value IS the map-relevant fact; `temp_dc` static
because its live value is not what the map is answering with it** — same
underlying clock-bound sim mechanism, different consumer question, hence
opposite plane assignment. This is the reasoning that makes the split
non-arbitrary rather than a guess from the field name.
### Separate planes vs. bitfields — the byte-cost comparison Jeroen asked for
Two candidate shapes for the sim-state plane, priced against T-1179's
measured rates (6.00 bytes/cell raw MessagePack, ~0.32× that with PNG
per-field ≈ 1.9 bytes/cell effective):
**Option 1 — separate L8 planes, one byte per field (`glaciation:
Vec<u8>` already ships this way; `flooded: Vec<u8>` as 0/1 or reusing the
existing byte width).** Cost: two single-channel PNG-encoded planes.
Stig's measurement ⑤ already names the win here directly — *"L8 is 49×
cheaper at every size... a free win if any wire field... can ship
single-channel before the client colorizes it"* — and `glaciation`
already is single-channel. At 330K gridunits, PNG-per-field measured
638,382 bytes total across all **six** existing fields (T-1179); a rough
per-field share (not uniform, since fields compress differently, but
`glaciation`'s own RLE run-count — 28.7% of dense at 330K, "moderately
compressible" — sits mid-table) puts one sim-state plane at roughly
tens of KB PNG-encoded, not hundreds. Two separate planes (glaciation +
flooded) roughly doubles that share but each stays independently
requestable — the client can re-fetch just the sim-state pair without
touching the static-geometry payload at all, which is the entire point of
splitting them.
**Option 2 — packed bitfield, both sim-state facts folded into fewer
bytes (e.g. glaciation's 3 real bits + a flooded bit packed into one byte,
per T-1179's bit-packing measurement).** Cost: T-1179's bit-packing
number is unambiguous and directly on point — bit-packing alone buys
"~1718% off raw... real but modest," and critically: **"packing bits
first actually hurts DEFLATE's job on the low-entropy fields... by
destroying their byte-aligned run structure; DEFLATE prefers finding runs
of identical raw bytes over finding runs of identical bit-groups spread
across byte boundaries."** `glaciation` and a hypothetical `flooded`
boolean are exactly the low-entropy, spatially-coherent case (glaciation
already showed moderate-to-good compressibility; a flooded boolean over a
coherent water body is likely similar or better) that Option 2's own
measured finding says bit-packing actively hurts.
**My call: Option 1, separate L8/single-byte planes, PNG-per-field
encoded — not bitfields.** Three independent reasons converge:
1. **T-1179's own measured finding rules bit-packing out on compression
grounds** for exactly this kind of field (low-entropy, spatially
coherent) — PNG-per-field already beats bit-packed-then-PNG'd at every
size in the table (0.32×/0.30×/0.32× vs 0.53×/0.56×/0.54× across
330K/2.07M/8.3M).
2. **Stig's measurement ⑤ prices the upload-side win of single-channel
planes directly** — L8 is 49× cheaper than RGBA8, and that win is
available *because* the planes are separate single-channel textures,
not because they're bit-packed. Packing bits into one byte doesn't
change the texture channel count on the client side; it just makes the
CPU-side unpack step more expensive for a compression loss.
3. **The TTL-split's whole point is independent re-fetchability.** A
packed byte containing both a static bit (if one were ever folded in)
and a sim-state bit would force the *entire byte* to inherit the
shorter TTL — you can't selectively re-request half a byte. Keeping
`glaciation` and `flooded` as separate planes (both sim-state, so this
specific tension doesn't bite today) is what keeps the TTL boundary
*aligned with the field boundary*, which is the property this whole
sub-task is asking for. Folding sim-state and static fields into the
same byte would be the one packing move that actually breaks something
structural, not just costs a few percent — good thing neither
candidate here needs that (both `glaciation` and `flooded` are
sim-state, so packing them together, if ever done, wouldn't cross the
TTL boundary — but I'm not recommending it anyway, given point 1).
**Concrete byte cost, stated plainly:** at the PNG-per-field encoding
already adopted for the rest of the schema, the sim-state plane (two L8
fields, `glaciation` + `flooded`) costs on the order of the same
per-field share any other moderately-compressible classification field
costs in T-1179's table — a small fraction of the total step-canvas
payload, not a meaningfully separate budget line. The TTL split is a
**freshness/re-request-cadence decision, not a bytes-saved decision**
splitting the planes doesn't shrink the payload, it lets the client avoid
re-fetching the (much larger) static-geometry majority of the payload
when only the sim-state pair has gone stale. That's the actual value:
bandwidth saved on **re-fetch frequency**, not on first-fetch size.
### Schema, final form
```
StepCanvas {
// envelope/echo fields (existing pattern, carried forward)
center, step_index, gridunit_spacing_m, min_wl_m, ...
// STATIC GEOMETRY PLANE — cached indefinitely-fresh, D-227 determinism
morphology: Vec<u8>, // dense, PNG-per-field — Lake/OpenOcean discriminants now
// basin-sourced from HydrologyResult (§(e), CLOSED), no wire change
elev_q: Vec<u8>, // dense, PNG-per-field
temp_dc: Vec<i16>, // dense, PNG-per-field — climate baseline, not live reading
moisture_q: Vec<u8>, // dense, PNG-per-field
vegetation: Vec<u8>, // dense, PNG-per-field
settlement_id: Vec<u32>, // dense, PNG-per-field (new)
courses: Vec<RiverCourse>, // sparse, MessagePack (existing, T-1170) — overflow-basin outlet
// edges extend into this list once the D8 sourcing fix (§(e)) lands
cliffs: Vec<CliffSegment>, // sparse, MessagePack (new, ratified lead-interview-1)
// SIM-STATE PLANE — short-TTL, re-requested on clock-bucket rollover
glaciation: Vec<u8>, // dense, PNG-per-field, L8
flooded: Vec<u8>, // dense, PNG-per-field, L8 (new)
}
```
**Note on the lakes fix (§(e)):** no new field appears in this struct.
The originally-proposed `water: Vec<u8>` and `lakes: Vec<LakeBasin>`
candidates are both withdrawn — Dudley's post-ratification finding
(`MorphologyZone::Lake` already exists, just poorly sourced) means the
fix is a **derive-pipeline data-source change**, not a wire-schema
addition. `morphology` and `courses` carry annotated notes above marking
where the fix lands; no field count changes.
**`CliffSegment` shape** (unchanged from round 1, restated for
completeness): `{ cell: (u16, u16), channel_depth: u16, /* cliff_edge
implied by list membership */ }` — direct, non-lossy carry of
`HydrologyResult`'s `channel_depth_scaled`/`cliff_edge` fields (Dudley,
T-1177), sparse list parallel to `courses`, `#[serde(default)]` empty on
non-carving windows.
**Client rule, restated once more since the TTL split makes it sharper:**
the client may cache the static-geometry plane forever and treat the
sim-state plane as a separately-expiring sub-fetch — but it may never
*infer* flooded/frozen state from the static plane (e.g. inferring
"flooded" from `morphology`'s water-zone classes as a substitute for the
real `flooded` field). The two planes answer different questions
(permanent water body vs. current water-height-vs-elevation state) and
conflating them client-side would silently violate the very TTL split this
section defines.
---
## (b) Region-as-step-0 — CORRECTED post-ratification (Jeroen's review; Tyre doing the parallel correction in his own amendment texts)
**Status update, stated plainly before the corrected text: my three-way
"CONFIRMED" closure below was wrong on rung *count*, right on
*spacing intuition*.** Jeroen's post-ratification review caught the gap:
I collapsed two distinct rungs into one. The corrected model, per the
coordinator's relay:
> **GLOBAL is rung 0** — the body-surface opener, a **VARIABLE-extent**
> canvas (the body's entire region grid, one gridunit per region — this
> is the D-243 elastic seam **made visible on the wire**, not hidden above
> the ladder as I originally wrote). This is the **canonical, always-kept
> tier**.
> **REGION is rung 1** — the largest **FIXED**-size rung: viewport-sized,
> evictable, exactly like every other step below it.
My original text conflated these because I reasoned from *spacing* alone
("step 0 needs a D-243 rung; Region is the coarsest one; therefore step 0
IS Region") and missed that the opener's defining property isn't its
gridunit spacing at all — it's that its **canvas extent is per-body
floating** (`round(2πR / 204.8 km)` regions around the equator, D-243's
own elastic-seam formula), which is categorically different from every
other step's fixed-px-budget canvas (my own (a)/(d) sections, Dudley's
3840×2160 convention). A rung whose *extent* varies per body cannot be
"the Region rung" in the same sense Region-at-rung-1 is, even though both
sample at region-sized gridunits. **Spacing and extent are two different
axes, and I only checked one.**
### The corrected reasoning, restated on the right axis
1. **Gridunit spacing at the opener is still region-sized — that part of
my original argument holds unchanged.** Nothing about the correction
invalidates why region-spacing is the right sampling density for a
whole-body view (T-1179's cell-count-scales-with-area argument,
D-226(d)'s whole-body-planetary-layer boundary at anything finer). The
opener's gridunits ARE region-sized. What I got wrong is naming that
fact "step 0 = the Region rung" instead of "step 0 samples at Region
spacing, but is its own rung above Region because its extent is the
variable whole-body grid, not a fixed viewport."
2. **The canonical/always-keep property moves with the correction, and
this is the load-bearing consequence for my (a) TTL-split work.**
Dudley's round-1 global-tier costing (~174 MB PNG-encoded across all
273 bodies) and my own (a) section's "static plane cached
indefinitely-fresh... only the global tier keep-always" language were
written assuming Region *was* step 0 — they need to be read as
describing **rung 0 (the opener)**, not rung 1. Region (rung 1) is
now correctly a **storage-evictable** sub-global tier, same as every
rung below it — it does NOT inherit "canonical" just because it's the
largest fixed rung. This is consistent with, not a reversal of,
Jeroen's lead-interview-1 ruling ("canonical survives only as the
global tier") — the correction is *which rung is "the global tier,"*
not whether the keep-always/evictable split itself holds.
3. **The elastic seam is not "above the ladder, never derived at" as I
originally wrote — it's rung 0's own canvas-extent parameter.** My
original bullet 3 said "the elastic seam sits *above* [Region], at
the per-body planetary count, which is not itself a spacing value the
Atlas derives at." That's the specific sentence Jeroen's correction
overturns: the elastic seam IS what the Atlas derives at rung 0 —
`round(2πR / 204.8 km)` regions isn't a hidden background fact, it's
the literal gridunit count of the opener's canvas, visible to the
player as "how big does this body's region-grid look." This is a
genuine strengthening of D-166's "seamless zoom ladder" mandate, not
a complication: the elastic seam was always going to be visible
*somewhere* the first time a player opens two differently-sized
bodies' Atlas views side by side, and rung 0 is the correct, honest
place for that to surface, rather than being smoothed over.
### What this changes in my payload schema (a) and wire schema (d) — corrected, not reworked
**No field-set change.** Rung 0's `StepCanvas` still uses the identical
field set from (a) — same six-static-plus-two-sim-state dense fields,
same two sparse lists, same whole-body name lookups. The correction is
entirely about **which rung is canonical and what "extent" means at the
top**, not about the payload contents. Concretely:
- `EncodedStepCanvas.width`/`.height` (Dudley's struct, §(d)) are **fixed
constants at every rung except rung 0**, where they are instead
`derive_region_count(body_radius_km)` — the same per-body function
D-243's elastic seam already defines, not a new derivation. This is
the one place in the whole ladder where `width`/`height` are a
body-dependent value rather than the shared fixed-px-budget convention
— worth a doc-comment on the struct so an implementer doesn't assume
every response has the same canvas dimensions.
- The **cache-tier "keep-always" retention floor (Stig's round-1 §4,
Dudley's round-2 §(d) global tier) now pins specifically on rung 0's
entries, keyed by `(body_id, rung=0)`** — not on `(body_id, rung=1
/*Region*/)` as my prior text implied. Region (rung 1) entries flow
through the same storage-TTL/time-since-last-visit eviction every
other sub-global rung uses.
- **Step index numbering shifts by one relative to my prior text**
throughout this document: everywhere I previously wrote "step 0" I
should now write "rung 0 (the opener)," and every reference to
"Region as the top of the sub-global ladder" now correctly reads as
"Region, rung 1, the first fixed-size step below the opener." I'm not
renumbering every instance below for this correction pass — flagging
the mapping once, here, as the authoritative key: **rung 0 = opener
(variable, region-spaced gridunits, canonical) → rung 1 = Region
(fixed, viewport-sized, evictable) → rung 2 = District → ... → deepest
rung = whatever Jeroen's chunk(64m)-bottom ruling lands on.**
### Convergence status
Tyre is producing the equivalent correction directly in his amendment
texts (D-243 gridunit entry, the step-ladder table) per the coordinator's
note — I'm not routing this one for a fresh cross-check the way I did the
original (b) confirmation, since Jeroen's own review is the authority
here and both of us are independently correcting against the same
relayed ruling, not proposing competing readings. If Tyre's amendment
text names the rungs differently (e.g. a different rung-0 label than
"opener/GLOBAL"), I'll conform my terminology to his filed text rather
than contest it — this section's job is getting the *architecture* right
for my schema's sake, not owning the naming.
---
## (c) Label de-dup at step seams — the concrete rule
Tyre's gray case (round-1 §3, bucket C): *"is client-side de-duplication...
legal presentation logic, or does it risk client and server disagreeing
about which gridunit owns this feature's label?"* His own read: *"legal,
IF the server always ships feature identity (a stable id) rather than the
client inferring identity from proximity."* My round-1 payload schema
already ships exactly that precondition (`settlement_id` per gridunit,
`edge_id` on every `RiverCourse`) — so the precondition is met, and the
rule below is the concrete mechanism that makes the "legal" branch actual,
not just theoretical.
### The rule
**Deterministic anchor choice, keyed on feature identity, never on which
canvas/step happened to fetch first.**
**For settlements (`settlement_id` occupying a contiguous run of
gridunits within one step's canvas, and potentially spanning a step or
canvas-tile seam at low zoom):**
> The label anchor is the **arithmetic-mean gridunit position of every
> cell carrying that `settlement_id` within the current step's canvas**,
> rounded to the nearest gridunit via a fixed tie-break rule (round half
> toward the lower `(x,y)` in row-major order — an arbitrary but
> *stable* tie-break, the same discipline `label_lake_basins`' row-major
> discovery order already uses for determinism, T-1177). This is a pure
> function of `(step, canvas_bounds, settlement_id, the cells carrying
> it)` — same inputs, same anchor, always, on both the server-echo side
> and any two clients computing it independently.
**Why this handles the seam case without special-casing it:** a
settlement whose footprint is split across two adjacent canvas tiles at
the same step (the mosaic case, not the step-cross case) has each tile
compute its own mean over *only the cells that tile actually received*
which means the two tiles will independently compute two different
anchor points (each tile's own partial-footprint centroid), and **both
draw a label**. This sounds like the bug the gray case worried about, but
it isn't, once you name the actual failure mode precisely: the risk was
never "two labels for one settlement," it's "two labels disagreeing about
*which* settlement, or a label a player can't correlate with the
feature." A settlement large enough to span a canvas-tile seam at a given
step is, by construction, large enough that *both* halves are visibly the
same feature to the player (its footprint fill color and `settlement_id`
match on both tiles) — showing the name near each visible fragment is
correct map behavior, not a duplication bug, the same way a real
paper-map atlas repeats a country's name across each visible page-tile
its territory spans. **The actual dedup requirement is narrower than "one
label globally": it's "never show two different names for the same id,
and never let two different ids merge into one label."** Both are
structurally impossible under this rule, since the anchor computation and
the name lookup are both pure functions of `settlement_id` — there is no
code path where the id resolves to different text on two tiles.
**Where genuine single-label dedup DOES matter: the step-cross case, not
the tile-seam case.** When the player crosses a step boundary (not a
tile boundary within one step), the *previous* step's held canvas and the
*newly arrived* step's canvas briefly coexist during hold-fetch-swap
(Stig's round-1 §2). Both canvases might carry labels for the same
settlement during that transition window. **Rule: the annotation layer
only ever draws labels from the currently-authoritative canvas — the
held (stale, about-to-be-replaced) canvas's annotation layer is not drawn
during the hold interval, only its terrain texture is (magnified, per the
between-step-magnification red flag).** This isn't a new mechanism — it
falls directly out of Stig's own architecture: the annotation layer is
described as an *unscaled sibling* that "re-run[s] the annotation layer's
world→screen transform against the new step's bounds" on arrival, which
already implies the annotation layer belongs to one step's data at a
time, not both simultaneously. I'm stating the corollary explicitly
because "which canvas's annotation layer is currently drawn" wasn't
spelled out as a rule anywhere in round 1's four documents, and it's
exactly the kind of implicit assumption that produces the doubled-label
bug if an implementer doesn't carry it forward.
**For river name labels (a `RiverCourse` polyline, potentially cropped
differently by two adjacent windows per Ruling 1e's window-independence
invariant):** the same identity-first principle, adapted to a line
feature rather than a point/area feature:
> The label anchor for a named river is the **midpoint, by arc-length, of
> the *cropped* `points` array the current canvas received for that
> `edge_id`** — not a global midpoint of the river's full extent (which
> the current canvas may not have visibility into at all, per Ruling 1e:
> the geometry is a pure function of `(seed, body, edge, rung)`, cropped
> per-window, never re-parametrized). Each canvas places its own river
> label at its own cropped-midpoint, independently. Same non-issue as
> settlements: a long trunk river visibly spans multiple canvas tiles, and
> a label per visible tile is correct cartography, not a duplication bug,
> as long as every tile's label resolves to the *same* name via the
> `river_names` lookup (§(d) below) keyed on the same `edge_id` — which it
structurally does, by construction, same argument as settlements.
**One explicit non-goal, stated so a future implementer doesn't
over-engineer this:** I am **not** proposing any cross-tile or
cross-canvas coordination protocol (no "who owns this label" arbitration,
no server-side label-placement pass). The rule above needs zero new wire
data and zero new server logic — it's entirely a client-side
presentation policy operating on data the schema in (a) already ships
(`settlement_id`, `edge_id`, the cropped geometry). This is deliberately
inside Tyre's bucket-A "legal interpolation" territory: the client is
*placing* a label for an already-identified thing, never *deciding* that
a thing exists or what it's called.
---
## (d) Final wire schema — CLOSED (envelope framing confirmed by Dudley)
**Envelope-framing question resolved: shape (a), one flat tagged
message.** Routed to Dudley via the coordinator — asked whether the
tagged envelope frames the full `StepCanvas` as one flat message (my
default assumption, matching today's `DistrictWindowLayer` shape scaled
up) or splits dense/sparse into separately-tagged sub-messages for
progressive delivery. **Dudley confirmed (a) without qualification**
(`dudley-round2.md` §(a)): every field comes off the same row-chunked
derive pass in one traversal, so a dense/sparse split saves no server
compute and only adds round-trips and partial-state handling — the
identical argument D-225 already used against per-layer whole-body
requests. The progressive-paint UX case (terrain visible before
courses/cliffs finish decoding) is real but solved client-side for free:
courses/cliffs are a tiny fraction of total decode time (a few KB against
a multi-hundred-KB-to-tens-of-MB dense payload), so the client paints the
terrain RTT layer immediately and defers the annotation layer's draw
calls by a frame — a rendering-order choice, not a wire-protocol one. He'd
revisit only if a future profiling pass found the dense and sparse derive
passes on genuinely different cost timelines; nothing measured this
workshop shows that (courses cost +0.090.21 ms against a ~5 ms baseline
— negligible, not staggered).
**Concrete carrier, per Dudley's `StepCanvasResponse` design** (matching
my field list exactly, field-for-field, including the resolved
`cliffs: Vec<CliffSegment>` sparse shape):
```rust
pub struct StepCanvasResponse {
pub body_id: String,
pub step_index: u32,
pub center: (i64, i64), // echoed, same staleness-guard pattern as district_window
pub status: StepCanvasStatus, // Ready | Pending | Error(String)
pub canvas: Option<EncodedStepCanvas>,
}
pub struct EncodedStepCanvas {
pub width: u32,
pub height: u32,
pub morphology: EncodedField, // PNG-per-field — Lake/OpenOcean now basin-sourced (§(e), CLOSED)
pub elev_q: EncodedField, // PNG-per-field
pub temp_dc: EncodedField, // PNG-per-field
pub moisture_q: EncodedField, // PNG-per-field
pub vegetation: EncodedField, // PNG-per-field
pub settlement_id: EncodedField, // PNG-per-field (my new dense field)
pub glaciation: EncodedField, // PNG-per-field, L8 — sim-state plane
pub flooded: EncodedField, // PNG-per-field, L8 — sim-state plane (my new field, needs adding to his struct)
pub courses: Vec<RiverCourse>, // sparse, MessagePack-native — overflow-basin outlets extend this (§(e))
pub cliffs: Vec<CliffSegment>, // sparse, MessagePack-native
}
```
**One reconciliation note against Dudley's struct as literally drafted:**
his `dudley-round2.md` §(a) code sample predates my TTL-split work in this
document and lists `glaciation` alongside the other five dense fields
without a `flooded` field or a plane distinction — that's sequencing, not
disagreement (his draft cites "Araminta's new dense field
`settlement_id`" by name, so he was already building directly off my
round-1 schema, just before the sim-state-plane work landed in this
round-2 pass). `EncodedStepCanvas` needs `flooded` added as a ninth
field. **No further field is needed for lakes** — §(e)'s final answer
(post-ratification, superseding an earlier draft of this section that
proposed `water`/`lakes` as new fields) reuses `morphology` and
`courses` unchanged; the lakes fix is a derive-pipeline data-source
change (Dudley's basin-membership sampling into
`derive_morphology_zone`, plus a follow-up D8-wiring task for the
outflow-course extension), never a wire-schema addition. Dudley's
whole-payload-together envelope argument still applies to `flooded`
one more field derived in the same row-chunked pass doesn't reopen the
flat-vs-split question — but there is nothing further to reconcile on
the lakes item specifically; it resolved to zero new fields.
### Encoding, confirmed unchanged from round 1
**PNG-per-field for every dense array field, MessagePack-native for every
sparse list field.** Nothing in round 1's cross-agent convergence or
Jeroen's lead-interview-1 rulings changes this — it's the unambiguous
T-1179 winner (smallest *and* fastest at every measured size,
21×–563× better than the alternatives against the reference cap), and no
other participant's round-1 position argued for a different encoding.
Restating the two encoding rules precisely, now that the field list is
final:
- **Dense fields** (`morphology`, `elev_q`, `temp_dc`, `moisture_q`,
`vegetation`, `settlement_id`, `glaciation`, `flooded`): each field is
its own PNG-encoded plane. `glaciation` and `flooded` specifically as
**L8** (single-channel, per Stig's measurement ⑤ finding that L8 is
49× cheaper than RGBA8 at every size) — both are already single-byte
values with no need for a wider channel format.
- **Sparse fields** (`courses`, `cliffs`): MessagePack-native
`Vec<Struct>`, unencoded beyond the envelope's own framing — both are
already small relative to the dense planes (T-1170's ~12 KB typical
for courses; cliffs smaller still given the rarity finding), and
MessagePack's compact `bin`/array framing is already efficient at these
sizes per T-1179's own measured 6.00 bytes/cell raw rate finding (no
wasted per-element type tags).
### The full field list, final
Restating (a)'s schema in wire-contract form, the complete answer to "the
per-gridunit payload schema + named-feature encoding + envelope framing"
(brief Expected Output 4):
**Per-step-canvas payload** (rides the tagged envelope, §1d's amendment
text — the new carrier, not `district_window`):
- 6 static + 2 sim-state dense fields (listed in (a) — field count
**unchanged** from the original round-2 pass; §(e)'s lakes fix reuses
`morphology`, adding no field)
- 2 sparse feature lists (`courses`, `cliffs`) — `courses` gains
overflow-basin outlet edges once §(e)'s D8-wiring follow-up lands, same
field, no new carrier
- Envelope/echo fields: `center`, `step_index` (replacing `granularity`/
`granularity_v2` per Tyre's §1c `select_rung`-replacement ruling),
`min_wl_m` (unchanged purpose — octave cutoff echo, still needed for
cache-key correctness per the existing doc comment's reasoning). At
rung 0 (the opener), `width`/`height` are body-dependent
(`derive_region_count(body_radius_km)`) rather than the fixed
3840×2160-class budget every other rung uses — per (b)'s correction.
**Whole-body lookups** (unchanged carrier — the existing names-only
request path, D-223's shape, fetched once per body, never inside the
step-canvas envelope):
- `atlas_city_names` (existing)
- `river_names: BTreeMap<u32 /* edge_id or trunk-root id */, String>`
(new, my round-1 proposal, unchanged in round 2 — nothing in round 1's
cross-agent discussion touched river naming, so I'm carrying it forward
as-is)
### What round 1 already confirmed and I'm not re-arguing
- The tagged-envelope migration itself: triggered, not merely likely
(Tyre §1d, unanimous across Dudley/Araminta/Tyre in round 1).
- Viewport-sized canvases at every rung below the opener (Dudley + Tyre,
independently argued convergence) — **corrected per (b)'s post-
ratification fix:** rung 0 (the opener, variable per-body extent) is
the **one** exception; Region is now rung 1, a **fixed**, viewport-
sized, evictable rung like every rung below it, not the exception
rung. Every rung from Region (rung 1) down uses the viewport-sized
policy — the "whole-body canvas" property belongs to rung 0 alone.
- One colorizer family, same field set at every step (my round-1 §3,
reconfirmed here as applying to the final field list including the new
cliff/sim-state fields — nothing about the TTL split or the cliff
ratification changes the "same vocabulary at every step" guarantee,
since neither is a new *classification*, just a new *field*).
### Byte-size implication of the final schema (not a new measurement — reasoning from T-1179's numbers)
The final schema adds two new dense fields (`settlement_id`,
`flooded``glaciation` already existed) and one new sparse field
(`cliffs`) to T-1179's originally-measured six-field set. None of these
change the order-of-magnitude verdict: T-1179's own six-field payload was
already 21×–563× the 30 KB reference cap at the three measured sizes; two
more near-free sparse/single-channel-dense fields (settlements are rare
and spatially coherent per the RLE precedent; cliffs are measured as
near-zero-occupancy) don't meaningfully change that ratio. **The
tagged-envelope call remains settled by round 1's numbers — I'm not
requesting a re-run of T-1179 for the final field list, since the
schema's byte-cost character (dominated by the same
`morphology`/`vegetation`-vs-`elev_q`/`temp_dc` compressibility split
T-1179 already characterized) hasn't changed, only grown by a small,
well-understood margin.**
---
## Summary for the lead interview
1. **(a) Payload schema, final:** 8 dense fields (6 static-geometry + 2
sim-state: `glaciation`, new `flooded`) + 2 sparse lists (`courses`,
new `cliffs`, the latter CLOSED per (b) below) + 2 whole-body name
lookups (`atlas_city_names`, new `river_names`). TTL split is physical
field separation, not a post-hoc policy — static plane cached
indefinitely-fresh (with storage-budget eviction per Jeroen's
amendment, distinct from staleness), sim-state plane short-TTL and
independently re-fetchable. Byte cost: separate L8 planes win over
bitfields on both compression (T-1179) and correctness (independent
re-fetchability) grounds — ruled against packing. Staleness-axis
reasoning stated per field, not inferred from the name: `glaciation`/
`flooded` are sim-state because their *live* value is the map-relevant
fact (seasonal frost, current water-height-vs-elevation); `temp_dc`
stays static because the map consumes it as a climate-baseline
classification input, not a live weather reading — same underlying
D-226 clock mechanism, different consumer question.
2. **(b) Rung 0 (opener) vs. rung 1 (Region): CORRECTED post-
ratification, not the three-way "Region = step 0" closure this
summary previously stated.** Jeroen's own review caught the gap: the
opener is its **own rung** (rung 0, variable per-body extent —
`round(2πR / 204.8 km)` regions, D-243's elastic seam made visible on
the wire, canonical/always-kept) sampled at region-sized gridunits;
Region is **rung 1**, the largest *fixed*, viewport-sized, evictable
rung — not the canonical tier. My spacing intuition (region-sized
gridunits at the top) was right; my rung *count* was wrong (I
collapsed two rungs into one). Tyre is producing the equivalent
correction in his own amendment texts in parallel; I'm conforming to
his filed terminology rather than contesting it if it differs from
"opener/GLOBAL." Also closed, unaffected by this correction: the
cliff wire shape (`cliffs: Vec<CliffSegment>`, sparse list,
zero-length when nothing carved, `#[serde(default)]`, parallel to
`courses`) — Tyre confirmed his round-1 "new arrays over
steal-a-bit" language was against bit-stealing an existing byte, not
for dense per-gridunit arrays.
3. **(c) Label de-dup: a deterministic, identity-keyed anchor rule**
(mean-position/tie-break for point features, cropped arc-length
midpoint for line features), computed independently per canvas/tile —
explicitly *not* a cross-tile coordination protocol. The real seam
risk was never "two labels," it was "two labels disagreeing" — both
structurally prevented by the schema already carrying stable ids.
Added the missing step-cross corollary (only the authoritative
canvas's annotation layer draws during hold-fetch-swap) since no
round-1 document stated it explicitly.
4. **(d) Final wire schema: CLOSED.** PNG-per-field (dense) +
MessagePack-native (sparse), unchanged encoding verdict from round 1,
field list finalized per (a) with the cliff shape closed per (b).
Envelope framing confirmed by Dudley (`dudley-round2.md` §(a)): one
flat tagged `StepCanvasResponse`/`EncodedStepCanvas`, my default shape
(a) held without qualification — every field derives in the same
row-chunked pass, so splitting the wire buys no compute savings and
only adds round-trip/partial-state cost; the progressive-paint UX case
is solved client-side (deferred annotation-layer draw calls) for free.
One implementation note carried forward: `EncodedStepCanvas` as
Dudley drafted it needs `flooded` added as a ninth field (sequencing,
not disagreement — his draft predates this round's TTL-split work) —
the addition doesn't reopen the flat-envelope ruling, since the plane
split is a client-side re-fetch-cadence concept, not a wire-framing one.
**Status at this point in the document: (a), (c), (d) fully closed, no
open cross-checks. (b) carries a post-ratification correction**
(rung 0/opener vs. rung 1/Region, per Jeroen's own review) rather than
standing as originally filed — the correction is architectural (which
rung is canonical, what "extent" means at the top), not a reopening of
the field-set or wire-schema work in (a)/(d), both of which are
unaffected. **One further item follows this summary, §(e) below (lakes)
— read past this point before treating the document as final; the true
closing status is stated at the end of §(e).**
---
## (e) CLOSED — lakes: morphology-fold, sourced from settled hydrology; endorheic cue via outflow-course presence
**Final answer, superseding my original "new `water` field" proposal
below.** Dudley's post-ratification addendum (`dudley-interview2-response.md`
§5) corrects a factual premise my original rejection of folding into
`morphology` was built on — I verified his code-read directly
(`district_profile.rs:556-565`, `generator.rs:1198-1223`,
`features.rs:104-105`) before accepting it, not on his say-so. The
reconciliation below is mine to make since I own encoding; his
derive-pipeline slotting is adopted unchanged.
### What I got wrong originally, and what actually resolves it
**My original §(e) rejected folding into `morphology` on the premise
that lake-vs-not would "require... widening an already-frozen
vocabulary."** That premise is false. `MorphologyZone::Lake`
(discriminant 1) **already exists** in the frozen 17-zone enum
(`generator.rs:1202`, doc-commented *"Lake body — hub-and-spoke;
perimeter access toward water"*) — confirmed by direct read, not
assumed. The gap was never a missing vocabulary entry; it's a missing
**data source**. Today, `derive_morphology_zone` emits `Lake` from a
crude threshold (`ocean_fraction_q >= 60`, `district_profile.rs:563-565`)
where `ocean_fraction_q` is a bilinear sample of `TerrainAnalysis
.ocean_mask`, itself nothing more than `elev[i] < sea_level` on the raw
heightmap (`features.rs:104`) — **zero connection to
`HydrologyResult`'s settled-equilibrium basins.** The code's own comment
at line 560 admits this ("Lake differentiation lives at ChunkContext") —
it was already a known, named gap before this workshop, just never
connected to the solver this workshop's ① measurement is about.
**My second objection — that per-cell basin membership is finer-grained
than morphology's region-level classification — is answered by Dudley's
sourcing fix, not by a new field.** The fix doesn't ask `morphology` to
carry a coarser aggregate than the solver computes; it asks
`derive_morphology_zone` to gain a **third input** (basin membership,
sampled the identical bilinear way `ocean_fraction_q` already is) and
emit the *existing* `Lake` discriminant when that membership test hits,
falling through to today's heuristic otherwise. This is per-gridunit
resolution at the SAME granularity every other `morphology` gate
already operates at (`derive_morphology_zone` is called per-gridunit
today, gated on `slope_q`/`elev_q`/`ocean_fraction_q` — all already
per-cell bilinear samples of coarser working-grid fields). There was
never an actual grain mismatch; I mis-stated the objection by treating
"basin membership" as if it were categorically different from every
other per-cell classification input `derive_morphology_zone` already
consumes, when it's the identical shape.
**Verdict: fold into `morphology`. No new dense field. Zero new wire
bytes.** `water: Vec<u8>` from my original proposal is withdrawn — the
byte-cost argument I made for it (near-zero, PNG compresses contiguous
classification blobs well) transfers unchanged to reusing `morphology`,
except the cost is now not just near-zero but **exactly zero**, since no
new array exists at all. This is a strictly better outcome than my
original proposal on every axis I originally argued from (byte cost,
frozen-vocabulary discipline, one-colorizer-family consistency) — the
correction makes the schema simpler, not just different.
**Sea vs. Lake stays exactly as today**`OpenOcean`/`Lake` are both
already-existing discriminants; nothing about this fix touches the
`ocean_fraction_q >= 80` open-ocean tier. Only the `Lake` emission site
gains a second, more-authoritative trigger (basin membership) ahead of
the existing heuristic fallback.
### Endorheic-vs-overflow — corrected mechanism, honest wiring caveat stated
**The coordinator's flagged weak point is real, and I'm ruling against
my own original proposal's premise, not defending it.** My original
"distinguishes via the sparse `cliffs` list" reasoning assumed carved
`cliff_edge` cells would exist often enough to be a usable signal.
T-1177's own POPULATION SURVEY closes that door completely: **zero
carved cells, zero carved-outlet basins, across all 267 real committed
bodies (100.00%)** — not "rare," observed *nowhere*. A visual cue wired
through `cliffs` would never fire in practice. I checked this number
directly in `measurements/t1177-hydrology.md` before accepting the
correction, not on the coordinator's relay alone.
**Chosen mechanism: option (a), outflow-course presence — zero new
data, semantically true, matching option (b)/(c)'s crossover concern
without needing either.** An overflow basin, by `BasinOutcome`
definition, has a resolved outlet path to the sea/another basin/open
ground; an endorheic basin has none. A river course (`courses:
Vec<RiverCourse>`, T-1170) leaving a lake gridunit IS the honest visual
signal for "this lake drains" — no course leaving is the honest signal
for "this lake doesn't." This needs no new field, no per-basin outcome
byte, and sidesteps the crossover-size question I raised for option
(b)'s `Vec<LakeBasin>` entirely (that question is now moot — there is no
`lakes` list to size).
**Honest wiring caveat, stated precisely because it's the one place this
isn't yet "free" in the way the coordinator's framing implied:** T-1177's
own document is explicit in its "does NOT do" scope —
*"`HydrologyResult`/`Basin`/`BasinOutcome` are prototype-only types, not
wired into `RiverNetwork`... or any `AtlasLayerResponse` payload."* I
checked this directly (`t1177-hydrology.md` line ~362) rather than
assuming the outflow-course mechanism already connects end-to-end. It
does not, today. `courses`/`river_downstream` are D8-network-derived
(T-1170's mechanism, keyed on `fdir[i]`/river cells extracted by
`drainage.rs`'s existing flow analysis); a hydrology basin's resolved
`outlet_path` (Dijkstra overflow routing, a *different* computation) is
not currently threaded into that D8 river network at all. **This is not
a reason to reject option (a)** — Ruling 7b of the T-1170 design
document already anticipated exactly this extension and pre-cleared it
as additive (*"the `TERMINAL` downstream sentinel is reserved now...an
interior sink becomes an additive drainage-extraction change, not a wire
migration; and the termination mechanism... generalizes unchanged to a
terminal lake shoreline. When you want endorheic basins, the work is in
D8 sink retention and lake morphology, and every piece this batch builds
consumes them without modification"*) — but it IS a real, small,
implementation-scoped wiring task, not something the wire schema gets
"for free" the moment `water=Lake` ships. **Stating the honest scope:**
an overflow basin's spill point needs to register as a D8 downstream
continuation (feeding `river_downstream`) rather than terminating at the
lake, so `courses` picks up the outlet edge naturally; an endorheic
basin's absence of such an edge is what already reads as "no course"
under the existing mechanism, requiring no change at all. This is
follow-up implementation work for whoever wires basin membership into
`derive_morphology_zone` (Dudley's pipeline-slot answer, below) —
flagging it explicitly rather than letting the schema doc imply the
visual distinction ships automatically the day `water=Lake` lands.
### Visual treatment — unchanged from my original proposal, still holds
Lakes draw as water at every rung, same colorizer family as sea/ocean
morphology — no new palette, per my round-1 §3 one-colorizer-family
guarantee (this was never contingent on the field being `water` vs.
`morphology` — a `Lake` discriminant colors identically either way).
Endorheic-vs-overflow reads via course presence at the lake's outlet,
not a distinct fill color or texture on the lake body itself — simpler
than my original "secondary visual cue" proposal (texture/saturation/
outline treatment), since a present-or-absent outflow course *is* the
visual difference, no additional styling rule needed from Stig beyond
drawing `courses` the way (c)'s label-anchor work already assumes he
does.
### Byte cost — better than originally argued, not merely confirmed
**Exactly zero new wire bytes**, stronger than my original "near-zero"
claim. `morphology`'s existing per-field cost (already priced across
every canvas size in T-1179's table) is unchanged in shape — only the
*source* of a subset of already-shipping `Lake`/`OpenOcean` discriminant
writes moves from a heightmap threshold to a hydrology-basin test. No
new array, no new encoding question, no new crossover-size analysis
(the `Vec<LakeBasin>` sizing question is withdrawn along with the field
it would have carried).
### Pipeline slot — Dudley's answer, adopted unchanged
`HydrologyResult` computed once per body at 512×256 (held in the
D-203-shaped global-tier cache, T-1177's own scope); a finer-rung
canvas derive needs basin membership at a gridunit's world position —
Dudley's proposal: `TerrainAnalysis` gains a basin-membership field
(basin id or bool, populated once when hydrology solves), sampled the
SAME bilinear way `ocean_mask`/`ocean_fraction_q` already are, feeding
`derive_morphology_zone`'s existing water tier as a new gate ahead of
the `ocean_fraction_q` fallback. This is round 2 §(b)'s seed-chaining
mechanism B (sample a coarser rung's continuous primitive, never
re-solve) applied to hydrology exactly as it's already applied to
`sea_level` — no new architecture, no new bench needed (his own
per-cell rate numbers already include equivalent-cost sampling
operations in the measured budget). Adopted without modification —
this is implementation-pipeline territory, not schema/encoding, and his
reasoning is sound on direct inspection.
### Convergence status: CLOSED, no open sub-questions remain
Unlike the original §(e), there is no remaining sizing call routed to
Dudley — the `lakes: Vec<LakeBasin>` vs. `water` 4th-value crossover
question is moot (neither ships; the fold-into-`morphology` answer
needs neither). The one caveat carried forward is the outflow-course
wiring gap named above, which is implementation scope for whoever lands
the basin-membership sourcing fix, not an open design question for this
document.
---
## True closing status (supersedes the "Summary for the lead interview" section above) — DOCUMENT FULLY CLOSED
- **(a) Payload schema:** closed. Field count is **unchanged** from the
original round-2 pass (six static + two sim-state dense fields, two
sparse lists) — §(e)'s lakes fix reuses `morphology`/`courses`, adding
nothing.
- **(b) Rung 0/1 split:** corrected in this pass (opener = rung 0,
variable extent, canonical; Region = rung 1, fixed, evictable) —
architectural correction, terminology pending final conformance to
Tyre's parallel amendment text.
- **(c) Label de-dup:** unaffected by both post-ratification items,
closed as before.
- **(d) Wire schema:** closed. No field-count change from §(e) — the
`EncodedStepCanvas` struct is annotated where the lakes fix lands
(`morphology`, `courses`) but gains no new field.
- **(e) Lakes: CLOSED, no open sub-questions.** Reversed my own original
proposal after verifying Dudley's code-read directly
(`district_profile.rs`, `generator.rs`, `features.rs`): `morphology`
already carries a `Lake` discriminant, mis-sourced from a heightmap
threshold instead of settled hydrology — the fix is a data-source
correction (Dudley's basin-membership sampling, adopted unchanged), not
a wire addition. Endorheic-vs-overflow resolved via outflow-course
presence (option (a) per the coordinator's framing), after directly
confirming in `t1177-hydrology.md` that the originally-proposed
`cliffs`-based cue would never fire (zero carved cells, 267/267 real
bodies) — ruled against my own earlier reasoning once the population
survey data made it untenable. One honest implementation caveat carried
forward (not a design question): `HydrologyResult`'s basin outlets
aren't yet wired into the D8 river network `courses` draws from —
pre-cleared as additive by T-1170 Ruling 7b, follow-up work for
whoever lands the sourcing fix, not a wire-schema gap.
**Whole document status: fully closed, no open cross-checks, no
pending sub-questions routed to any other participant.** Ready for
filing.
@@ -0,0 +1,79 @@
---
title: "Body Map Viewer — How It Now Works"
description: "Jeroen's original outline written back as-built: the ratified architecture in the same style and brevity"
type: workshop
status: active
workshop: body-map-viewer
created: 2026-07-25
---
# How it now works
Written back at Jeroen's request after the final ratifications, in the style and
brevity of [his original outline](jeroen-outline.md); corrected after his
Global/Region rung review and the lakes reconciliation.
---
To honor late/lazy compute, everything is designed around LoD information exposure
with two separate eviction axes: nothing is ever *stale* (determinism — a canvas
for a fixed seed is byte-valid forever), but sub-global geometry still gets
*evicted on time-since-last-visit* to save storage for planets you visit once.
Only the live sim-state components carry a real TTL: one clock-bucket of the
field's own fastest driver (tidal for flooded on moon-bearing bodies, seasonal
otherwise) — the map can be at most one bucket stale, and the sim has no fresher
answer than that. The global level for each body is always kept (~9 MB for all
267 bodies, PNG-encoded — small enough to simply stay resident in memory) so
atlas navigation is snappy after first calc.
architecture:
- pixel drawing is a function of the client. The map component has two layers: a
render-to-texture terrain layer (the data canvas colorized CPU-side — measured
cheap, ~78 ns/cell — into texel-exact textures, L8 planes where single-channel)
and an unscaled screen-space annotation layer for names, glyphs,
rivers-as-lines and markers. There is no zoom-scaled canvas anymore; the entire
compensation error class is dead by construction. What is shown and drawn is a
map art function, not a data function — the client styles and tweens, but never
invents geometry the wire didn't carry.
- content determination is a server function. The server answers "what is at this
world coordinate at this zoom step" with one flat tagged StepCanvasResponse:
dense per-gridunit fields (biome/morphology, elevation, temperature, moisture,
vegetation, settlement-id — plus frozen and flooded as separately-fetchable
short-TTL planes), sparse feature lists (river courses, cliffs — zero-length
almost everywhere), and whole-body name lookups joined client-side.
PNG-per-field encoding: a full data canvas is ~638 KB, ~5 ms to encode.
- zoom levels are stepped. The map opens on the **body surface — the global view,
rung 0**: a canvas sized by the body itself (as many regions as the body has,
one gridunit per region — the elastic seam made visible), and this is the one
canonical, always-kept canvas. Below it, **five fixed-size rungs**, each pinned
to a D-243 unit: **Region → District → Quarter → Block → Chunk (64 m, the
deepest)** — all viewport-sized and evictable, never whole-body. Scroll clicks
step through them, cursor-anchored, edge-scroll panning. The bottom-out rule is
simply 1 screen px per 64 m gridunit — no magnification margin needed, because
a chunk is already a legible map feature. The tile (1 m) level is not map
content; walking on tiles is the Phase-5 viewport's job. Display fidelity is
step-dependent: 1×1 px per gridunit at the deep steps where detail matters,
relaxing toward ~5×5 only at the shallow end where extent is what grows.
- on opening of a body: the heightmap and inputs are read, the seed generators
run the global calculations — including settled hydrology: rivers that end in
basins fill lakes to their true spill level, overflow onward to the sea or hold
as endorheic when the climate supports it, and carve gorges only where the
geometry truly demands it (measured across all 267 real bodies: it never has
yet — the cliff fields ship empty but the arithmetic is proven for the body
that someday needs them). This all lands in the global canvas: not drawn, but
determining what is in each gridunit — and it is kept forever. The lakes it
fills draw on the map as water (the existing Lake classification, sourced from
the settled solver; lake edges refine with zoom the same way coastlines do),
and an endorheic basin reads exactly as it should: a lake with no river
leaving it.
- on zooming a step: the server takes the viewport bounds, derives the step's
data canvas at that rung's spacing (~64 ms for a typical canvas, everything
measured, worst cases bounded), with each finer step consuming the coarser
layers as seed information the deterministic way — reading the continuous
primitives and baselines (cache-accelerated when the coarser canvas is
resident, derived fresh when not, byte-identical either way). The client
colorizes, textures, annotates, and the map simply shows more truth the closer
you look.
@@ -222,13 +222,14 @@ necessary (workshops are not closed early on partial convergence).
| # | Ticket | Headline result | Doc |
|---|---|---|---|
| ① | T-1177 | **Settled hydrology VIABLE**: 512×256 ~24 ms/body; all 273 bodies Rayon-parallel ~0.70.8 s; 8.3M cells ~5.7 s single-thread (no production path needs it synchronously). Cliff representation: dominant height + `channel_depth` + `cliff_edge` flag. Finding: gorge carving is structurally RARE (priority-flood finds true spill levels; zero carved cells at production scales — needs chained-basin geometry). Solver: priority-flood + Dijkstra overflow, pure function, determinism-proved. | [t1177](measurements/t1177-hydrology.md) |
| ① | T-1177 | **Settled hydrology VIABLE**: 512×256 ~24 ms/body; all 273 bodies Rayon-parallel ~0.70.8 s; 8.3M cells ~5.7 s single-thread (no production path needs it synchronously). Cliff representation: dominant height + `channel_depth` + `cliff_edge` flag. Finding: gorge carving is structurally RARE (priority-flood finds true spill levels; zero carved cells at production scales — needs chained-basin geometry). Solver: priority-flood + Dijkstra overflow, pure function, determinism-proved. **POPULATION SURVEY addendum (post-adversarial, Troblum finding that the 273-body bench was one body solved 273×): all 267 real committed bodies solved independently, ~0.86s total, ZERO carved cells / zero cliff_edge cells population-wide (267/267) — the rarity finding upgrades from "one real body + structural argument" to "not observed anywhere in the real population," strengthening the cliff Phase-4 wire-cost case.** | [t1177](measurements/t1177-hydrology.md) |
| ② | T-1178 | **Parallel throughput HOLDS at scale** (the T-1143 extrapolation gap, closed): ~190220 ns/cell parallel flat from 330K→8.3M cells; 330K canvas ~64 ms, 2.07M ~0.4 s, 8.3M ~1.71.8 s (7.58.8× speedup); single-thread flat ~1.65 µs/cell, matches prior baseline within 8%. Throughput cross-validated on three bodies across two call paths (square real-`build_district_window_layer` = courses-light, 310 in window; rectangular replica loop = courses-empty by construction — agreeing within 0.3%); the courses-inclusive rate at real production density is measured once, GJ1c 330K with 18 courses at 195.0 ns/cell (within 2%, consistent with the <5% course-cost bound). | [t1178+t1154](measurements/t1178-t1154-derive-bench.md) |
| ③ | T-1154 | **Block GO, Tile GO on cost** (~1.81.9 µs/cell, same band as District/Quarter). Octave cutoff buys ZERO below District spacing (verified: wavelength table bottoms at 128 m). Deepest-step realistic canvas (216×384 m @ 1 m = 83K cells): **~17 ms parallel — trivially interactive**. `voxel_relief` already in the Atlas path; `voxel_mosaic` untouched by it. Real gates are wire carrier + D-226(d) canvas policy, not cost. | [t1178+t1154](measurements/t1178-t1154-derive-bench.md) |
| ④ | T-1179 | **PNG-per-field wins everywhere** — smallest AND fastest: 330K canvas = 638 KB (0.32× raw, 5.4/3.6 ms enc/dec) vs raw rmp 1.99 MB; RLE loses to raw (elev/temp near-noise per cell). **No encoding brings a step canvas near the existing windowed-payload budget** (best case 21× the ~30 KB cap at the smallest size; 563× at 8.3M) — the tagged-envelope migration cannot be dodged by payload optimization. Whether the letter of the D-226 §2 *field-count* rule is what triggers it is a workshop synthesis call, not a measured result. Corrected density: 6.00 B/cell measured raw. | [t1179](measurements/t1179-wire-table.md) |
| ⑤ | T-1180 | **Upload cost is a non-issue**: worst case (8.3M px RGBA8 create) ~3.2 ms median, ~2.42.9 ms frame-delta spike — never near the 16.6 ms budget. **L8 is 49× cheaper at every size** (~0.50.7 ms at 4K). Prefer `texture.update` reuse on step-cross; use L8 for single-channel planes. Windowed-only measurement (headless renderer fakes uploads). | [t1180](measurements/t1180-imagetexture.md) |
| ⑥ | round 2 (lead interview 1 ruling) | **MEASURED — CPU colorize is the c1 gate, not upload.** `Image.set_pixel` flat ~78 ns/cell (330K→8.3M, no cliff): 330K ~25.7 ms, 2.07M ~165 ms, 8.3M ~644 ms. Direct `PackedByteArray` write is ~2× SLOWER (~153 ns/cell) — counter to the naive "skip the method call" assumption; **prefer `set_pixel`**. At the realistic per-step canvas size (330K, Dudley's viewport-sized recommendation) colorize is cheap (25.7 ms) and ranks second only to server derivation in the arrival-side cost chain (ahead of ④'s decode and ⑤'s upload); at the 8.3M stress-ceiling size it becomes non-trivial (~1/3 of server derive time at that size) — reinforces viewport-sized canvases over any canonical whole-canvas colorize. Headless-valid (pure CPU, no GPU path, unlike ⑤). | [t-setpixel-c1](measurements/t-setpixel-c1.md) |
Bench code (all `#[ignore]`d release tests): `server/src/atlas/hydrology_equilibrium.rs` + `server/tests/hydrology_equilibrium_bench.rs`, `server/tests/bmv_gridunit_bench.rs`, `server/tests/wire_encoding_bench.rs`.
Bench code (all `#[ignore]`d release tests): `server/src/atlas/hydrology_equilibrium.rs` + `server/tests/hydrology_equilibrium_bench.rs`, `server/tests/bmv_gridunit_bench.rs`, `server/tests/wire_encoding_bench.rs`. Client-side: `client/tests/` (temporary uncommitted drivers per measurement doc, ⑤/⑥).
## Input Documents
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,351 @@
---
title: "Body Map Viewer — Dudley Round 1"
description: "Server derivation position: hydrology algorithm + cliff representation, step-canvas generation + canonical-vs-viewport, compute-chunk partitioning, cache tiers"
workshop: body-map-viewer
round: 1
owner: Dudley
status: complete
decision_refs: [D-166, D-226, D-227, D-243, D-203]
---
# Dudley — Round 1 Position
All four numbers I was gated on are in the appendix and I helped produce three
of them (①②③, plus half of ④'s framing). I'm not going to re-derive them here —
I'll cite and build on them. Nothing below is extrapolated past what
`t1177-hydrology.md`, `t1178-t1154-derive-bench.md`, and `t1179-wire-table.md`
measured. Where I lean on Stig's ⑤ for the render-side implication of a
server decision, I'll say so explicitly.
## Question 1 — Hydrology: algorithm, cost, cliff representation
**Algorithm: priority-flood (Barnes/Planchon-Darboux class) + Dijkstra overflow
routing, exactly as prototyped in T-1177.** Min-heap flood fill from
below-sea-level + grid-edge seeds finds each basin's true spill point; ascending-
spill-level basin processing lets a resolved lower basin be a valid target for a
higher basin's overflow search; carving applies only where peak flow accumulation
on the carved path clears `RIVER_THRESHOLD = 200`. This is a **pure function**
(`solve(elevation, width, height, sea_level, climate) -> HydrologyResult`) — no
tick loop, no RNG, no wall-clock — which is the only shape D-010 and D-227 permit
for anything that later feeds a save-relevant derivation. I'm recommending we
adopt it as-is; the prototype module (`server/src/atlas/hydrology_equilibrium.rs`)
is production-shaped, not throwaway.
**Cost, both regimes measured:**
- **Single-body solve** (the real production shape — hydrology settles once per
body, like `drainage::analyze` does today): 512×256 (real GJ1c working grid) ≈
24 ms single-threaded. This is cheap enough to not need parallelizing internally
at all.
- **All 273 bodies, Rayon `par_iter` across bodies** (not within one solve — see
below for why): **0.70.8 s total**, both initial and stability-re-run passes
agree within 1-3%. This is the number that answers "can we settle hydrology for
the whole Reach at world-open without it being felt": yes, comfortably, and it
parallelizes on the *correct* axis.
- **8.3M-cell single grid** (the stress ceiling, not a real request shape): ~5.7 s
single-threaded. I want this number on record because it's the honest "computer
catches fire" case Tyre's implications doc asked for — but no production path
needs to pay it. Hydrology is a **per-body Layer-1 property**, computed once at
512×256 (or whatever the working-grid resolution ends up being — see the
step-canvas discussion below for why 512×256 stays the right size even in the
stepped model), not re-solved per step-canvas or per zoom step. It is exactly
the kind of thing D-203's `BodyWorldState` LRU cache already exists to hold.
**Why `solve()` isn't internally parallelized, and why that's fine.** The
priority-flood fill and the Dijkstra overflow search are both globally sequential
by construction — one shared min-heap with strict pop order, architecturally
identical to `road_graph.rs`'s own unparallelized `astar`. A basin's outcome can
depend on a lower basin having already resolved (the chaining case in the
`DownstreamTarget` model), so cells are not independent pure functions the way
`layer_proxy.rs`'s row-chunked derive is (D-227's per-cell independence, which is
*exactly* what lets that path parallelize so cleanly — see Q3). Hydrology doesn't
have that property internally. It doesn't need to: the real workload is "273
independent bodies," and that parallelizes trivially and is the number that
matters.
**Cliff/multi-height representation (red flag 4).** Proposal, grounded directly
in what the solver already computes, not chosen independently of the
measurement: **dominant height (`elevation`, unchanged) + `channel_depth: u16`
(quantized) + `cliff_edge: bool`.**
- `elevation` keeps carrying the gridunit's rim/dominant/walkable height — zero
change to the existing single-height contract for the non-gorge case (which is
nearly every case — see below).
- `channel_depth` is a direct, non-lossy carry of `channel_depth_scaled` — the
solver already produces this number, no re-derivation downstream.
- `cliff_edge` marks a rim/discontinuity cell so the client map-art function
renders a cliff-face transition instead of a smooth gradient at that boundary.
I considered and rejected a min/max height pair: it loses the *shape* of the
transition (point-drop-at-one-edge vs. spans-the-whole-gridunit) that
depth+edge-flag preserves implicitly, and it's a worse fit to what the solver
actually outputs — it would require synthesizing two heights from one measured
depth value for no informational gain.
**The honest finding that should reframe red flag 4's urgency: gorge carving is
structurally rare.** All three production-scale benches (512×256, 768×432,
3.84M... 8.3M) carved **zero cells**. This isn't a bug — I traced it to a real
structural property: priority-flood finds the *true* global minimum rim of a
basin, so the first cell any overflow search reaches outside a sealed lake is, by
construction, never higher than that basin's own spill level. A single sealed
basin, however jagged its rim, always carves zero. Genuine carving needs a
narrower geometry — two independently-sealed basins connected by a corridor
exactly one cell wide in both dimensions and higher than both basins' own rims —
which is real terrain but rare at continental working-grid resolution. I
constructed four fixture attempts trying to force it and each one correctly
diagnosed as "basins merged" or "spill level absorbed the connector," not a code
defect (`server/src/atlas/hydrology_equilibrium.rs`, "Gorge carving" test
section, four named tests covering the mechanism directly since an end-to-end
production-scale trigger proved impractical to construct honestly).
**What this means for the wire-cost side of red flag 4:** the field is real and
needed for correctness, but it will be populated `false`/`0` for the overwhelming
majority of gridunits at every canvas size measured. The wire-cost argument
against including it is weaker than the red flag assumed — a rarely-nonzero
`bool` + quantized `u16` is cheap under any of the encodings in ④'s table (PNG's
DEFLATE in particular loves a field that's constant almost everywhere). I'd
rather we carry it at Phase-4 Atlas scope than defer cliffs to Phase-5 purely on
a cost argument that the measurement doesn't actually support — Tyre and
Araminta should weigh in on whether "rare but real, cheap to carry" changes the
Phase-4-vs-Phase-5 call red flag 4 posed; I'm only settling the representation
and the frequency, not the scope ruling.
## Question 2 — Step-canvas generation and canonical-vs-viewport (red flag 3)
**Recommendation: viewport-sized canvases at every step, no canonical fixed
canvas anywhere on the ladder. This isn't just the cheaper cache shape — at the
deep steps it's the only one that stays legal under D-226(d).**
The reasoning, stated plainly because red flag 3 called this "the sharpest thing
in the whole design" and I don't think that's overstated:
- A **canonical** fixed-size canvas at a given step's spacing would have to cover
enough of the body to serve every possible viewport at that step — at shallow
steps (region/district spacing) that's affordable (it's what the orbital tile
mosaic already does today, per-tile ImageTextures at capped density). At the
**deepest** step (10 px/tile, 1 m spacing), a canonical canvas covering enough
ground to be reusable across viewports is a near-whole-body metre-resolution
derivation — which is precisely what D-226(d)'s surviving whole-body-planetary-
layer prohibition forbids, independent of whether the cost is affordable.
- A **viewport-sized** canvas at 1 m spacing is exactly the 216×384 m (82,944
cell) shape I measured in T-1154: **17 ms parallel, comfortably interactive**,
and by construction never a whole-body layer — it's bounded by what's on
screen, the same "windowed viewport" carve-out D-226(d) already opened for the
district/quarter rungs (T-1124 amendment). This is the *only* canvas policy
that keeps the deep end of the ladder inside the boundary that survives this
workshop's governance delta.
**Practical shape at each step: viewport-sized, sized to a fixed pixel budget
(the same "land on a 4K window" instinct from Jeroen's outline), not to the
literal current window size.** Concretely: server renders each step's data
canvas at a fixed canvas-pixel budget (e.g. 3840×2160, or whatever the
round-2 wire-contract decision lands on) regardless of the client's actual
monitor resolution, the same way the current windowed-family already treats
`WIRE_CAP_CELLS` as a budget, not a literal viewport echo. This gives headroom
for larger monitors without re-deriving per-monitor, and it's the shape all
four of my measured cost numbers (330K/2.07M/8.3M) already assume — I didn't
measure a "canonical whole-body" shape at any step because I don't think one
should exist past the region/orbital rungs.
**Cost at each rung, all MEASURED not extrapolated (own numbers, T-1178/T-1154):**
| Step spacing | 330K canvas (parallel) | 2.07M canvas (parallel) | 8.3M canvas (parallel) | Realistic viewport canvas |
|---|---:|---:|---:|---|
| District (2,048 m) | 63.7 ms | 394.6 ms | 1,827 ms | — |
| Block (128 m) | same band, ~190-220 ns/cell | same band | same band | — |
| Tile (1 m) | same band | same band | same band | **82,944 cells (216×384 m) → 17 ms** |
The per-cell parallel rate (~190220 ns/cell) is flat from 330K to 8.3M cells at
every spacing I tested (District through Tile) — this is the T-1143-killing gap
closed: there is no cliff where chunking overhead or cache pressure eats the
win at scale. **Cost does not gate any step in the ladder.** What gates the deep
steps is the wire carrier (④: even the best encoding is 21×–563× the existing
30 KB windowed-payload cap) and the canonical-vs-viewport governance boundary
above — neither is a cost question my measurements can resolve, both of which
I've now priced and named explicitly for Araminta/Tyre's round-2 synthesis.
**One caveat I want on record precisely because it's easy to gloss over:** the
8.3M-cell numbers above are for a *canvas that size*, not a *viewport that
size*. A 3840×2160 canvas at 1 m spacing covers only 3.84 km × 2.16 km of
ground — tiny. If "8.3M cells" is ever read as "a viewport-sized Tile-rung
canvas," that's wrong; the viewport-sized Tile canvas is the 83K-cell number
(216×384 m), not the 8.3M one. The 8.3M row exists to answer "does the derive
path degrade at that many cells" (no), not "is an 8.3M-cell canvas a realistic
Tile-step request" (it categorically is not, by the viewport-sizing policy
above).
## Question 3 — Compute-chunk partitioning across the Rayon queue
**The existing row-chunked `into_par_iter()` loop in `build_district_window_layer`
(`layer_proxy.rs:1564-1586`) is the mechanism, and it holds at every size and
every spacing on the ladder — this is the single most load-bearing "measured,
not extrapolated" result across all four gates.** Same per-cell rate (190220
ns/cell parallel) confirmed at 330K, 2.07M, and 8.3M cells, at District, Block,
and Tile spacing, across three independently-run fixtures (my own synthetic
gradient body, the real GJ1c body, and Araminta's independent GJ338Bd run for
④) — three cross-validating measurements landing in the same band is a robust
finding, not a single-run artifact.
**Step boundaries as compute-chunk boundaries (premise 8) is compatible with
this mechanism as-is, with one caveat.** The row-chunking happens *within* one
step's derive call — it's how a single step canvas's cells get distributed
across the 16 Rayon workers, not how work is split *between* steps. Premise 8's
"step boundaries = compute-chunk boundaries" reads correctly as: each step is
one `build_district_window_layer`-shaped call (one derive request, internally
row-chunked), and steps are never partially computed or streamed mid-derive —
you get a whole step canvas or you're still waiting. That matches the
hold-fetch-swap model Stig's ⑤ measured render costs for.
**The caveat: courses are excluded from every chunked-cost number I measured
at 8.3M and at the deep-step 83K shape.** The row-chunked *replica* loop used
for every rectangular (16:9) canvas measurement is courses-empty by
construction (H2 in `t1178-t1154-derive-bench.md`) — it has no `RiverNetwork`
wiring at all. The real production path (square windows, real river network)
measured courses-inclusive at real density: **+0.090.21 ms against a ~5 ms
District-cap baseline (under 5%)**, cross-checked at 18 real courses in a
331,776-cell GJ1c window (195.0 ns/cell, within 2% of the courses-sparse
synthetic number). So the chunking mechanism's cost story holds either way —
courses add a small, bounded tax, not a different order of magnitude — but
anyone implementing step-canvas serving should wire courses into the row-chunk
loop from day one rather than treating them as a bolt-on, since the deep-step
83K number I reported (17 ms) technically excludes them and I don't want that
caveat to get lost between this document and an implementation ticket.
**One thing I did NOT measure and want flagged for round 2/implementation:**
whether chunking should change shape when a step canvas also needs to serve as
seed input for the next tier down (Jeroen's outline: "this at the same time
serves as seed information for the deeper cascade"). Today's row-chunked loop
produces one flat output array — using that array as *input* to a finer step's
derive is a data-flow question (does the finer step re-derive from the same
`(seed, position)` pure function, or does it consume the coarser step's output
values directly?), not a partitioning-cost question. My measurements assume
each step re-derives from `(seed, position)` independently per D-227's
derive-don't-store discipline (the coarser canvas is not an input the finer
canvas depends on for correctness, only for display continuity/pre-fetch
ordering) — if round 2 wants the finer tier to actually consume the coarser
tier's values as a literal input, that's a different architecture than what I
benchmarked and needs its own cost pass.
## Question 4 — Cache tiers server-side
**The global-tier cost, computed from measured numbers, not a vibe (red flag
2):** Jeroen's "always keep the global level" instinct is affordable **if
"global level" means the orbital/region-spacing rung, not a per-step cache of
every rung at every zoom the player has visited.** Using T-1179's measured
6.00 bytes/cell raw rate (or ~0.32× that with PNG-per-field, ~1.9 bytes/cell
effective):
- At district-spacing global canvas (~330K cells is the deep end of what
"orbital" would ever need — the real orbital rung is coarser, region-spaced,
which is fewer cells, not more): raw ≈ 1.99 MB/body, PNG-encoded ≈ 638 KB/body.
- **Across all ~273 bodies, raw: ~543 MB. PNG-encoded: ~174 MB.** Consistent
with red flag 2's own back-of-envelope (~440 MB at 5×5, ~13 GB at 1×1) — my
numbers land lower because I'm using the *measured* PNG-encoded rate rather
than the brief's pre-measurement estimate, and because "global" for the
orbital rung should be region-spaced (far fewer cells than the district-
spacing canvas I priced above as a conservative upper bound).
**Recommendation: "always keep the global level" = a resident, disk-backed
per-body cache at the orbital/region rung only, never a permanent allocation
for finer rungs.** ~174 MB (PNG-encoded, all 273 bodies) is a reasonable
resident/disk budget for "atlas navigation snappy after first calc" — this is
squarely a caching decision, not a live-memory one, and D-227's "transient
evictable cache" framing applies even to a tier that's practically
never-evicted in normal play: it must still be *derivable*, not
authoritative, so a cold-start or corrupted cache is a recompute, never data
loss. This is the same shape D-203's existing `BodyWorldState` LRU already
established server-side (50-body cap, ~5 MB budget, oldest-`last_accessed`
eviction, pinned current-location/neighbors) — I'd extend that same
resource/pattern to cover the orbital-rung step canvas rather than invent a
parallel cache mechanism. Concretely: `BodyWorldState` (or a sibling resource
following its exact shape) gains an `orbital_canvas: Option<EncodedCanvas>`
field, populated lazily on first Atlas-open for that body by the existing
Rayon background-queue population path (D-206), evicted only on the existing
LRU discipline (which for 273 bodies at ~638 KB PNG-encoded each barely
pressures a 50-entry cap sized for a much smaller ~100 KB/entry Layer-1
budget today — this may argue for either a higher cap or a split resource;
I'd defer that split to whoever implements it, since it's a tuning question
once the byte budget above is accepted, not an architecture one).
**Finer rungs (district/block/tile step canvases): TTL(detail, time,
distance), never "always keep."** This is D-227's concrete eviction policy
made specific:
- **detail** (rung depth) is the dominant term — deeper rungs get shorter TTL,
monotonically, because they're both cheaper to re-derive (my own measured
~190220 ns/cell parallel holds flat across every rung, so re-derivation
cost does NOT argue for longer retention at deeper rungs the way it might in
a system where finer detail was expensive) and cover less ground (so a
given player session visits far more distinct deep-rung windows than
shallow ones, meaning the *hit rate* on a long TTL at a deep rung would be
low anyway — cache entries at Tile spacing are viewport-sized and
effectively single-use across a normal pan).
- **time** and **distance** (player/viewport focus, both character location
AND atlas-viewport-as-player-focus per premise 1) modulate the base TTL the
same way the existing FIFO-256 window cache + client 24-entry LRU already
behave, generalized: entries far from current focus (either kind) decay
faster; entries at the current focus are effectively pinned the way D-203
pins current-location/neighbor bodies.
- **Concretely, I'd propose the TTL formula as a multiplicative discount**:
`ttl = BASE_TTL[rung] × time_decay(age) × distance_decay(distance_from_focus)`,
with `BASE_TTL` the only per-rung tunable and the two decay functions shared
across rungs — this keeps the policy legible (one knob per rung, two shared
curves) rather than a per-rung bespoke formula, which matters for the same
reason the classification pipeline avoids per-rung decision-boundary tables
(T-1150 §6 precedent Araminta's ② question cites). I'm not committing to
exact `BASE_TTL` numbers here — that's a tuning pass once the wire contract
and step count are settled in round 2, not a round-1 architecture call.
**What I'm explicitly NOT proposing:** a disk-backed persistent tier for
anything finer than the orbital/global rung. Every finer-rung canvas is cheap
enough to re-derive (my own flat ~200 ns/cell parallel number holds at every
size and spacing I tested) that a disk tier for them would be optimizing a
cost that isn't the bottleneck — the bottleneck at finer rungs is wire size
(④) and step-cross latency (Stig's ⑤: sub-5ms upload, not a texture-upload
problem either), not derive cost. A disk tier for deep rungs would add
complexity (self-cleaning sweep, disk I/O latency on a cache miss) to solve a
problem the measurements say doesn't exist. This is my answer to Stig's
question 4's first candidate shape ((i) server-side SQLite cache DB) as it
applies to the *global* tier specifically: I think a simple in-process
resource (the D-203-shaped extension above) is sufficient for the orbital
rung's ~174 MB budget without reaching for SQLite at all — SQLite would only
earn its keep if the budget were large enough to need database-grade
eviction bookkeeping, and 273 rows at one field each doesn't need that. If
Stig's cross-boundary analysis (server-primary vs. client-primary reads) finds
a reason SQLite specifically is preferable for other reasons (client-facing
query shape, cross-session persistence semantics), I'd defer to that — my
claim is narrower: the *cost* doesn't force SQLite, a plain resource would
work fine at this size.
## Summary for round 2 / Jeroen's interview
1. **Hydrology is settled-viable, cheap, and mostly non-carving.** Ship the
T-1177 prototype's algorithm as-is; solve once per body (like
`drainage::analyze` today), hold in the D-203-shaped cache, never re-solve
per step or per request.
2. **Cliff representation is `channel_depth` + `cliff_edge` alongside
unchanged `elevation`** — cheap to carry given how rare carving measures
as being; Phase-4-vs-Phase-5 scope call is Tyre/Araminta's, not mine to
settle alone.
3. **Viewport-sized canvases at every step, always — no canonical fixed
canvas past the orbital/region rung.** This is the only policy that keeps
the deep ladder inside D-226(d); cost was never the constraint here,
governance is.
4. **The row-chunked par_iter mechanism already in `layer_proxy.rs` is the
answer to compute-chunk partitioning** — it holds flat from 330K to 8.3M
cells at every spacing, at three independent cross-validations. Wire
courses into it from the start; don't let the courses-empty replica-loop
measurement become the shipped assumption.
5. **Cache tiers: one resident/disk global tier at the orbital rung
(~174 MB PNG-encoded across all bodies, D-203-shaped resource extension),
TTL(detail, time, distance) for everything finer, no disk tier below
orbital.** The wire carrier (④) and the canonical-vs-viewport boundary,
not derive cost, are what gate the deep steps — cost is a solved problem
at every rung I measured.
None of the four things I was gated on turned into a "computer catches fire"
result. The genuinely open items for round 2 are Araminta's wire contract
(the tagged-envelope call is now unavoidable per ④, I agree with that
reading), Tyre's gridunit↔D-243 snap-or-float ruling, and the Phase-4-vs-
Phase-5 cliff scope call — none of which are cost questions my measurements
can resolve alone.
@@ -0,0 +1,768 @@
---
title: "Body Map Viewer — Dudley Round 2"
description: "Envelope mechanics (wire/serving design), seed-chaining ruling, step-ladder tables, and final cache-tier spec incorporating Jeroen's storage-eviction amendment"
workshop: body-map-viewer
round: 2
owner: Dudley
status: complete
decision_refs: [D-166, D-225, D-226, D-227, D-243, D-203]
---
# Dudley — Round 2 Position
Read `lead-interview-1.md` and all three other round-1 documents first. Four
items in the coordinator's split: envelope mechanics (wire/serving half —
Tyre owns the D-record text), the seed-chaining ruling (closing round-1 open
item 8), step-ladder tables (with Tyre — producing the tables Jeroen rules
from at interview 2), and the final cache-tier spec incorporating Jeroen's
storage-eviction amendment.
**Coordination note:** I first attempted to reach Tyre directly (SendMessage)
to split envelope-mechanics drafting live and he wasn't addressable yet, so I
designed the wire/serving half unilaterally, grounded directly in the actual
`decode_inbound`/`Inbound` code (`server/src/bridge/mod.rs`). Tyre reached me
mid-draft with his own three concrete questions (message shape, legacy
coexistence, step-0 carrier) plus his snap-rule constraints and three
candidate step-ladder skeletons (Options A/B/C, all D-243-disciplined,
correcting an error in my own first pass — see §(c)). This document is
revised to answer his questions directly and replaces my original two tables
with cost fills against his skeletons. Araminta also relayed her dense/sparse
envelope-framing question (via the coordinator) — answered at the end of
§(a). All three of us are converging live; this is the reconciled version.
---
## (a) Envelope mechanics — wire/serving design
### The constraint this has to satisfy, stated precisely
I read the actual code before designing this, not just the doc summaries.
`server/src/bridge/mod.rs`'s `decode_inbound` demuxes five inbound map/array
shapes today (`Inputs`, `AtlasRequest`, `StarMapRequest`, `CityNamesRequest`,
`BrowseRequest`), and the module's own doc comment (lines 67-75) already
names this exact moment:
> "The next genuinely NEW inbound shape (a sixth) must migrate the channel to
> the tagged-envelope framing D-225 deferred — do not add a sixth probe."
A step-canvas request is unambiguously a sixth new shape. So this isn't a
judgment call about *whether* to migrate — the ceiling was pre-declared and
this is the shape that trips it. What I'm designing is the concrete *how*.
### What "tagged envelope" already means in this codebase — don't invent a new mechanism
Reading `Inbound`'s existing three newer variants (`StarMapRequest`,
`CityNamesRequest`, `BrowseRequest`), the "tag" D-225's 2026-06-12 amendment
asked for is **already partially implemented**, just not generalized: each of
those three structs carries a mandatory boolean discriminator field the
others don't have at all (`star_map: bool`, `city_names: bool`, `browse:
bool`), and `decode_inbound`'s `ShapeProbe` defensively checks that at most
one discriminator is present in any given frame, rejecting ambiguous unions
outright (PR #176 review H1). This is a **tagged envelope in substance** — a
required marker field distinguishing shapes — it just hasn't needed a shared
name or a unified probe struct because five shapes fit in one hand-rolled
`ShapeProbe`.
**My recommendation: extend this exact pattern for the sixth shape, don't
invent a parallel envelope format.**
```rust
/// A step-canvas data-canvas request (body-map-viewer workshop, T-1176
/// follow-on). Discriminator field `step_canvas: true` — same tagged-marker
/// pattern as `star_map`/`city_names`/`browse`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepCanvasRequest {
pub step_canvas: bool, // mandatory discriminator, always true when present
pub body_id: String,
pub step_index: u32, // the discrete step (§(c) below), never a raw spacing float
pub center: (i64, i64), // world-metres, snapped to the step's D-243 rung grid (Tyre's snap ruling)
pub extent: (u32, u32), // canvas px budget, e.g. 3840x2160 — fixed budget, not literal viewport echo
pub min_wl_m: u32, // octave cutoff, quantized band (existing window_min_wl_m precedent)
}
```
Add `StepCanvasRequest(StepCanvasRequest)` as a sixth `Inbound` variant,
extend `ShapeProbe` with a `step_canvas: Option<IgnoredAny>` field, and add it
to the mutual-exclusivity sum and the try-order chain in `decode_inbound`.
This is a small, mechanical, additive change — the demux *mechanism* doesn't
need a redesign, because the five-shape ceiling was never about the
mechanism breaking, it was a **discipline marker** ("stop adding shapes this
way past five, go do the real thing"). The "real thing" it points at, once I
traced the actual code, turns out to be exactly the pattern already in place
— which is good news, not a corner cut: it means the migration is
low-risk, not a rewrite.
**Response side is the actual new shape, and it does need its own message —
not a field.** `AtlasLayerResponse`'s own doc (`layer_proxy.rs:605-631`) is
explicit that a windowed-family second field is "a dedicated response
message by rule, not a second `Option`" (D-226 T-1124 §2). A step-canvas
response is emphatically that case — Tyre's amendment text already rules
this (§1d of his round-1 doc). Concretely:
```rust
/// Response to a StepCanvasRequest. Deliberately NOT a field on
/// AtlasLayerResponse (D-226 T-1124 §2's windowed-family ceiling names this
/// exact case) — a wholly separate response type outside that family.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepCanvasResponse {
pub body_id: String,
pub step_index: u32,
pub center: (i64, i64), // echoed, same staleness-guard pattern as district_window
pub status: StepCanvasStatus, // Ready | Pending | Error(String) — mirrors the existing three-way status enums
pub canvas: Option<EncodedStepCanvas>,
}
/// One dense field, PNG-per-field encoded (T-1179's measured winner).
pub struct EncodedField {
pub png_bytes: Vec<u8>,
}
pub struct EncodedStepCanvas {
pub width: u32,
pub height: u32,
pub morphology: EncodedField,
pub elev_q: EncodedField,
pub temp_dc: EncodedField,
pub moisture_q: EncodedField,
pub vegetation: EncodedField,
pub glaciation: EncodedField,
pub settlement_id: EncodedField, // Araminta's new dense field
pub courses: Vec<RiverCourse>, // sparse, MessagePack-native (unchanged shape)
pub cliffs: Vec<CliffSegment>, // sparse, MessagePack-native — see §(a) note on the Dudley/Araminta tension below
}
```
`SimBridge` gains `fn send_step_canvas_response(&self, resp:
&StepCanvasResponse) -> Result<(), BridgeError>`, parallel to the existing
five `send_*` methods — same pattern `TcpBridge` already implements for
`send_atlas_response`/`send_star_map_response`/etc. This is the entire
"envelope migration" in mechanical terms: **one more tagged inbound variant,
one more dedicated outbound method, both following patterns already proven
five times over in this exact file.** I want this stated plainly because
Tyre's round-1 text (correctly) frames the migration as "expected scope,
challenging but doable" at the governance level — at the *code* level, once
you actually read `bridge/mod.rs`, it's closer to "doable" than
"challenging." The challenge is elsewhere (the client-side rebuild Stig owns,
the cache-tier and step-ladder work below), not in extending this demux.
**Note on the cliff-field dense-vs-sparse tension (round-1 conflict #1,
`round-1-notes.md` §3):** I'm adopting Araminta's sparse `Vec<CliffSegment>`
shape in the struct above, not my round-1 text's implicit dense-array
framing. Reading her argument again against my own hydrology numbers, she's
right and I was underspecified, not actually disagreeing: I only ever
proposed the *field set* (`channel_depth` + `cliff_edge`), never committed to
dense-array-shape explicitly, and given the measured rarity (zero carved
cells at every production-scale bench I ran), a mostly-empty `Vec` costs
strictly less than a mostly-zero dense array under every encoding in T-1179's
table — sparse wins outright here, no tradeoff to weigh. Tyre's "new arrays"
language in his synthesis was, I think, imprecise rather than a third
position — resolving as: **`cliffs: Vec<CliffSegment>` parallel to
`courses`, both sparse, both MessagePack-native, neither PNG-encoded.**
### Legacy `district_window` / `AtlasLayerRequest` coexistence during migration
**No retirement message, no deprecation flag, no dual-write.** The legacy
windowed carrier keeps working completely unchanged — same struct, same
`serve_district_window`, same cache key, same wire bytes — for as long as the
*current* Atlas viewer (`AtlasWindowViewer`, the thing Stig's round-1 doc
describes retiring client-side) keeps requesting it. The server has no
concept of "the old viewer" — it just answers whichever request shape
arrives. Once Stig's stepped viewer ships and the old `AtlasWindowViewer`
code is deleted client-side, the server simply stops receiving
`AtlasLayerRequest.window_center`-populated requests, and `district_window`
quietly goes cold — no server-side cleanup ticket needed, per D-005/D-192's
co-ship guarantee (client and server ship together, so there's never a
window where an old client talks to a new server or vice versa). This is the
same "eviction → recompute, always valid" property D-227 already gives every
cache entry; an unrequested carrier isn't a liability, it's just unused code
someone can delete in a follow-up cleanup ticket once the cutover is
confirmed complete, not a coexistence *problem* requiring design work now.
**One thing worth flagging for the ticket plan (not a design gap, a
sequencing note):** the migration should ship the new `StepCanvasRequest`
path fully working, verified via the live Gauntlet pipeline (project
convention), *before* the client cuts over — i.e. the server-side sixth-shape
addition and the client-side viewer rewrite don't have to land in the same
PR, since the old carrier keeps serving the old client throughout. This
lowers implementation risk (the migration doesn't need a flag-day cutover)
and is worth Tyre/the lead naming explicitly in the ticket plan.
### Direct answers to Tyre's three questions
**1. Message shape: discriminator field on the existing stream, not a new
wrapper/framing layer.** Concretely, `step_canvas: bool` on a new
`StepCanvasRequest` struct, extending the exact `ShapeProbe` pattern that
already governs `star_map`/`city_names`/`browse` (`server/src/bridge/mod.rs`,
read directly — see above). This *is* "a required marker field," which is
literally the alternative-satisfying language D-225's 2026-06-12 amendment
used ("a tagged envelope (or a required marker field)") — I'd steer away from
inventing a wrapper/framing layer on top of the existing stream, because the
stream itself doesn't need restructuring, only one more disambiguated shape.
Same answer for the response side: a new `StepCanvasResponse` type +
`SimBridge::send_step_canvas_response`, not a wrapped/multiplexed response
envelope. My reasoning for this over a more general `envelope_version`/
`msg_type` integer tag: zero new demux machinery, proven five times already,
and I don't have a seventh shape waiting that would justify generalizing the
mechanism now — if you want to formally name a reusable envelope *type* in
the record text for whatever comes after this one, that's a framing choice
the record can state independent of what actually ships in code (the code
only needs one more discriminated variant either way).
**2. Legacy coexistence: `district_window` survives unchanged, and I'd let
the envelope subsume only the NEW step-canvas traffic, not migrate existing
windowed traffic onto it.** Reasoning: `district_window`'s current consumer
(the existing `AtlasWindowViewer`) isn't being redesigned by this workshop —
it's being *replaced* by Stig's new stepped viewer. There's no reason to
migrate the old carrier's wire shape when the old carrier's only client is
also being deleted; migrating it would be extra engineering (touch the
legacy struct, the legacy cache key, the legacy client decode path) for a
code path with a defined end-of-life, not an extended future. Your lean
matches mine exactly — keep `district_window` alive, byte-for-byte, for
whatever old-viewer traffic exists until the client cutover, then it goes
cold and gets deleted in a follow-up cleanup ticket. One carrier "cleaner in
the abstract" isn't worth the extra migration surface for a carrier that's
already scheduled for deletion.
**3. Step-0/Region rides the NEW envelope, not the legacy whole-body
path.** Now that you and Araminta have confirmed Region = step-0, it's
generated by the *same* mechanism every other step uses (a `StepCanvasRequest`
at `step_index: 0`, D-243 rung = Region, viewport-sized per the capped-tile
mosaic shape T-1143 §4 already established) — not a special case riding
`district_grid`/`region_grid`'s existing whole-body `Option` fields on
`AtlasLayerResponse`. Putting step-0 on the legacy whole-body family would
mean the new stepped client has to speak two different response protocols
depending on which step it's on (the new envelope for steps 1-5, the old
family for step 0) for no architectural gain — one request shape, one
response shape, across the whole ladder is simpler for both sides and is
what "step boundaries = compute-chunk boundaries" (premise 8) already implies
uniformly. The existing `region_grid`/whole-body layers keep serving whatever
non-stepped consumers still want them (if any survive the cutover) — they're
just not what step-0 of the new ladder uses.
### Araminta's dense/sparse framing question — answered
Relayed via the coordinator: does the tagged envelope frame the step-canvas
response as (a) one flat tagged message with N typed fields (her default,
today's `DistrictWindowLayer` shape scaled up), or (b) dense/sparse split
into separately-tagged sub-messages enabling partial/progressive delivery?
**Answer: (a), one flat message — I don't think (b) is forced by anything in
my design, and I'd argue against it for this pass specifically.** My
`StepCanvasResponse` struct above is exactly her default assumption: one
`Ready | Pending | Error` response carrying all eight fields (six dense +
`courses` + `cliffs`) together, matching the whole-payload-together
precedent D-225 already established for `Layer1Output` ("the layers are
produced together in one drainage pass... per-layer requests save no compute
and only add round-trips") and the countervailing case (b) would trade
against isn't free:
- **Cost argument against (b):** every field in the response comes off the
*same* row-chunked derive pass (`build_district_window_layer`'s loop
computes all six dense fields per cell in one traversal, per my own
measured numbers) — splitting delivery into sub-messages doesn't save any
server-side compute, it only adds round-trips and partial-state handling
on both ends for a payload that's already fully computed by the time the
*first* sub-message could go out. This is the identical argument D-225 used
against per-layer whole-body requests, and it applies with the same force
here.
- **The progressive-paint UX case is real but belongs one layer up, not
in the wire protocol.** If "classification planes first, courses/cliffs
after" is worth doing for perceived latency, it's cheap to get without
splitting the *server response* — the client can paint the terrain RTT
layer as soon as the (whole) response decodes and defer building the
annotation layer's draw calls by a frame or two, since courses/cliffs are
a tiny fraction of total decode time (sparse lists, a few KB, against a
multi-hundred-KB-to-tens-of-MB dense payload per Table B below). That's a
client-side rendering-order choice Stig can make freely without any
server-side protocol complexity — no reason to pay demux/partial-delivery
cost on the wire for a UX win available for free on the receiving end.
- **Where (b) would earn its keep and doesn't yet:** if a future
measurement showed the dense-field derive pass and the sparse-list
derive pass (courses/cliffs) were on meaningfully different cost/latency
timelines — e.g. if course invention turned out to be slow enough to want
to ship the terrain first and stream courses in later — that would be a
real argument for (b). Nothing measured this workshop shows that: courses
add "+0.090.21 ms against a ~5 ms baseline" per my round-1 citation of
`bench_course_cost_on_vs_off` — negligible, not staggered. I'd revisit
this if a future profiling pass finds a real timeline split, but I'm not
designing speculative complexity against a cost gap that isn't there.
**Net: her default holds. One flat tagged `StepCanvasResponse`, all fields
together, matching D-225's whole-payload precedent — she can write the
client-facing field spec against shape (a) without qualification from my
side.**
---
## (b) Seed-chaining ruling — independent re-derivation
**Ruling: each step re-derives independently from `(seed, position)`. A
step's output is never consumed as literal input by a finer step's
derivation.** This closes round-1 open item 8, and it's not a new
architectural call — it's D-227 applied to a question I should have answered
more explicitly in round 1 instead of flagging as unmeasured.
### The argument
**D-227 purity is the whole case.** `subtile(x,y,z) = derive(seed, atlas,
position)` — every derived value is a pure function of the fixed inputs, not
of some other derived value's cached output. If a finer step consumed a
coarser step's *computed* array as an input (rather than re-deriving from the
same `(seed, position)` the coarser step also started from), the finer step's
correctness would depend on the coarser step having been computed first, with
a specific set of parameters, and cached — which means:
1. **A cache-dependency chain**, exactly what D-227's "evictable, cache is a
bonus, never truth" discipline forbids. If the coarser step's cache entry
is evicted (my own TTL policy in §(d) below deliberately evicts sub-global
geometry), a finer step that depends on it as literal input either fails
to derive or must first regenerate the coarser step — silently
reintroducing exactly the "step boundaries as compute-chunk boundaries"
complexity premise 8 was trying to keep simple, plus a correctness risk
if the regenerated coarser step doesn't byte-match what was evicted (it
would, since both are pure functions of the same seed — but now *proving*
that becomes load-bearing instead of automatic).
2. **A determinism ordering hazard.** Two clients requesting the same finer
step from a cold cache, in different orders relative to any coarser-step
requests, would need to agree on exactly which coarser-step values fed
the derivation — this is solvable, but it's solving a problem that
doesn't exist if every step independently calls `derive_at_metres(seed,
position, this_step's_own_min_wl_m)`.
3. **It contradicts my own measured cost model.** All of T-1178/T-1154's
numbers (the flat ~190220 ns/cell parallel rate at every rung) are for
independent per-cell derivation calls — `derive_window_cell` never reads a
neighboring cell's *derived* value, let alone a different rung's. If a
finer step needed the coarser step's actual array as input, that's a
different, unmeasured computational shape (a dependency graph between
rasters, not a row-chunked independent-cell map) — my GO verdicts at
every rung do not cover that shape.
**What Jeroen's outline phrase actually means, read against the code that
exists today.** "This at the same time serves as seed information for the
deeper cascade" — I read this as describing the existing, already-shipped
pattern in `district_profile.rs`/`detail_scatter.rs`: a district's
`invent_primitives` call reads the *region baseline* (a coarser, independent
derivation) as one of several deterministic inputs alongside the fixed seed
and position, the same way `voxel_relief` already contributes to
`elev_q`/`slope_q` at Block spacing today (confirmed in my own T-1154
measurement doc). That's not "consume the coarser step's computed array
element-for-element as this step's starting point" — it's "the coarser
rung's own *independently-derivable* baseline value at this position is one
of several `(seed, position, ...)`-keyed inputs to the finer rung's own
independent derivation call." Both are pure functions; the finer one just
happens to call the coarser one's derivation function internally as a
sub-computation, not read its *cached* output. This distinction is exactly
what keeps D-227 intact: `derive_district(seed, pos)` calling
`region_baseline_at_district(seed, pos)` internally is still one pure
function of `(seed, pos)` — no cache dependency, no ordering hazard, because
the "coarser step" being referenced is re-executed as code, not looked up as
data.
**Concretely, the ruling is: "consuming the coarser output" already happens,
and it happens the *only* way D-227 permits — as a nested pure-function
call, evaluated fresh every time, never as a read from the coarser step's
cache entry.** This is not new work; the region-baseline-feeds-district
pattern is the precedent, and step-canvas generation should follow it
exactly: a Tile-step's derive call may internally call the same
`region_baseline_at_metres`/`derive_at_metres`-family functions a Region-step
call would, at the same `(seed, position)`, but it never reads
`StepCanvasResponse` bytes from a prior request as an input.
### What this settles and what it doesn't
- **Settles:** my own cost numbers (measurement ②/③) are validated as the
correct model for step-canvas generation — no re-measurement needed,
because "independent re-derivation, coarser rung called as a nested
function" is exactly the shape those benchmarks already exercise
(`derive_at_metres` calling into the same octave-primitive functions at
every rung).
- **Doesn't settle:** whether every finer step's derivation *should* call
every coarser rung's function as a sub-computation (a "does Tile-rung
derivation call Block-rung, District-rung, AND Region-rung functions in
sequence, or just the immediately-coarser one?" question) — that's an
algorithm-composition detail for whoever implements `derive_at_metres`'s
Tile/Block extension, not an architecture question this ruling needs to
answer. The architecture-level answer is just: however many coarser
functions get called, they're called fresh, never read from a response
cache.
---
## (c) Step-ladder tables — with Tyre
**Correction to my own first pass, before the tables:** my original draft of
this section used a "Tile-adjacent (4 m)" spacing that is **not a D-243
rung** — I conflated a T-1154 benchmark data point (which measured 4 m only
as a probe value, not a named ladder level) with a real step. Tyre's message
caught this by construction — his three skeletons are built strictly from
D-243's actual six rungs (voxel 1 m, chunk 64 m, block 128 m, quarter 512 m,
district 2,048 m, region 204.8 km) and nothing else, which is the right
discipline and the one I should have applied the first time. The tables
below use only real D-243 rungs, per his skeletons, with my cost fills.
**Measurement ⑥ status:** not received — checked
`docs/workshops/body-map-viewer/measurements/`, still only ①–⑤ present as of
this document. Cost fills below are ①–④ only; ⑥ affects Stig's shader-vs-CPU
call, not these tables.
**Canvas-extent convention for every row below:** the fixed 3840×2160 px
budget you proposed, viewport-sized (never canonical past step 0) — using
your suggested convention directly, not a different one.
### Cost fills against your three skeletons
**Option A — one-rung-per-step (6 steps): Region → District → Quarter →
Block → chunk (64 m) → voxel (1 m).**
| Step | D-243 rung | Spacing | Step factor (from prev) | Viewport extent @ 3840×2160 canvas | Cells | Derive cost (parallel) | Wire cost (PNG-per-field) |
|---|---|---:|---:|---|---:|---:|---:|
| 0 | Region | 204.8 km | — | whole body (capped-tile mosaic, T-1143 §4) | 8,294,400 | 1,827 ms (measured) | 16.88 MB (measured) |
| 1 | District | 2,048 m | 100× | 7,864 × 4,424 km | 8,294,400 | 1,827 ms (measured) | 16.88 MB (measured) |
| 2 | Quarter | 512 m | 4× | 1,966 × 1,106 km | 8,294,400 | ~1,827 ms (same band, T-1154 confirms Quarter costs the same per-cell rate as District) | ~16.88 MB |
| 3 | Block | 128 m | 4× | 491 × 276 km | 8,294,400 | ~1,827 ms (measured directly at Block spacing, T-1154: same ~190220 ns/cell parallel band) | ~16.88 MB |
| 4 | chunk | 64 m | 2× | 246 × 138 km | 8,294,400 | **UNMEASURED — flagging honestly, not filling with a guess.** T-1154 tested Block (128 m), Tile-adjacent (4 m — an ad hoc probe, not this rung), and Tile (1 m). 64 m spacing was never benched directly. The cutoff-mechanism finding (T-1154: `VOXEL_OCTAVE_WAVELENGTHS_M` bottoms at 128 m, so nothing truncates below Block) strongly implies chunk costs the *same* flat ~1.8 µs/cell single-thread / ~200 ns/cell parallel rate every other rung in this band does — but "strongly implies" is not "measured," and I'm not reporting a number I didn't run. | Same caveat — implied ~16.88 MB by the flat-rate pattern, not measured. |
| 5 (deepest) | voxel | 1 m | 64× | **8.3M cells at 1 m spacing = 3.84 × 2.16 km — this is the wrong deep-step shape**, see the general note below | 8,294,400 | same caveat as row 4 pattern, ~1,827 ms if it held | ~16.88 MB |
**Option A's real problem isn't the missing chunk measurement — it's that a
fixed 3840×2160 canvas at voxel (1 m) spacing is oversized for what a real
viewport needs at 10 px/tile** (3840 px ÷ 10 px/tile = 384 m — my own T-1154
"realistic deep-step canvas" used exactly this reasoning to arrive at
216×384 m, 82,944 cells, not an 8.3M-cell canvas). Applying your fixed-canvas
convention literally at every step breaks down at the deepest step
specifically because the fixed-px-budget convention and the display-ratio
target (10 px/tile) can't both hold at 1 m spacing simultaneously — this
isn't a flaw in your skeleton, it's a flaw in "same canvas budget at every
step" as a blanket rule, which is a genuinely useful thing for these tables
to expose. I'd apply the fixed-3840×2160 convention at every step **except**
the deepest, where the canvas must instead be sized to the display-ratio
contract (my round-1 T-1154 number: 216×384 m, 17 ms, ~486 KB) — noted
consistently in all three tables below, not just this one, since it's a
convention-level correction, not a skeleton-specific one.
**Option B — skip-chunk (5 steps): Region → District → Quarter → Block →
voxel (1 m).**
| Step | D-243 rung | Spacing | Step factor | Viewport extent @ 3840×2160 (except deepest) | Cells | Derive cost (parallel) | Wire cost |
|---|---|---:|---:|---|---:|---:|---:|
| 0 | Region | 204.8 km | — | whole body | 8,294,400 | 1,827 ms (measured) | 16.88 MB (measured) |
| 1 | District | 2,048 m | 100× | 7,864 × 4,424 km | 8,294,400 | 1,827 ms (measured) | 16.88 MB (measured) |
| 2 | Quarter | 512 m | 4× | 1,966 × 1,106 km | 8,294,400 | ~1,827 ms (T-1154 same-band confirmation) | ~16.88 MB |
| 3 | Block | 128 m | 4× | 491 × 276 km | 8,294,400 | ~1,827 ms (measured directly, T-1154) | ~16.88 MB |
| 4 (deepest) | voxel | 1 m | **128×** | 216 × 384 m (display-ratio-sized, per the correction above, NOT the fixed-canvas convention) | 82,944 | **17 ms (measured directly, T-1154)** | **~486 KB (measured rate, T-1179)** |
**This is the skeleton I'd recommend, and the missing-chunk-measurement
problem is exactly why: every single row in Option B is either directly
measured or in the same measured cost band as a directly-measured rung —
there is no row I have to caveat as unmeasured.** That's not a coincidence
of which skeleton I like; it's a direct consequence of chunk (64 m) never
having been benched, and Option B is the one skeleton among your three that
doesn't need it. If chunk's cost does turn out to match the flat-rate pattern
(likely, per the cutoff-mechanism reasoning above), Option A costs the same
as Option B row-for-row at every *other* rung anyway — the only real
difference is Option A has six fetches per full zoom-out-to-in traversal
where Option B has five, and pays one unmeasured/probably-redundant rung to
get there, since chunk sits between Block and voxel with a huge factor
either way (128× Block→voxel in Option A's own 64m→1m final leg, vs 128×
Block→voxel directly in Option B — the chunk step doesn't actually *reduce*
the biggest jump in the ladder, D-243 names chunk as "stream/derive unit,"
not a natural *display* rung, which is exactly Tyre's own hesitation about
it in his message).
**Option C — coarse-doubled (4 steps): Region → Quarter → Block → voxel,
skipping District.**
| Step | D-243 rung | Spacing | Step factor | Viewport extent | Cells | Derive cost | Wire cost |
|---|---|---:|---:|---|---:|---:|---:|
| 0 | Region | 204.8 km | — | whole body | 8,294,400 | 1,827 ms (measured) | 16.88 MB (measured) |
| 1 | Quarter | 512 m | **400×** | 1,966 × 1,106 km | 8,294,400 | ~1,827 ms (same band) | ~16.88 MB |
| 2 | Block | 128 m | 4× | 491 × 276 km | 8,294,400 | ~1,827 ms (measured) | ~16.88 MB |
| 3 (deepest) | voxel | 1 m | 128× | 216 × 384 m (display-ratio-sized) | 82,944 | 17 ms (measured) | ~486 KB (measured) |
**Cost-wise, Option C is not cheaper than B — the per-step cost numbers are
identical to B's (same rungs at the same measured rate), it just skips
District.** So the choice between B and C is **not a cost question at
all** — every row I can cost is the same regardless of which skeleton wins.
It's purely the UX-pacing question your message already correctly separated
out (my answer to your question 2 below).
### Answering your three direct questions
**1. Which skeleton do the costs favor?** None of them are uncomfortable —
every derive cost across all three options is comfortably interactive (17 ms
to 1.8 s, the same band my round-1 doc already established as safe against
any step-cross tolerance). **The costs don't pick a winner; they only
disqualify chunk (64 m) from being *load-bearing* for the decision**, because
it's the one rung nobody has actually measured. That makes Option B (which
never needs chunk) the *cleanest* recommendation on evidentiary grounds, not
because A or C are unaffordable — I'd frame this to Jeroen as "B is the
option with zero asterisks," not "A and C are too expensive."
**2. Does the D-243 factor unevenness (100×, 4×, 4×, 128×) matter for
serving/precache, or is it purely UX pacing?** Both, but asymmetrically —
**it matters for serving in exactly one place (the Region→District 100× jump
at step 0→1), and is purely UX pacing everywhere else.** Reasoning: my cache
tiers (§(d) below) already treat Region as a categorically different tier
(keep-always, global) from every step below it (storage-evictable) — the
100× factor at the top of the ladder lines up with a real architectural
seam that already exists in the cache design, not just a display jump. The
4×/4×/128× factors *within* the sub-global tier don't correspond to any
serving-side seam — District, Quarter, and Block are all "sub-global
geometry," evicted by the same storage-TTL mechanism, served by the same
row-chunked derive path, at the same flat per-cell cost. So: the top jump is
architecturally real (it's the global/sub-global cache boundary already
established for other reasons); every jump below it is exactly what you
said — a UX-pacing question about how gradual the zoom feels, with no
serving-side consequence I can find. This is useful to tell Jeroen directly:
he can pick B vs C on feel alone for the lower rungs without worrying he's
picking a cost or caching regression.
**3. Viewport metre-extent assumption per step?** Confirmed: your fixed
3840×2160 canvas budget, with the one correction above (the deepest step
must use the display-ratio-sized canvas — 216×384 m at 10 px/tile — not the
fixed-px-budget convention, because the two conventions are mutually
incompatible exactly at voxel spacing, per Option A's row 5 above). I'd
apply "fixed 3840×2160 px budget" as the rule for every step except the
last, and "sized to the display-ratio contract" as the rule for the last
step specifically — both tables B and C already reflect this split.
### Confirmation from Stig's measurement ⑥ (relayed, landed after the tables above were drafted)
Stig's `Image.set_pixel` colorize cost measurement (77.5 ns/cell flat across
330K8.3M cells, 25.7 ms at 330K, beating `PackedByteArray`-direct ~2×) closes
his round-1 open item and independently confirms c1 as CPU-first — not
something my tables need to change for, but his own px-band recommendation
(1×1 at Block/Tile "full fidelity," 1×1 preferred at Quarter/District as long
as the realistic canvas stays under ~2M cells, ~5×5 fallback reserved for
shallow/orbital steps where canvas *extent* is what grows, not density) is
worth stating explicitly against my tables because **it's the same policy my
cost fills already assume, from a different, client-side cost driver.**
Every row in Options A/B/C above already uses 1 gridunit-per-pixel (cell
count = canvas px count) at every step from Region through Block — I never
invoked the ~5×5 fallback anywhere in my server-side cost fills, because
nothing in my derive-cost numbers forced it (the flat ~190220 ns/cell
parallel rate holds at every canvas size I measured, so there was no
server-side reason to sample coarser-than-1:1 at any step). Stig's finding
adds the client-side half of that same argument: colorize cost is
**cell-count-driven, not display-density-driven**, so downsampling to 5×5 at
a step that doesn't need it wouldn't even save client-side coloring time
proportionally — the cost is paid per gridunit regardless of how many screen
pixels each one covers. **Two independent cost models (server derive, client
colorize) land on the same policy: 1×1 wherever the resulting cell count is
affordable (every step through Block, per both his ~2M-cell comfort line and
my own flat-rate measurements), reserving the coarser ratio only for the one
place canvas *extent* — not density — is actually the pressure (the
whole-body Region/orbital step, T-1143 §4's capped-tile mosaic).** This
confirms, not revises, every table above; I'm noting it because two
independently-measured cost models agreeing on a policy neither one was
designed to argue for is exactly the kind of convergence worth flagging to
Jeroen rather than leaving as a coincidence buried in two separate documents.
---
## (d) Cache tiers — final spec incorporating Jeroen's storage-eviction amendment
Jeroen's amendment (verbatim, `lead-interview-1.md` ruling 2): *"we still may
also want to evict non global level geometry based on time to save storage
for planets the player visits but never goes back to."* Captured there as:
**staleness-eviction and storage-eviction are distinct axes** — geometry
never goes stale (re-derivable, byte-identical forever), but sub-global
geometry still gets evicted on time-since-last-visit as a storage-budget
policy, independent of whether it's "correct." The global tier alone is
keep-always.
This sharpens, rather than replaces, my round-1 TTL proposal — I had
conflated "TTL" as a single staleness-driven mechanism; Jeroen's ruling
correctly separates it into two policies with different triggers and
different consequences on a miss.
### Tier 1 — Global (Region/orbital rung): keep-always, my round-1 number stands
**~174 MB PNG-encoded across all ~273 bodies** (recomputed check against
Table B's step-0 numbers above: step 0's capped-tile mosaic shape, per
T-1143 §4, is smaller per-body than a full district-spacing canvas — my
round-1 174 MB used district-spacing as a conservative upper bound, so 174 MB
remains a safe ceiling, not an underestimate, for whatever the actual
step-0/orbital canvas shape lands on).
- **D-203-shaped resource extension** — `BodyWorldState` (or a sibling
resource matching its exact pattern) gains an `orbital_canvas:
Option<EncodedStepCanvas>` field.
- **Never evicted by time-since-last-visit.** This is the one tier Jeroen's
amendment explicitly excludes ("the global tier alone is keep-always") —
"always keep the global level" from the original outline stands unmodified
by the amendment; the amendment only sharpens what happens *below* this
tier.
- **Still an evictable cache in D-227's sense, not stored truth** — a
corrupted or manually-cleared global-tier entry is a recompute (~24 ms
hydrology + the derive cost from Table B row 0), never data loss. "Keep-
always" is a *policy choice about when eviction runs*, not an exemption
from D-227's "cache, never source of truth" discipline.
### Tier 2 — Sub-global geometry (District/Quarter/Block/Tile step canvases): dual-axis eviction
This is where Jeroen's amendment lands. Two independent axes, evaluated
separately, either one sufficient to evict an entry:
**Axis 1 — staleness: NEVER for geometry.** Morphology, elevation, moisture,
vegetation, glaciation, settlement presence, river/cliff geometry — every
field D-227 covers as `derive(seed, position)` — is byte-identical on every
recompute. There is no staleness concept for these fields at all; an entry
that exists is always correct, forever, regardless of age. This part of my
round-1 proposal (the `time_decay`/`distance_decay` multiplicative TTL
formula) was **wrong to frame as staleness** — re-reading Jeroen's ruling, I
was solving "when does this become wrong" for data that is never wrong. What
I actually needed was axis 2.
**Axis 2 — storage-budget eviction: time-since-last-visit, a distinct
mechanism.** A per-entry `last_accessed` timestamp (the exact field D-203's
`BodyWorldState` already carries — I'm reusing the pattern, not inventing a
new one), swept periodically (not per-tick — a coarse background sweep,
matching the "not blocking for user output" premise). An entry whose
`last_accessed` exceeds a per-rung threshold is evicted **for storage
reasons, not correctness reasons** — the distinction matters operationally:
a storage-eviction miss is silently identical in cost to a cold-start miss
(re-derive at Table B's measured cost, 17 ms1.8 s depending on rung), it is
never treated as an error or a "the data might be stale, re-verify"
condition, because there is nothing to verify — it's a pure function, it
recomputes to the same bytes every time.
**Concrete formula, revised from round 1:**
```
evict_if: time_since_last_visit(entry) > STORAGE_TTL[rung]
```
One term, not the three-term multiplicative formula I proposed in round 1 —
Jeroen's amendment removes the need for `distance_decay` and `detail`-scaling
as *separate* factors once staleness is off the table: what's left is purely
"how long has it been since anyone looked at this," which is
time-since-last-visit alone. `STORAGE_TTL[rung]` is still the one per-rung
tunable (deeper rungs plausibly get a shorter floor, since they're both
cheaper to regenerate and cover less ground per entry — same reasoning as my
round-1 doc, just now attached to a storage-thrift rationale rather than a
staleness one), but the mechanism collapses to a single, legible sweep rather
than a compound scoring formula. This is a simpler design than what I
proposed in round 1, which I'll flag plainly: **Jeroen's amendment made my
original proposal simpler, not more complex** — separating the two axes
removed a term instead of adding one, because "distance from current focus"
turns out to be redundant with "time since last visited" for a
storage-thrift purpose (a location far from current focus that the player
*just* visited doesn't need evicting for storage reasons yet; one they
haven't touched in a long session, regardless of current distance, does) —
distance matters for *prioritizing what to precache next*, which is a
different mechanism (smart precache, premise 1) than *what to evict for
storage*, and conflating them in one formula was my round-1 mistake.
**What "player visits a planet, never goes back" concretely evicts under
this policy:** every sub-global step-canvas entry for that body — District
through Tile, every step index, every center the player's viewport ever
requested — ages out independently on its own `last_accessed`. The global
(Region/orbital) entry for that same body does NOT evict (Tier 1's
keep-always rule), so "atlas navigation snappy after first calc" survives
even for an abandoned body — the player can still open the Atlas and see the
planet at global zoom instantly; only the finer-grained "I was standing at
this exact district window" entries age out, which is exactly the storage
being reclaimed (per-step canvases are the bulk of the byte cost — District
alone is 16.88 MB per cached window per Table B, vastly larger than the
174 MB *total* global tier across all 273 bodies).
**Sim-state fields (frozen/flooded) — the map-time-axis TTL, separate from
both axes above, unaffected by this amendment.** Jeroen's lead-interview
ruling on the map time axis (a different ruling from the storage-eviction
amendment, but adjacent) already established: static geometry cached
indefinitely-fresh, frozen/flooded carried as separately-cached short-TTL
planes re-requested as sim time advances. This is a genuine staleness TTL
(the sim state actually can become wrong as game time passes), and it's
orthogonal to the storage-eviction axis above — a frozen/flooded plane has
both a staleness TTL (it goes wrong after some sim-time interval) AND is
subject to the same storage-eviction sweep as its parent geometry entry (if
nobody's visited the body in a long time, evict the whole cached window,
sim-state plane included, for storage reasons — regenerating both on the
next visit is cheap either way).
### Tier 3 — client-side (Stig's, composing not competing, per lead-interview ratification)
Restating only to confirm compatibility, not re-designing Stig's tier:
Stig's `FileAccess` disk cache dir + in-memory LRU (round-1 §4) already
implements the identical two-axis split independently — his "geometry:
LRU-evict-only, no TTL" is my Axis 1 (never stale) plus his own LRU acting as
*his* storage-thrift mechanism (client disk budget, not server RAM budget);
his "sim-state: explicit TTL sweep" is the same map-time-axis TTL described
above. **The two tiers were designed independently and landed on the same
two-axis shape** — worth naming as convergent validation of Jeroen's
amendment being the right cut, not just a server-side patch.
### Summary table
| Tier | Scope | Staleness eviction | Storage eviction | Size |
|---|---|---|---|---|
| 1 — Global | Region/orbital canvas, per body | Never (D-227 pure) | **Never** (Jeroen: keep-always) | ~174 MB, all bodies |
| 2 — Sub-global geometry | District→Tile step canvases | Never (D-227 pure) | **`time_since_last_visit > STORAGE_TTL[rung]`** | unbounded resident, bounded by sweep |
| 2b — Sim-state planes | Frozen/flooded overlays | **Yes** — real staleness TTL (map-time-axis ruling) | Same sweep as parent geometry entry | small, per-window |
| 3 — Client disk/memory | Stig's `FileAccess` + in-memory LRU | Mirrors tier 2/2b split exactly | Client-local LRU + disk budget | ~440 MB ceiling at 5×5, disk-safe |
**Note on the "5×5" figure in the row above:** that's Stig's round-1 citation
of red flag 2's original conservative estimate for the *global* tier
specifically (all 273 bodies' Region/orbital canvases, disk-resident) — it
predates his own measurement ⑥ px-band refinement above and isn't affected by
it, since ⑥'s finding is about *sub-global* steps (Block/Quarter/District)
staying 1×1, not about the global tier's own sizing. My own Tier 1 number
(~174 MB, PNG-encoded) is the tighter, measured figure for the same tier —
both numbers describe the same "keep-always" budget from two different
starting estimates (his conservative pre-④ ceiling vs. my post-④ measured
rate); not a live disagreement, just two vintages of the same calculation
worth reconciling to one number (mine) before this goes to filing.
---
## Summary for interview 2
1. **Envelope mechanics are lower-risk than the governance framing might
suggest** — the tagged-marker pattern the sixth shape needs is already
proven five times in `bridge/mod.rs`; this is one more `Inbound` variant
and one more `SimBridge` method, not a demux redesign. Legacy
`district_window` needs zero coexistence engineering — it just keeps
working until nothing requests it. Step-0/Region rides the same new
envelope as every other step, not the legacy whole-body family. One flat
tagged response (Araminta's default shape (a)), not a dense/sparse split
— the progressive-paint UX case is real but cheaper to get client-side
than by adding wire-protocol complexity for a cost split the measurements
don't show.
2. **Seed-chaining: independent re-derivation, ruled and argued from D-227
purity** — a finer step may call a coarser rung's derivation *function*
internally (the existing region-baseline-feeds-district precedent), but
never reads a coarser step's cached *response* as literal input. My
round-1 cost numbers are validated as the correct model for this shape.
3. **Three step-ladder skeletons costed against Tyre's D-243-disciplined
options; Option B (skip-chunk, 5 steps: Region→District→Quarter→Block→
voxel) recommended** — the only skeleton where every row is directly
measured or in a directly-measured cost band, because chunk (64 m) was
never benched and Option B is the one option that doesn't need it. Costs
don't disqualify Option A or C (nothing is unaffordable), they just can't
fully justify chunk as its own step. The D-243 factor unevenness matters
for serving at exactly one seam (Region→District, which lines up with the
global/sub-global cache boundary) and is pure UX pacing everywhere below
it — Jeroen can pick B vs C on feel alone for the lower rungs. My own
first draft of this section used an invented 4 m "Tile-adjacent" spacing
that isn't a real D-243 rung — corrected once Tyre's message caught it;
the deepest step also needs a display-ratio-sized canvas (216×384 m, not
the fixed-3840×2160 convention that works at every shallower step).
4. **Cache tiers finalized**: Jeroen's amendment simplified my round-1
three-term TTL formula into a cleaner two-axis model — geometry never
goes stale (removed from the eviction question entirely), sub-global
geometry evicts purely on time-since-last-visit for storage thrift, the
global tier alone is keep-always, sim-state planes keep their own
separate real staleness TTL. Stig's independently-designed client tier
landed on the identical two-axis shape, which I read as confirmation the
amendment cut the problem correctly.
Nothing in this round changed a round-1 cost number. What changed is
precision: the cliff wire-shape tension resolves to Araminta's sparse
framing, the seed-chaining question resolves to independent re-derivation
with a named precedent, the cache-tier formula gets simpler (not more
complex) once staleness and storage-thrift stop being conflated, and the
step-ladder section itself got one honest correction (the invented 4 m
spacing) caught by live coordination with Tyre rather than by me catching it
alone — worth naming, since the correction is exactly what round 2's
synthesis format is supposed to produce.
@@ -0,0 +1,58 @@
---
title: "Body Map Viewer — Lead Interview 1 (post round 1)"
description: "Jeroen's rulings on the round-1 open decisions"
type: workshop
status: active
workshop: body-map-viewer
created: 2026-07-25
---
# Lead Interview 1 — Jeroen's rulings (post round 1)
Held after [round-1-notes.md](round-1-notes.md). Presented: the three independent
convergences (tagged envelope triggered; viewport-sized canvases below
orbital/region; T-1177 cliff representation), the composing cache proposals, the
two named tensions, and the 10-item OPEN-FOR-SYNTHESIS list.
## Ratified by silence (presented as settled-unless-objected; no objection)
- **Tagged-envelope migration: triggered** (three seats, measured numbers). Round 2
designs the mechanics.
- **Viewport-sized canvases at every step below orbital/region; canonical survives
only as the global tier.** (This also resolves the brief's canonical-vs-viewport
interview question.)
- **Cliff representation** `elevation` + `channel_depth` + `cliff_edge` (unanimous
among adopters).
- **Cache composition**: Dudley's server-side global tier (~174 MB PNG-encoded,
D-203-shaped resource, not SQLite) + Stig's client-side `FileAccess` cache dir —
compose, don't compete. Both rejected SQLite in every shape independently.
## Rulings
1. **Step ladder — "Round 2 proposes, I rule at interview 2."** The synthesis round
produces 23 concrete ladder tables (step counts, factors, per-step derivation
rung + measured cost); Jeroen picks from real tables, not principles.
2. **Map time axis — current state via TTL-split**, his prior hint made concrete:
static geometry cached indefinitely-fresh (determinism), frozen/flooded carried
as separately-cached short-TTL planes re-requested as sim time advances.
**Amendment (verbatim):** *"we still may also want to evict non global level
geometry based on time to save storage for planets the player visits but never
goes back to."* → Captured as a design rule: **staleness-eviction and
storage-eviction are distinct axes.** Geometry never goes stale (re-derivable,
byte-identical), but sub-global geometry entries still get evicted on
time-since-last-visit as a storage-budget policy. The global tier alone is
keep-always.
3. **Cliffs — sparse `CliffSegment` feature list, Phase-4 scope.** Aligns the
rarity finding, Araminta's encoding logic, and Tyre's scope ruling. Troblum
still stress-tests the rarity basis (one real body + synthetics) in round 2.
4. **Round 2: GO, with measurement ⑥ run now** — Stig runs the missing
`Image.set_pixel` cost measurement (330K/2.07M/8.3M cells, client-side) in
parallel with synthesis so the shader-vs-CPU (c1) call lands with a number.
Troblum joins with the adversarial mandate.
## Carried to round 2 (synthesis work, no ruling needed)
Envelope mechanics (message shape, demux, legacy-carrier coexistence);
seed-chaining data flow (independent re-derivation vs consuming the coarser step's
output — Dudley's costs assume the former); region-as-step-0 confirmation;
label placement de-dup at step seams (Tyre's gray case → Araminta).
@@ -0,0 +1,182 @@
---
title: "Measurement ⑥: GDScript CPU-colorize cost — Image.set_pixel vs PackedByteArray-direct (330K8.3M cells)"
ticket: T-1176 (body-map-viewer workshop, round 2 synthesis)
owner: Stig
workshop: body-map-viewer
status: complete
---
# Measurement ⑥ — CPU-colorize cost (the c1 input)
## What this answers
Round 1 (`stig-round1.md` §3) recommended CPU-side coloring for the terrain
RTT layer as the default, explicitly flagged as **pending** this exact
number, and named it the missing sixth measurement. Jeroen ruled at lead
interview 1: proceed, run ⑥ now, so the c1 shader-vs-CPU call lands with a
number instead of a "known-good fallback" hedge.
This measures the classify-byte → palette-lookup → write-into-`Image` loop —
the CPU-side half of the terrain-layer build, at the same three canvas sizes
(330K / 2.07M / 8.3M gridunits) everything else in the appendix was measured
at — so it can be compared directly against ④'s encode/decode numbers and
①'s derivation numbers on the same size axis.
## Method
**Headless is correct here, unlike ⑤.** ⑤ (ImageTexture upload) required a
real GPU-backed display because Godot's headless dummy renderer fakes GPU
texture uploads — timing that path headless would have measured nothing.
This measurement has **no GPU/rendering-driver dependency at all**:
`Image.set_pixel()` and `PackedByteArray` element writes are ordinary
CPU-side data-structure operations, and `Image.create()`/
`Image.create_from_data()` allocate a plain in-memory buffer — none of it
touches the RenderingServer or a swapchain. Confirmed by inspection of the
actual production code path this mirrors
(`atlas_window_overlay.gd::_rebuild_texture_if_needed`, which itself never
does anything GPU-side until the *separate* `ImageTexture.create_from_image`
call — the exact boundary ⑤ already priced). Run via
`godot --headless --path client --script res://tmp_drive_t_setpixel_c1.gd`
from the main checkout, no `DISPLAY` needed.
- Driver: a temporary `SceneTree` script (`client/tmp_drive_t_setpixel_c1.gd`,
deleted after this measurement — not committed, same convention as ⑤'s
`tmp_drive_t1180.gd`).
- Two candidate implementations, since the delta between them **is** the c1
decision input (per the task instruction):
- **(A) `Image.set_pixel(x, y, Color)` per cell** — the exact shape
`atlas_window_overlay.gd`'s `_rebuild_texture_if_needed()` /
`_build_tile_texture()` use today: read a classification byte from a
per-cell array, look up a `Color` in a palette table, call
`img.set_pixel()`.
- **(B) Direct `PackedByteArray` buffer write + `Image.create_from_data`**
— skip the per-pixel method call and `Color` object construction; look
up a 4-byte RGBA quad and write it directly into a flat buffer at
`(row*width+col)*4`, then hand the whole buffer to
`Image.create_from_data()` once. Same output format
(`Image.FORMAT_RGBA8`) as path A and as today's production code.
- **Realistic colorize loop, not a synthetic fill.** The per-cell
classification array is a 17-zone D-239 §6 morphology-vocabulary spread
(`RandomNumberGenerator`, fixed seed 1234, `randi_range(0, 16)` per cell)
— every class actually gets looked up across the canvas, exercising real
branchy palette-array access rather than a constant/degenerate case that
would flatter either path.
- 12 iterations per case, 2 discarded as warmup, 10 kept. Median, p95, min,
max computed over the 10 kept samples. Timed with `Time.get_ticks_usec()`
immediately around each colorize call (palette-array construction and the
synthetic classification array are built once, outside the timed loop).
- Run twice back-to-back for stability confirmation (below).
## Environment
Same machine as ①–⑤ (16-core, Rayon 14, no GPU involvement in this
particular measurement since it's headless). No background load running
during this measurement.
## Results — run A
| size | path | median_ms | p95_ms | min_ms | max_ms |
|---|---|---:|---:|---:|---:|
| 330K | A_set_pixel | 25.695 | 27.852 | 24.644 | 27.852 |
| 330K | B_byte_buffer | 51.033 | 51.510 | 49.246 | 51.510 |
| 2.07M | A_set_pixel | 165.170 | 174.554 | 158.683 | 174.554 |
| 2.07M | B_byte_buffer | 319.174 | 333.268 | 308.201 | 333.268 |
| 8.3M | A_set_pixel | 643.514 | 655.528 | 634.316 | 655.528 |
| 8.3M | B_byte_buffer | 1261.385 | 1284.790 | 1245.232 | 1284.790 |
## Results — run B (stability re-run)
| size | path | median_ms | p95_ms | min_ms | max_ms |
|---|---|---:|---:|---:|---:|
| 330K | A_set_pixel | 26.186 | 26.892 | 25.238 | 26.892 |
| 330K | B_byte_buffer | 52.566 | 55.523 | 49.407 | 55.523 |
| 2.07M | A_set_pixel | 161.351 | 164.131 | 155.692 | 164.131 |
| 2.07M | B_byte_buffer | 316.721 | 329.357 | 310.894 | 329.357 |
| 8.3M | A_set_pixel | 641.453 | 742.036 | 637.308 | 742.036 |
| 8.3M | B_byte_buffer | 1266.614 | 1291.422 | 1252.194 | 1291.422 |
Run A and run B agree within ~23% on every case (the one outlier, 8.3M
`A_set_pixel` p95 at 742 ms vs 656 ms in run A, is a single high sample in a
10-kept-sample set — the median, the number this doc leads with, moves by
under 0.3ms between runs). Same stability profile as ⑤'s own two-run
confirmation.
## Headline numbers (using run A, run B confirms stability)
| size | path | median | ns/cell |
|---|---|---:|---:|
| 330K (331,776 cells) | `set_pixel` | 25.7 ms | **77.5 ns/cell** |
| 330K | byte-buffer | 51.0 ms | 153.8 ns/cell |
| 2.07M (2,073,600 cells) | `set_pixel` | 165.2 ms | **79.7 ns/cell** |
| 2.07M | byte-buffer | 319.2 ms | 154.0 ns/cell |
| 8.3M (8,294,400 cells) | `set_pixel` | 643.5 ms | **77.6 ns/cell** |
| 8.3M | byte-buffer | 1,261.4 ms | 152.1 ns/cell |
**Per-cell rate is flat across all three sizes for both paths** (~78 ns/cell
for `set_pixel`, ~153 ns/cell for byte-buffer, both within ~3% across the
25× cell-count range from 330K to 8.3M) — no cliff, matching the "flat rate
holds at scale" pattern every other measurement in this appendix (①②③④⑤) also
found. This is a clean, linearly-scaling GDScript-interpreter cost, not an
algorithmic blowup.
**`Image.set_pixel` is ~2× FASTER than the direct `PackedByteArray` write —
the opposite of the naive assumption.** I expected the raw-buffer path to
win by skipping `Color` object construction and the `set_pixel` method-call
overhead; measured, it loses by roughly 2×, consistently at every size. Read
on this: GDScript's own per-element `PackedByteArray` indexed write
(`buf[off] = ...`, four separate indexed writes per cell in path B) carries
enough per-access interpreter overhead of its own that it outweighs whatever
`set_pixel`'s internal `Color`-to-RGBA8-conversion cost is; `set_pixel` is
presumably a single, more-optimized engine-side call per pixel rather than
four separate GDScript-level array-index operations. This is a genuinely
useful finding for implementation: **prefer `Image.set_pixel` over hand-
rolled buffer writes in GDScript** — the "avoid the method-call" instinct
that's often correct in compiled languages does not hold here.
## What this means for the frame/interaction budget
**643 ms at the largest canvas size (8.3M cells) is not free — this is the
first CPU number in the whole appendix that DOES land near a budget that
matters, and it changes the shape of the c1 recommendation.**
Context against the rest of the appendix, same 8.3M-cell canvas:
- Server derivation (②): ~1.71.8 s parallel — the client colorize cost
(0.64 s) is roughly a third of the server's own derive time, not a rounding
error against it.
- Wire decode, PNG-per-field (④): ~91 ms — colorize is **~7× the decode
cost** at this size.
- ImageTexture upload (⑤): ~35 ms — colorize is **~130200× the upload
cost** at this size.
At the 330K size (the workshop's own "acceptable fallback" resolution,
5×5 px/gridunit), `set_pixel` colorize costs **25.7 ms** — comfortably
interactive on its own (single-digit frame budgets, well under any
reasonable "player is waiting for this step" tolerance), and this is the
size that matters most for the deep/mid steps of the ladder per Dudley's
round-1 recommendation (viewport-sized canvases, not the 8.3M "stress
ceiling" shape — his own round-1 doc is explicit that 8.3M is a canvas-size
stress test, not a realistic per-step request shape). **At the sizes the
ladder will actually request in steady-state play, CPU colorize is cheap.**
It only becomes a real cost at the largest canvas sizes this appendix
measured as an upper-bound stress case, which per Dudley's own viewport-
sizing recommendation should rarely if ever be requested as a literal
step-canvas payload.
## Implication for the hold-fetch-swap sequence (§2 of my round-1 doc)
Colorize happens once per arrived step canvas, in the hold-fetch-swap
sequence's "on arrival: decode, colorize, upload, swap" chain. At 330K cells
(the realistic per-step size), colorize (25.7 ms) is now the **second most
expensive step** in that chain after server derivation itself, ahead of both
decode (④: ~3.6 ms at 330K) and upload (⑤: ~0.2 ms at 330K,
create_from_image RGBA8) — worth naming explicitly since round 1 didn't have
this number and could have under-priced the arrival-side cost.
## Repro
```bash
# Driver script was client/tmp_drive_t_setpixel_c1.gd (temporary, deleted
# after this measurement — not in the committed tree). Launch command used:
cd /var/mnt/data/projects/settled-reach
godot --headless --path client --script res://tmp_drive_t_setpixel_c1.gd
```
@@ -417,3 +417,204 @@ time — or only ever paid once per body, offline/precomputed, the same way
architecture question for the workshop's cache-tier synthesis (Dudley's
question 4 / red flag 2), not a solver-cost question this measurement can
settle alone.
---
## POPULATION SURVEY (post-adversarial addendum, 2026-07-23)
**Gap found by Troblum's adversarial pass, round 2:** the "273 bodies in
0.70.8 s" number above (`bench_parallel_273_bodies_at_512x256`) is real and
still correctly answers the question it was built for — but that question is
narrower than it reads at a glance. The bench calls `gj1c_512x256()` **once**
and reuses the same `elev`/`sea_level` inside the `par_iter` closure for all
273 iterations (`server/tests/hydrology_equilibrium_bench.rs`, confirmed by
direct read: `let (elev, sea_level) = gj1c_512x256();` sits outside the
`(0..body_count).into_par_iter().map(...)` block, and the 273×68-basin sum
in the original run's printed output is the tell — 68 is GJ1c's own basin
count, times 273). **It is GJ1c solved 273 times, not 273 distinct real
bodies.** It proves per-body-open wall-clock affordability (still true, still
a valid, useful measurement — see "What survives unchanged" below) but it
proves nothing about how basin counts, endorheic classification, or —
load-bearing for the cliff Phase-4 ruling — **carved-cliff-edge frequency**
vary across the real body population, since every one of the 273 "bodies" in
that bench is byte-identical terrain.
This addendum closes that gap directly: every real committed heightmap PNG
in the repo, solved independently, once each, at the real production
512×256 working grid, with each body's own PNG-embedded `sea_level` (not a
shared default).
### What was run
New bench: `server/tests/hydrology_equilibrium_bench.rs::bench_population_survey_all_committed_bodies`
(added alongside the existing benches, same file, same `#[ignore]`d
release-only convention). Discovers every `wiki/star-systems/*/bodies/*/heightmap.png`
on disk (**267 files**, confirmed by direct `find` — matches the coordinator's
cited figure), decodes and downsamples each to 512×256 (identical path
`gj1c_512x256()` already used, generalized to every body), and solves each
one independently via Rayon `par_iter` — 267 distinct `solve()` calls on 267
distinct elevation grids, each body's own real `sea_level` read from its
PNG's `sea_level` tEXt chunk (falling back to 0.3 only if a chunk is absent,
same convention `load_heightmap_png` already uses elsewhere).
```bash
cd server
cargo test --release --test hydrology_equilibrium_bench bench_population_survey_all_committed_bodies -- --ignored --nocapture
```
**Known scope limit, stated plainly:** every body uses the same
`ClimateInputs { moisture_q: 55 }` — real per-body moisture would need
`BodyWorldState.districts`/`regions` climate context wired in, which the
original T-1177 prototype already documented as out of scope ("No per-basin
moisture/climate lookup"). This affects the **endorheic** classification
count (a real per-body climate pass could shift how many basins classify
endorheic vs. overflow) but does **not** affect the carving finding below —
carving is gated on ELEVATION geometry (the narrow two-independently-sealed-
basins-plus-single-cell-corridor condition the original module doc
describes), never on `moisture_q`; climate only decides the endorheic/
overflow split of a basin whose carving behavior is already fixed by terrain
shape alone.
### Results — MEASURED, run twice for stability
| Run | Total wall time | ms/body avg | Total basins | Overflow | Endorheic | Lake cells | **Carved-outlet basins** | **cliff_edge cells** |
|---|---:|---:|---:|---:|---:|---:|---:|---:|
| Initial | 858.35 ms | 3.215 | 22,270 | 21,240 | 1,030 | 2,694,012 | **0** | **0** |
| Stability re-run | 857.30 ms | 3.211 | 22,270 | 21,240 | 1,030 | 2,694,012 | **0** | **0** |
Both runs agree to within 0.1% on wall time and are **exactly identical** on
every basin/lake/carve count — which is exactly what determinism predicts
(same 267 PNG files in, same pure-function solver, same output every time)
and is itself a useful confirmation that the population survey's own results
aren't measurement noise. A determinism spot-check on the would-be top
outlier is built into the bench (re-solves the #1-by-cliff-edge-count body a
second time and asserts byte-identical `cliff_edge`/`channel_depth_scaled`
output) — it did not fire in either run because no body had any cliff_edge
cells to check, but the assertion path exists and is exercised the moment
any future body does carve.
**Total wall time across the entire real 267-body population: under 0.9
seconds, single Rayon pass, 16 cores.** This is a slightly smaller number
than the original 273×GJ1c bench's 0.70.8 s, consistent with the two benches
measuring genuinely comparable per-body costs (real bodies average
3.2 ms/body here vs. the original bench's ~2.63.0 ms/body on repeated
GJ1c — same order of magnitude, real terrain is not meaningfully more
expensive than GJ1c's own terrain on average). **The original "under a
second for the whole population" affordability verdict is CONFIRMED, not
just assumed, by this survey** — it was previously proven only for one
body's cost profile repeated; it now holds directly, measured on the real
population.
### The headline finding: carving is not merely rare — it was not observed at all, population-wide
**Zero carved cells. Zero carved-outlet basins. Zero bodies with any
`cliff_edge` cell. 267 out of 267 real committed bodies (100.00%) produced
no cliff/gorge output whatsoever**, at the real 512×256 production working
grid, with each body's own real elevation data and real sea level.
This is a **stronger** result than the original T-1177 measurement's
"structurally rare" finding, not merely a confirmation of it. The original
measurement found zero carved cells across three benches — but two of those
three were synthetic gradient/ridge grids at unrelated resolutions (768×432,
3840×2160), and the one real body tested (GJ1c) is a single data point. It
was an honest, well-argued structural finding (the priority-flood
true-minimum-rim property genuinely does make single-basin carving
mathematically impossible, and that reasoning holds regardless of which body
you test), but "structurally rare, verified on one real body" and
"structurally rare, verified on the entire real population" are different
strengths of evidence, and Troblum's finding was right to demand the latter
before the cliff Phase-4 ruling leans on the rarity argument for its wire
cost case.
**Top 15 bodies by cliff_edge cell count — every single one is 0.** The
"outlier" list below is therefore a non-event by construction (there is no
outlier to report — the highest cliff_edge count found anywhere in the
population is 0, tied 267 ways), but it's included for completeness and
because it shows the population has real geometric diversity (basin counts
range from GJ103c's 40 basins with zero endorheic classification to
GJ1e-m1's 187 basins with 12 endorheic) even while uniformly failing to
trigger carving — the "no carving" result isn't from a population of
near-identical flat worlds, it's from real, varied terrain that genuinely
never happens to produce the narrow two-basin-saddle geometry carving
requires:
| Body | cliff_edge cells | carved-outlet basins | Total basins | Overflow | Endorheic | Lake cells |
|---|---:|---:|---:|---:|---:|---:|
| GJ1c | 0 | 0 | 68 | 66 | 2 | 4,623 |
| GJ1e-m1 | 0 | 0 | 187 | 175 | 12 | 24,041 |
| GJ1002b | 0 | 0 | 61 | 60 | 1 | 3,543 |
| GJ1005Ac | 0 | 0 | 54 | 53 | 1 | 3,395 |
| GJ103c | 0 | 0 | 40 | 40 | 0 | 1,463 |
| GJ105Ac | 0 | 0 | 57 | 54 | 3 | 8,224 |
| GJ1073c | 0 | 0 | 43 | 43 | 0 | 2,084 |
| GJ1075c | 0 | 0 | 31 | 30 | 1 | 2,813 |
| GJ107Ae | 0 | 0 | 56 | 55 | 1 | 3,662 |
| GJ111d | 0 | 0 | 70 | 67 | 3 | 6,047 |
| GJ1111b | 0 | 0 | 145 | 136 | 9 | 23,664 |
| GJ1116Ac | 0 | 0 | 141 | 135 | 6 | 15,873 |
| GJ1116Bc | 0 | 0 | 45 | 43 | 2 | 3,404 |
| GJ1125c | 0 | 0 | 62 | 60 | 2 | 5,154 |
| GJ1156d | 0 | 0 | 42 | 39 | 3 | 4,810 |
(Sorted by cliff_edge count descending, so this is the FULL ranking's top —
every one of the other 252 bodies not shown also reports exactly 0.)
### Verdict on "carving is structurally rare" — upgraded, not merely confirmed
**Holds population-wide, and more strongly than originally stated.** The
correct framing going forward is not "rare" but **"not observed in the real
committed body population at the real production working-grid resolution,
for a structurally-argued reason that predicts the same result on
unobserved bodies too."** The structural argument (priority-flood's
true-minimum-rim property makes single-sealed-basin carving mathematically
impossible; genuine carving needs a rare two-independently-sealed-basins-
plus-single-cell-corridor geometry) was always the load-bearing part of the
original finding, not the one-body sample — this survey confirms the
structural argument's prediction holds at 267× the sample size, with zero
counterexamples.
**What this changes for the cliff Phase-4 wire-cost argument:** strengthens
it. The original ruling's wire-cost case ("a rarely-nonzero field costs
almost nothing under PNG/sparse encoding") is now backed by "zero-populated
across the entire real population," not "zero-populated in one spot-check
plus a structural argument." The `cliffs: Vec<CliffSegment>` sparse field
(Araminta's round-1 shape, adopted in round 2) will ship empty for every
currently-committed body — which is exactly the cheap case the wire-cost
argument always assumed, now measured rather than inferred.
**What this does NOT change:** the field still needs to exist and be
correctly wired, because "never observed yet" is not "cannot occur" — the
mechanism's own unit tests (hand-constructed fixtures,
`cheapest_overflow_path_finds_the_true_minimum_crossing` etc., unchanged by
this survey) already prove the carving arithmetic is correct in isolation,
and a future body (procedurally generated, not yet authored, or an existing
body re-baked with different parameters) could still produce the narrow
geometry that triggers it. The Phase-4-not-Phase-5 scope ruling (Tyre's
round-1 call) is, if anything, easier to defend now: carrying a field that
costs nothing on 267/267 real test cases and is verified-correct by
construction is a strictly better position than carrying a field justified
mostly by argument.
**One honest limitation of this survey, named for the same reason the
original measurement named its own:** this is still the CURRENT real body
population (267 authored bodies) at the CURRENT heightmap generation
parameters, not a claim about every possible future body a procedural
generator might produce. If world generation later produces bodies with
qualitatively different terrain character (the original measurement's own
example: "heavily tectonic, high-relief, many small nested basins"), this
survey doesn't cover that population — it covers the one that exists today,
completely (267/267, not a sample), which is a categorically stronger claim
than the original one-body spot-check but is still bounded by "bodies that
exist in this repo as of 2026-07-23."
### Repro
```bash
cd server
cargo test --release --test hydrology_equilibrium_bench bench_population_survey_all_committed_bodies -- --ignored --nocapture
```
Bench code: `server/tests/hydrology_equilibrium_bench.rs`
(`bench_population_survey_all_committed_bodies`, added alongside the
existing T-1177 benches in the same file). No source file under `server/src/`
was modified — same scope discipline as the original measurement pass.
@@ -504,6 +504,132 @@ planning for these rungs should size off the flat ~1.8 µs/cell single-thread
---
## INTERVIEW-2 ADDENDUM (2026-07-23): chunk (64 m) — the new deepest rung
Jeroen's interview-2 ruling replaced the tile/voxel deepest rung with chunk
(64 m, D-243's "stream/derive unit") — *"the actual tile level rung seems
unusable. maybe replace with 64?"*, judging 10 px/tile-scale content
(~192×108 m full-screen) as in-world viewport territory (Phase 5), not Atlas
map content. Chunk had never been benched (the original T-1154 pass tested
Block 128 m and Tile-adjacent 1 m/4 m; chunk sits between them and was
skipped). New benches, same file (`server/tests/bmv_gridunit_bench.rs`),
same discipline as the rest of this document.
### Chunk per-cell rate, 4,096-cell sweep
| Spacing | Cutoff | Total (4,096 cells) | ns/cell |
|---|---|---:|---:|
| Chunk (64 m) | 64 m (Nyquist) | 7.53 ms | 1,838.1 |
| Chunk (64 m) | 0 (uncut) | 7.447.57 ms | 1,816.01,847.9 |
| Block (128 m) [reference] | 128 m | 7.497.61 ms | 1,827.81,856.8 |
**MEASURED, run twice for stability, both passes agree within ~1%.**
Confirms directly (not inferred from Block) the same "no truncation left"
finding this document already established: cutoff=64m vs. uncut is within
noise, because `VOXEL_OCTAVE_WAVELENGTHS_M`'s finest entry (128 m) is
already coarser than 64 m, so nothing is skipped at either cutoff value.
**Chunk pays the identical flat ~1.8 µs/cell single-thread rate the whole
Block-through-chunk band shares** — same conclusion as this document's
existing "cross-cutting note," now confirmed at the actual new deepest rung
rather than assumed to transfer from Block.
### Chunk realistic deep-step canvas (replaces the retired 216×384 m tile bench)
Full 3840×2160 canvas (8,294,400 cells) at 1 gridunit-per-screen-px — the new
bottom-out rule (see the interview-2 response doc for the full derivation):
chunk does NOT need the old tile rung's 10×-magnification-margin display
convention, so this uses the SAME fixed-canvas-budget convention every
other rung in this document uses, no special case.
| Path | Wall time | ns/cell | Speedup |
|---|---:|---:|---:|
| PARALLEL (16 threads, row-chunked) | 1,724.481,732.84 ms | 207.9208.9 | — |
| SINGLE-THREAD | 15,051.5915,145.11 ms | 1,814.71,825.9 | — |
| — | — | — | 8.738.74× |
**MEASURED, run twice for stability, both passes agree within 0.5%.** This
is NOT the 17 ms the old tile-rung deep-step bench reported for its
216×384 m window — chunk's canvas covers 245.8 km × 138.2 km (two orders of
magnitude more ground) at the same 8.3M cell count, so the derive cost is
correspondingly larger in absolute terms. **1.721.73 s parallel is still
comfortably affordable** — the same band this document's District/Quarter/
Block full-canvas numbers already occupy (1,7311,827 ms at 8.3M cells).
Cost does not gate chunk as the deepest rung.
### Go/no-go: Chunk (64 m spacing) — GO, replacing the retired Tile verdict
**GO for interactive per-step serving, cost-wise, with no viewport-window
qualifier needed.** Unlike the old Tile verdict (which required a
viewport-sized-canvas carve-out to stay inside D-226(d), since a canonical
whole-body 1 m canvas would have been a governance violation), chunk's
full-canvas cost (1.7 s at 8.3M cells, same fixed-canvas-budget convention
as every shallower rung) requires no special case at all — it is priced and
served exactly like Region/District/Quarter/Block. This is a genuine
simplification the interview-2 ruling produced, not a workaround: chunk
doesn't have the same-order-of-magnitude legibility problem a 1 m ground
tile had at screen resolution, so it never needed the 10× margin (or the
canvas-sizing exception that margin forced) in the first place.
---
## S2 ADDENDUM (2026-07-23): deep-step × high-river-density courses cost
Ruled before filing: the last zero-data-point cell on the courses-cost
axis. Every prior courses-inclusive measurement in this document (Cross-check
1, 18 courses/331,776 cells) is at District spacing; nothing measured
courses-on cost at Chunk or Block. New bench:
`bench_s2_courses_density_at_chunk_and_block`, using the real GJ1c river
network (densest real confluence region — the river cell with the most
other river cells within an 8 px search radius), built via the actual PUBLIC
invention pipeline (`river_course::build_edges` + `river_course::invent_course`,
both `pub`; `layer_proxy::invent_courses_near_window` itself is private to
that module, so this bench replicates its per-edge invention loop using the
same public primitives — the same replica-loop discipline this document's
`rect_window_replica` already established).
### Results — MEASURED, run twice for stability
| Rung | Window | Courses in window | Avg points/course | Courses OFF | Courses ON | Delta |
|---|---|---:|---:|---:|---:|---:|
| Chunk (64 m) | 64×64 cells (4,096 m × 4,096 m) | 2 | 1,732.0 | 1,861.91,913.3 ns/cell | 3,489.73,533.7 ns/cell | **+84.7% to +87.4%** |
| Block (128 m) | 64×64 cells (8,192 m × 8,192 m) | 2 | 867.0 | 1,849.41,855.7 ns/cell | 2,544.12,816.8 ns/cell | **+37.6% to +51.8%** |
Both runs agree within run-to-run noise typical of this document's other
4,096-cell sweeps; the direction and rough magnitude are stable across both
passes.
**This is a real, structural finding — meaningfully larger than District's
<5% courses-cost figure (H2/H3 above), not the same order of magnitude.**
Traced to a concrete mechanism, not left as an unexplained number:
`near_perennial_water`'s cost is `O(courses × points-per-course)`, and
`invent_course` resamples each course's control polyline at
`station_spacing_m` — the SAME spacing value as the rung's own cutoff. A
course spanning a fixed chord length gets proportionally MORE points the
finer the rung's station spacing is: this bench's own instrumentation
confirms 1,732 points/course at Chunk (64 m stations) vs. 867 at Block
(128 m stations) — almost exactly the 2× ratio matching the 2× spacing
ratio. District's courses (2,048 m stations) have roughly 32× fewer points
per course for the same chord length than Chunk's, which is exactly why
District's courses-cost figure was small and nobody had measured this
effect before — no courses-inclusive bench existed below District until
this pass.
**Still affordable in absolute terms.** Even the worst case (+87% on a
~1.9 µs/cell baseline) lands at ~3.5 µs/cell — a full 8.3M-cell chunk-
spacing canvas at that rate would be ~3.3 s parallel (at the same ~8.7×
speedup this document's other benches measure), larger than the courses-off
1.7 s figure but not a "computer catches fire" case. **The real implication
is implementation-side:** `near_perennial_water` resampling courses at the
SAME spacing as the rung it serves is a real cost driver that scales
inversely with rung spacing — a station-spacing cap independent of rung
spacing (courses don't need MORE points just because the rung asking for
them is finer, if the goal is "is this cell near a river" rather than
"render the river at full rung resolution") is a plausible optimization
worth flagging for the implementation ticket. This is a finding, not a
ruling — a river-rendering design call, not a cost-measurement one.
---
## Notes on scope and what this document does not claim
- This document does not decide the wire carrier, the windowed-family
@@ -0,0 +1,314 @@
---
title: "Round 1 Notes — Body Map Viewer Workshop"
description: "Compiled record of Round 1 positions: per-question summary, cross-agent agreements, conflicts/tensions, the open-for-synthesis decision list, and scope-drift check against settled premises."
type: workshop
status: active
workshop: body-map-viewer
agent: qatux
round: 1
created: 2026-07-25
---
# Body Map Viewer — Round 1 Notes
**Compiled by:** Qatux
**Round:** 1 — Positions against the measured appendix
**Participants:** Dudley, Araminta, Stig, Tyre (Troblum is round 2 only)
**Sources:** [dudley-round1.md](dudley-round1.md), [araminta-round1.md](araminta-round1.md),
[stig-round1.md](stig-round1.md), [tyre-round1.md](tyre-round1.md)
All four positions are argued from the measured appendix (①–⑤,
[the brief's appendix](body-map-viewer-workshop-brief.md#pre-workshop-measurement-appendix))
with no extrapolation beyond what was measured — each agent says so explicitly and
the citations check out against the four measurement docs.
---
## 1. Per-question position summary
### Dudley — server derivation (hydrology, step-canvas generation, chunking, cache tiers)
1. **Hydrology algorithm:** adopt T-1177's priority-flood + Dijkstra prototype as-is
(production-shaped, not throwaway). Cost: ~24 ms/body single-threaded at 512×256;
all 273 bodies in parallel ≈ 0.70.8 s; solved once per body (like `drainage::analyze`
today), held in a D-203-shaped cache, never re-solved per step/request.
2. **Cliff representation:** `elevation` (unchanged) + `channel_depth: u16` +
`cliff_edge: bool` — direct, non-lossy carry of solver output, no min/max synthesis.
Gorge carving measured **structurally rare** (zero carved cells across all three
production-scale benches) — weakens, doesn't eliminate, the wire-cost argument.
Explicitly defers the Phase-4-vs-Phase-5 scope call to Tyre/Araminta.
3. **Canonical-vs-viewport (red flag 3):** **viewport-sized canvases at every step,
no canonical fixed canvas past the orbital/region rung** — argued as a governance
necessity (D-226(d)), not merely a cache-cost preference. Fixed pixel budget per
canvas (e.g. 3840×2160), not literal client viewport echo.
4. **Compute-chunk partitioning:** the existing row-chunked `into_par_iter()` loop in
`build_district_window_layer` holds flat (190220 ns/cell) at every measured size
and spacing — "the single most load-bearing measured result across all four gates."
Flags that courses are excluded from the replica-loop numbers (small, bounded tax,
not a different order of magnitude) and that chunking-as-seed-input-for-next-tier
is unmeasured, an open data-flow question for round 2/implementation.
5. **Cache tiers:** global/orbital tier only, "always keep" ≈ ~174 MB PNG-encoded
across all 273 bodies, as a D-203-shaped resource extension (not SQLite — cost
doesn't force it). Everything finer: `TTL = BASE_TTL[rung] × time_decay ×
distance_decay`, no disk tier below orbital.
### Araminta — named-feature encoding, per-gridunit payload, encoding continuity
1. **Named-feature encoding is three decisions, not one**, per the existing carrier
three-way rule: settlements/POIs → ids-with-lookup against the already-built
`atlas_city_names` pool (D-223) — presence + id per gridunit, never inline name.
Rivers → geometry already settled (T-1170 Ruling 1c `courses`); adds a new
whole-body `river_names: BTreeMap<edge_id, String>` lookup, many-edges-to-one-name.
Roads: out of scope but the pattern generalizes for free when they land.
2. **Payload schema:** the existing six dense fields stay as-is (no new classification
vocabulary); one new dense field `settlement_id: Vec<u32>`; the cliff fields are
**sparse, not dense** — a `cliffs: Vec<CliffSegment>` list parallel to `courses`,
not two more mostly-zero dense arrays (this differs from Dudley's dense-field framing
— see Conflicts below). States a hard client rule: color/style/tween presentation
only, never invent geometry/flooding/cliffs — missing data is a schema gap, not a
license to interpolate.
3. **Encoding continuity:** one colorizer family confirmed across every step (same
field set, same enum vocabularies, only sampling density changes). Average-back is
safe for continuous fields (`elev_q`/`temp_dc`/`moisture_q`) but **meaningless for
categorical fields** (`morphology`/`vegetation`/`glaciation`) — coarser steps must
re-derive via dominant-mode/plurality pick, never arithmetic mean. "Clearings/ponds
at low zoom" is scoped as sub-zone octave detail within an existing classification,
not new vocabulary — no new wire field needed.
4. **Wire-contract summary for synthesis:** confirms PNG-per-field for all dense arrays,
MessagePack-native for sparse lists; confirms the payload is categorically over the
30 KB cap at every size regardless of her additions — doesn't change the
tagged-envelope verdict.
### Stig — client component, step-cross UX, overlay compositing, cache store
1. **Client structure:** kill `_canvas.scale` entirely — two sibling layers replace it:
RTT terrain/classification layer (texel-exact `ImageTexture`, generalizing the
existing `_build_tile_texture`/`_rebuild_texture_if_needed` mosaic path from
per-tile to universal) + an unscaled screen-space annotation layer (world→screen
transform per frame, literal px sizes, deletes `_zs()`/`_zs_stroke()`/
`_zs_ring_radius()` by construction). T-1158 (viewer decomposition) is **subsumed**
— recommends cancellation with a supersession note, not separate scheduling.
2. **Step-cross UX:** hold-fetch-swap baseline (cursor-anchored), cache-hit swaps
immediately, cache-miss holds the current texture magnified during fetch (bounded
by measurement ①/④: uncached 330K canvas ≈ 75 ms derive + 5 ms encode). Morph/tween
is real but **strictly cosmetic and sequenced after** the baseline ships, as an
optional toggle. Upload cost (⑤) confirmed non-gating; prefers `texture.update()`
reuse over fresh allocation (avoids Texture object churn, despite being marginally
slower in raw ms) and L8 wherever a plane is single-channel.
3. **Overlay compositing (c1):** **ship CPU `set_pixel` coloring first**, not shaders
— today's code already does this and works; nothing in ①–⑤ prices CPU coloring cost
at step-canvas scale (330K8.3M cells), which Stig names explicitly as **the missing
sixth measurement** (`Image.set_pixel` cost at those sizes). Shader migration is the
natural landing spot for T-1175's tapering work later, not a precondition for the
ladder itself.
4. **Client cache store:** **(iii) plain `FileAccess` cache dir + index** — rejects both
(i) server-side SQLite (relocates a client decision onto the server's process
boundary, dilutes the "server owns the DB" mental model) and (ii) godot-sqlite (a
relational addon for a point-lookup-with-expiry access pattern that doesn't need
one). Two-tier eviction: geometry entries get LRU + a retention floor at step-0
(the concrete "always keep global" mechanism, disk-budget-safe at ~440 MB where RAM
would not be); sim-state-tagged entries get an explicit TTL sweep (the literal
self-cleaning behavior Jeroen asked for). In-memory `atlas_window_tile_set.gd`-style
LRU stays on top as the hot tier.
### Tyre — governance ratification, gridunit↔D-243, determinism boundary, cliff ruling
1. **Governance delta ratified as concrete amendment texts** (five, drafted and ready
to file): D-166 corollary repoint (per-step derivation floor + display ratio,
between-step magnification named as a bounded exception not a silent violation);
T-1143 ruling 3 superseded for zoom transport only (cursor-centering/edge-scroll/
full-reset survive); `select_rung`/coverage-walk replaced by the step index;
**tagged-envelope migration ruled triggered, not merely likely** (21×–563× over
the ceiling, "off by two to three orders of magnitude before encoding is even
considered"); T-1170 carrier rule survives with a terminology gloss only
("windowed payload" → "tagged step-canvas envelope").
2. **Gridunit↔D-243 — the load-bearing call: gridunit SNAPS to D-243 rungs, does not
float.** Argued from determinism (`derive_at_metres` on viewport-relative spacing
would contaminate the cache key with a presentation parameter, breaking premise 9's
"canvas for a fixed seed never changes") and from cost (measurement ③ shows no
derivation-cost reason to invent a seventh spacing value — the six D-243 rungs
already cover every depth cheaply). What floats instead: the **display ratio**
(px-per-gridunit, 1×1 to 5×5), a pure client-side presentation parameter, decoupled
from spacing. Files the D-243 gridunit vocabulary entry as "a role, not a new rung."
3. **Determinism boundary:** generalizes the T-1170 course-invention ruling into a
three-bucket test — legal client interpolation is interpolation over a **closed,
server-supplied input set** (two arrived textures, wire-carried control points);
illegal is inventing any sample outside that set or smoothing across a step/
truncation/cliff boundary. Three concrete legal cases named (step-cross morph,
river/road curve-fitting through wire-carried stations, texture-to-viewport
scaling), two illegal (upsampling terrain detail, blending across a step boundary
as if continuous), one gray case flagged for Araminta (label de-dup at coarse-step
seams — legal only if identity, not proximity, drives it).
4. **Cliff ruling:** adopts Dudley's dominant-height + `channel_depth` + `cliff_edge`
representation outright (settled by what the solver emits, zero re-derivation).
**Rules Phase-4 Atlas scope, not deferred to Phase-5** — argues deferral would
silently contradict "settled hydrology" itself (a map showing a smooth shoreline
where the solver computed a carved channel misrepresents the settled state), and
that the rarity finding makes inclusion nearly free rather than making exclusion
safe. Flags his own caveat: rarity is measured on one real body (GJ1c) + two
synthetics, not surveyed across ~273 bodies — names this explicitly as something
for Troblum's round-2 adversarial pass to stress.
---
## 2. Cross-agent agreements (settled going into round 2 — do not re-litigate)
- **Dudley, Araminta, Tyre** all treat the **tagged-envelope migration as settled by
the numbers**, not a round-2 open question. Each cites T-1179 independently and
lands on the same order-of-magnitude reading (21×–563× over the 30 KB cap). Tyre's
position sharpens the brief's "likely triggers" framing to "triggered, unambiguous."
Round 2 should treat *whether* as closed and spend synthesis time only on the
*shape* of the tagged envelope (which is genuinely open — see §4).
- **Dudley and Tyre** independently land on **viewport-sized canvases, no canonical
fixed canvas past the orbital/region rung** — Dudley from cost-shape-plus-D-226(d)
necessity, Tyre from the same D-226(d) argument plus the determinism/cache-key
argument in his gridunit↔D-243 ruling. These are two independently-argued paths to
the same conclusion, not one agent citing the other — worth naming as convergent,
not coincidental.
- **Dudley, Araminta, and Tyre all adopt the T-1177 cliff representation
(`elevation` + `channel_depth` + `cliff_edge`) without dispute.** The only open
edge is dense-vs-sparse wire shape (§3) and Phase scope (§3), not the field set
itself.
- **Araminta and Tyre agree explicitly** that the client may never invent
geometry/flooding/cliff state — Araminta states it as a payload-schema hard rule,
Tyre generalizes it into the three-bucket determinism-boundary test. Same
conclusion, complementary framings (schema discipline vs. interpolation discipline).
- **Stig and Dudley/Araminta agree** that morph/tween between steps is real,
cosmetic-only, and non-blocking for the baseline — Stig sequences it as a
follow-up after hold-fetch-swap ships; Tyre's bucket-A ruling gives it the
determinism green light; nobody argues for shipping it in the first cut.
- **All four agents report their four gated measurements (①–④) as clean GO/VIABLE
results with no extrapolation** — no agent treats any of the pre-workshop numbers
as contested or requiring a re-run.
---
## 3. Conflicts and tensions (named, not yet resolved)
- **Dudley vs. Araminta — dense vs. sparse wire shape for the cliff fields.** Dudley's
round-1 text frames `channel_depth`/`cliff_edge` as "the wire field" without
specifying dense-array vs. sparse-list shape, and his cost argument ("cheap under
any of the encodings in ④'s table... PNG's DEFLATE in particular loves a field
that's constant almost everywhere") reads as implicitly assuming dense per-gridunit
arrays alongside the other six. Araminta explicitly rejects that shape: "as dense
per-gridunit arrays would be wasteful given near-zero occupancy" and proposes a
sparse `cliffs: Vec<CliffSegment>` list parallel to `courses` instead. Tyre's
synthesis leans toward Araminta's read ("I'd steer toward 'new arrays' over 'steal
a bit'... a sparse-friendly encoding... helps this field cost less than its raw
byte width suggests" — but this sentence is ambiguous on dense-vs-sparse, using
"new arrays" language while endorsing a sparse-friendly encoding argument).
**This needs an explicit round-2 resolution, not an inferred one** — the three
positions are close but not identical, and "new arrays" (Tyre) vs. "sparse feature
list, not arrays" (Araminta) is a real implementation-shape fork even if the wire
bytes converge.
- **Stig vs. the brief's implicit assumption — shader-vs-CPU (c1) is not close to
resolved, and Stig's position is a deferral, not an answer.** The brief posed c1 as
"genuinely open for the workshop"; Stig's position doesn't rule for CPU on the
merits so much as rule for CPU *by default* pending a measurement that doesn't
exist yet. This isn't a disagreement with another agent (no one else took a
position on c1), but it is a gap between what the brief expected round 1 to
produce (a position) and what it got (a sequencing recommendation plus a flagged
missing measurement) — surfaced here so the lead interview doesn't mistake "ship
CPU first" for "shader question closed."
- **Scope-boundary tension, not a disagreement: Dudley defers the Phase-4-vs-Phase-5
cliff-scope call to "Tyre/Araminta," and Tyre rules on it unilaterally** ("Phase-4
Atlas scope, not deferred to Phase-5") in the same round without an explicit
Araminta sign-off in her own document (her round-1 text adopts the field shape but
does not independently address the Phase-4-vs-Phase-5 question at all). Tyre's
ruling may be correct and well-argued, but strictly by the documents, it is a
two-of-three-invited-parties call, not a three-way confirmed one — worth a direct
Araminta confirm-or-object in round 2 rather than treating it as fully closed.
---
## 4. OPEN-FOR-SYNTHESIS decision list (for the lead interview)
Carried into Jeroen's Round 1 lead interview and/or Round 2 synthesis, in the order
the brief's Expected Outputs raises them:
1. **Step count and step factor** (largest → visible tile) — every agent treats this
as still-open and downstream of their own numbers; Dudley's table gives per-rung
costs but explicitly does not propose a step count. This is the brief's own
Jeroen-interview question 2 and remains unanswered by round 1.
2. **Map time axis** (climatology vs. current sim state for frozen/flooded) — not
addressed head-on by any of the four round-1 documents beyond Araminta's schema
table flagging it as "where the outline's sea/flooded distinction needs a decision
the workshop, not me, should rule on." Still fully open; this is Jeroen-interview
question 1.
3. **Tagged-envelope migration shape** (not the yes/no, which is agreed — see §2 —
but the concrete framing: new IPC message type, demux mechanics, how the legacy
`district_window` carrier coexists) — Tyre's amendment text names the destination
but defers the mechanical design; this is round-2 synthesis work.
4. **Cliff field wire shape: dense arrays vs. sparse feature list** — the Dudley/
Araminta/Tyre tension in §3, needs an explicit round-2 call, not an inferred one.
5. **Cliff field Phase-4 vs. Phase-5 scope** — Tyre ruled Phase-4; Araminta has not
independently confirmed; the §3 two-of-three tension needs closing explicitly.
6. **Shader-vs-CPU styling (c1)** — Stig's position is "CPU first, sequence shader
later," not a resolution of the open question the brief posed. **Depends on the
missing sixth measurement (below) before it can be treated as settled even
provisionally.**
7. **NEW: the missing sixth measurement — `Image.set_pixel` cost at 330K/2.07M/8.3M
cells (GDScript, client-side).** Stig names this explicitly as a gap: nothing in
measurements ①–⑤ prices CPU per-cell coloring cost at step-canvas scale, and the
c1 shader-vs-CPU call cannot be made "with the same rigor as everything else in
this brief" without it. This should be added to the measurement appendix (as ⑥)
or explicitly scoped as a round-2/pre-implementation follow-up — flagging per the
coordinator's instruction that this lands in the decision list, not just Stig's
own document.
8. **Chunking-as-seed-input-for-next-tier data flow** — Dudley flags this as
unmeasured and architecturally undecided (does a finer step re-derive independently
from `(seed, position)`, or literally consume the coarser step's output values?).
His own cost measurements assume independent re-derivation; round 2 needs to either
confirm that assumption or scope a new cost pass if the alternative is preferred.
9. **Region-as-step-0 confirmation** — Tyre's §1e amendment note flags this needs
confirming with Araminta in round 2 synthesis ("Region likely IS the global/step-0
rung, worth confirming").
10. **The gray determinism case — label placement de-duplication at step seams**
Tyre flags this explicitly as unresolved and hands it to Araminta's named-feature
call; not yet addressed in her document.
---
## 5. Scope-drift check against settled premises
No round-1 document relitigates a settled premise (19) or a "settled by round A"
clarification-round item. Specific checks:
- **Stepped zoom, vocabulary (tile vs. gridunit), data-resolution tunable, hydrology
as deterministic equilibrium** — all four agents use the settled vocabulary and
model correctly throughout; no drift back to continuous zoom or "tile" misuse
detected in any of the four documents.
- **Server-determines-content / client-draws-art (premise 2)** — held firmly by all
four; Araminta's explicit client-rule statement and Tyre's determinism-boundary
bucket test both *reinforce* this premise rather than erode it. No agent proposes
client-side derivation of any invented-detail category.
- **CPU/Rust-server-side-only derivation, GPU presentation-only (premise 2, the c2
pre-empt)** — untouched; Stig's shader discussion (c1) is explicitly about
*styling/coloring already-derived data*, not derivation, and he states this
distinction himself. No drift toward GPU-side derivation anywhere in round 1.
- **One caution, not a violation:** Stig's structural recommendation to cancel
T-1158 outright (not just note supersession) is a **ticket-plan action**, one
category beyond a "position" — it's appropriate content for round 1 (the brief's
Expected Output 2 explicitly wants conflicting tickets named), but it is a
concrete cancellation recommendation rather than a design position, worth the lead
noting as an actionable item distinct from the architecture questions proper.
- **No Phase-5/Phase-6 drag detected.** Stig explicitly declines to scope Phase-5
reuse speculatively ("I don't want to speculatively design against now — that
would be exactly the kind of later-phase drag the cascade discipline warns
against") — this is the correct cascade-discipline call, named here because it's
a positive scope-discipline example, not a violation.
---
## Summary for the lead interview
Round 1 produced four internally consistent, cross-citing positions with **no cost
surprises** — every measured gate came back GO, and three of the four agents
converge on viewport-sized canvases and the tagged-envelope migration independently.
The genuinely open items are load-bearing but narrow: step count itself, the map
time axis, the cliff field's wire shape (dense vs. sparse) and phase scope, the
shader-vs-CPU call (blocked on a **new, named missing measurement**
`Image.set_pixel` cost at step-canvas scale), and two smaller synthesis-confirmation
items (chunking/seed-input data flow, Region-as-step-0). Nothing here requires
relitigating a settled premise, and no scope drift toward later cascade phases was
observed.
@@ -0,0 +1,435 @@
---
title: "Round 2 Notes — Body Map Viewer Workshop"
description: "Compiled record of Round 2 synthesis positions: convergences, verified-absent tensions, and the exact interview-2 decision list against lead-interview-1's rulings."
type: workshop
status: active
workshop: body-map-viewer
agent: qatux
round: 2
created: 2026-07-25
---
# Body Map Viewer — Round 2 Notes
**Compiled by:** Qatux
**Round:** 2 — Synthesis (envelope mechanics, seed-chaining, step-ladder tables,
final cache spec, TTL-split payload, adversarial pass)
**Participants:** Dudley, Araminta, Stig, Tyre, Troblum (adversarial)
**Sources:** [dudley-round2.md](dudley-round2.md), [araminta-round2.md](araminta-round2.md),
[stig-round2.md](stig-round2.md), [tyre-round2.md](tyre-round2.md),
[troblum-round2.md](troblum-round2.md) (920 lines, incl. ADDENDUM + FINAL UPDATE
scorecard), [lead-interview-1.md](lead-interview-1.md),
[measurements/t-setpixel-c1.md](measurements/t-setpixel-c1.md) (⑥),
[measurements/t1177-hydrology.md](measurements/t1177-hydrology.md) (POPULATION
SURVEY addendum, 2026-07-23)
This round produced a filing-ready package: **1 new D-record + 11 amendment
texts + 8 ticket dispositions** (Tyre §a/§d), all four round-1 open items closed
(envelope mechanics, seed-chaining, step-ladder tables, cache tiers), a
population-scale re-verification of the cliff rarity claim, and a full
adversarial pass that resolved 5 of 7 original findings in-round. Troblum's own
FINAL UPDATE scorecard is the authoritative statement of what remains open —
this document verifies it against the four synthesis documents rather than
restating it blind.
---
## 1. Per-agent positions, round 2
### Dudley — envelope wire/serving design, seed-chaining ruling, step-ladder cost fills, final cache spec
- **(a) Envelope mechanics:** read `server/src/bridge/mod.rs` directly and found
the "tagged marker" pattern D-225 asked for **already implemented five times**
(`star_map`/`city_names`/`browse` each carry a mandatory boolean discriminator,
`ShapeProbe` enforces mutual exclusivity). Recommends extending the exact
pattern: `StepCanvasRequest { step_canvas: bool, body_id, step_index, center,
extent, min_wl_m }` as a sixth `Inbound` variant; response is a **dedicated**
`StepCanvasResponse` message (not a field on `AtlasLayerResponse` — D-226
T-1124 §2's own rule names this exact case). Legacy `district_window` survives
byte-unchanged with no coexistence engineering — it just goes cold once its
only client (the retiring `AtlasWindowViewer`) is deleted. Step-0/Region rides
the *new* envelope, not the legacy whole-body family. Answers Araminta's
relayed dense/sparse framing question: **one flat tagged message** (her
default shape), not a split — no server-side compute is saved by splitting,
and the progressive-paint UX case is cheaper to solve client-side.
- **Cliff wire-shape self-correction:** explicitly adopts Araminta's sparse
`Vec<CliffSegment>` shape over his own round-1 implicit dense-array framing,
stating plainly he was "underspecified, not actually disagreeing" — resolves
round-1 conflict #1.
- **(b) Seed-chaining ruling:** **independent re-derivation** — each step calls
`derive_at_metres(seed, position, ...)` fresh; a finer step may call a coarser
rung's derivation *function* internally (the existing region-baseline-feeds-
district precedent) but never reads a coarser step's cached *response* as
literal input. Argued from D-227 purity (cache-dependency chains and
determinism-ordering hazards are exactly what independent re-derivation
avoids) and states this validates his own round-1 cost numbers as the correct
model.
- **(c) Step-ladder tables:** live-coordinated with Tyre mid-draft; corrected his
own first-pass error (an invented, non-D-243 "4 m Tile-adjacent" spacing).
Cost-filled three D-243-disciplined skeletons (Options A/B/C). **Recommends
Option B** (skip-chunk, 5 steps) as the only skeleton where every row is
directly measured or same-band-confirmed — chunk (64 m) was never benched, and
B is the one option that doesn't need it. States plainly that cost does not
distinguish B from C (identical per-row costs); the choice is pure UX pacing,
which he explicitly hands to Jeroen.
- **(d) Final cache spec:** Jeroen's storage-eviction amendment **simplifies**
his round-1 three-term TTL formula, not complicates it — drops
`distance_decay` entirely once staleness and storage-thrift stop being
conflated, landing on one axis: `evict_if: time_since_last_visit(entry) >
STORAGE_TTL[rung]`. Global tier (~174 MB PNG-encoded, D-203-shaped resource,
keep-always) unchanged from round 1. Notes his own design and Stig's
independently-designed client tier converged on the identical two-axis shape.
### Araminta — final payload schema (TTL-split), region-as-step-0, label de-dup, final wire schema
- **(a) TTL-split payload:** the field *set* doesn't change (ratified by
lead-interview-1); this section assigns each field to a **static-geometry
plane** (cached indefinitely-fresh) or a **sim-state plane** (short-TTL,
re-requested on clock-bucket rollover) as a **physical field separation**, not
a policy layered on afterward. States the reasoning per field, not inferred
from name: `glaciation`/new `flooded` → sim-state (their *live* value is the
map-relevant fact); `temp_dc` → static (the map consumes it as a
climate-baseline classification, not a live weather reading — same D-226
clock mechanism, different consumer question). Rules against bit-packing the
two sim-state fields, citing T-1179's own finding that packing hurts DEFLATE
on low-entropy fields — separate L8 planes win on both compression and
independent-re-fetchability grounds.
- **(b) Region-as-step-0: CLOSED three-way** (Araminta + Tyre + Jeroen's
ruling) — states two clauses together per Tyre's wording nuance: Region = step
0, **and** step 0 is the sole canonical/always-keep tier; nothing coarser than
Region is a rung (the planetary seam is elastic, not steppable). Three
independent arguments given (Tyre's snap ruling forces it structurally;
continuity with the existing orbital mosaic; elimination — no coarser D-243
rung exists).
- **(c) Label de-dup:** a deterministic, identity-keyed anchor rule — mean-cell
position (point features) or cropped arc-length midpoint (line features),
computed independently per canvas/tile, explicitly **not** a cross-tile
coordination protocol. Adds a corollary no round-1 document stated explicitly:
during hold-fetch-swap, only the currently-authoritative canvas's annotation
layer draws — the held/stale canvas's terrain texture shows (magnified) but
its labels do not.
- **(d) Final wire schema: CLOSED**, envelope framing confirmed by Dudley's
design. Flags one sequencing note (not a disagreement): Dudley's struct as
literally drafted predates her `flooded` field and needs it added as a ninth
field — doesn't reopen the flat-envelope ruling.
### Stig — measurement ⑥, final c1 call, cache-store spec, px-per-gridunit band
- **(a) Measurement ⑥ headline:** `Image.set_pixel` flat ~78 ns/cell across
330K8.3M cells (25.7 ms / 165.2 ms / 643.5 ms); a hand-rolled
`PackedByteArray` direct write is **~2× slower**, counter to his own
going-in assumption — explicitly flags this as a "don't hand-roll the buffer
write" finding for implementers.
- **(b) C1 final call: CONFIRMED CPU-first**, not a flip to shaders. States the
number "changes how I'd frame the recommendation" — colorize is trivial at
the realistic 330K per-step size (25.7 ms) but meaningful at the 8.3M
stress-ceiling size (~1/3 of server derive time there) — reads this as a
second, independent validation of Dudley's viewport-sizing policy rather than
a new risk, since the policy structurally avoids ever requesting an 8.3M-cell
canvas as a real payload. Names the shader path's trigger condition precisely
(T-1175's tapering work landing, or a future step-count decision pushing the
realistic per-step size meaningfully above ~2M cells) rather than leaving it
a vague someday.
- **(c) Cache-store spec, final:** extends round 1's two-mechanism design
(retention-floor+LRU for geometry, TTL for sim-state) to **three independent
sweep mechanisms** kept structurally separate: Tier 1 (global/step-0) —
retention floor only, no sweep ever; Tier 2 (sub-global geometry) — two
independent triggers, (2a) time-since-last-visit and (2b) LRU-capacity
budget; Tier 3 (sim-state planes) — explicit staleness TTL. Gives a concrete
`IndexEntry` schema. Explicitly defers `STORAGE_TTL`/budget constants as "a
tuning pass once real play-pattern data exists," not an architecture call.
- **(d) Px-per-gridunit band:** step-dependent, not flat — 1×1 at the deep/
ground steps (full fidelity where the player is nearest visible detail),
1×1 preferred at mid steps while realistic canvas count stays under ~2M
cells, ~5×5 reserved for the shallow/orbital step only. Grounded in ⑤+⑥
together: upload cost is flat regardless of display density, and colorize
cost is cell-count-bound, not density-bound — so there's no cost reason to
sacrifice fidelity at the deep end.
### Tyre — final amendment texts, envelope governance disposition, step-ladder tables, deprecation sweep
- **(a) One new D-record + 11 amendment texts**, each stated as record-ready
("paste directly into the named record"): the new render-architecture record
(a.0); D-166 corollary repoint (a.1); T-1143 ruling 3 supersession (a.2);
`select_rung` replacement (a.3); D-243 gridunit vocabulary entry, including
Stig's ⑥-derived step-dependent display-ratio band (a.4); D-226 ceiling
**re-scope**, not retirement (a.5); the map-time TTL-split + staleness/storage
axis distinction, verbatim-grounded in Jeroen's amendment (a.6); the cliff
sparse-list terminology repoint, now **survey-confirmed** by the population
addendum (a.7); the D-226(d) per-request/per-derivation framing + client-cache
accumulation cap, closing Troblum's S3 (a.8); the persistent-cache
schema/version tag, closing Troblum's S5 (a.9).
- **(b) Envelope governance disposition:** the windowed-family ceiling's
**purpose survives untouched**; its **mechanism is re-scoped to the legacy
`district_window` carrier only** — the tagged envelope is a new carrier the
old rule does not apply to by construction. States explicitly: **"the ceiling
rule is RE-SCOPED, not retired"** and that the deprecation table's
conditional SUPERSEDE branch "does NOT fire." Folds in Dudley's three wire
rulings verbatim as confirmation his own drafted text required no changes.
- **(c) Step-ladder tables:** supplies the D-243-disciplined skeleton
constraints (six rungs only, step 0 = Region, deepest = voxel, display ratio
decoupled from spacing); Dudley fills costs. **Joint recommendation: Option
B** — states this is "an evidence argument, not a taste one... Dudley and I
both land here independently." Names the one real seam explicitly: the
Region→District ÷100 jump lines up with the global/sub-global cache-tier
boundary; every factor below it is pure UX pacing with no serving-side
consequence, which he states can be decided "on feel alone."
- **(d) Deprecation sweep:** 11 DQR dispositions (including two added directly
from Troblum's findings: D-226(d) AMEND for the accumulation cap, and a D-192
cross-reference note) + 8 ticket dispositions (T-1176 close-as-delivered,
T-1158 cancel, T-1175 re-scope-and-unblock, T-1157 re-scope, T-1174 keep
unchanged, T-1153/T-1152 stay `done` with code-retirement flagged, their test
suites retired with their code) + an explicit `_canvas.scale` retirement path
for the implementation ticket.
- **Names the exact interview-2 items himself, at the end of his document** (see
§3 below — verified against this).
### Troblum — adversarial pass (BLOCKING/SERIOUS/NOTE, then ADDENDUM, then FINAL UPDATE)
Three-part document, read in full. Initial pass (before any round-2 files
existed) found 2 BLOCKING (B1: the "273 bodies" hydrology bench is one body
solved 273 times, not a population survey; B2: the seed-chaining fork was
unresolved and every cost number implicitly assumed one answer to it), 5
SERIOUS (S1 eviction-interaction costing; S2 courses-density coverage gap; S3
D-226(d) letter-vs-purpose on client-cache accumulation; S4 sim-state phase
cadence; S5 disk-cache schema/version tag), and 3 NOTE items (N1 units
convention; N2 Region-as-step-0 unconfirmed at time of writing; N3 clean bill
on the courses cost bound itself).
**ADDENDUM** (after reading all four round-2 files): re-verified every finding
against source code, not agent prose. B2 **RESOLVED** — checked
`district_profile.rs:1705-1732` directly and confirmed the nested-function-call
pattern Dudley's ruling describes is *already what every measured ns/cell
number exercises*, not merely argued to be consistent with it. Attacked the
envelope design directly with three angles: a discriminator-collision attack
that **did not pan out** (cleared on inspection of `bridge/mod.rs:96-152`), the
version-skew angle that found Tyre's §(a.9) had **already closed** the gap
before Troblum could report it as new, and a genuinely new cache-format
cutover angle that traced to a **clean bill** (the legacy viewer never had a
disk cache to begin with, so there's no stale-key orphaning risk). Checked
Option B's "zero asterisks" claim row-by-row and found one precision note (the
Quarter row is same-band-inferred, not this-session-remeasured at 8.3M) —
explicitly states this doesn't change the recommendation. S1 downgraded to
NOTE (mechanism fully resolved by both Dudley's and Stig's independently
converging designs; only a tuning-pass number remains, correctly deferred).
S3 and S5 confirmed RESOLVED against Tyre's exact amendment texts.
**FINAL UPDATE** (after independently verifying the new population-survey
bench against source): confirmed the new bench (`bench_population_survey_all_
committed_bodies`) is genuinely distinct from the original defective bench —
loads each of 267 real heightmap PNGs independently inside the `par_iter`
closure, distinct `body_id`/`sea_level` per iteration, carries its own
determinism spot-check and a discovery-count guard. **B1 → RESOLVED.** States
the final scorecard explicitly: of seven original findings, five are resolved
(B1, B2, S3, S5, and S1's mechanism half — with S1's cost-number half correctly
deferred as tuning), and **exactly two remain open for interview 2**: S2
(courses-inclusive derive cost has zero measured coverage at the deep-step ×
high-river-density combination) and the S4 residual (sim-state TTL
phase-cadence is still an unnamed numeric variable, though the field-assignment
half Araminta resolved is fully closed).
---
## 2. Convergences (many this round — verified against the source documents, not merely asserted)
- **Flat envelope, one message, not a dense/sparse split.** Araminta's round-1
default assumption; Dudley independently designs and confirms it in his own
`StepCanvasResponse` struct with an explicit cost argument (no server compute
saved by splitting, since every field comes off the same row-chunked pass);
Tyre folds Dudley's ruling into the governance text verbatim. Three-way
agreement, not two citing one.
- **D-226 §2 ceiling re-scope, not retirement — both seats that address it say
re-scope.** Tyre's governance framing ("its purpose survives... its mechanism
is re-scoped to the legacy carrier only") and Dudley's serving-level design
(legacy `district_window` "survives unchanged," the envelope "carries only
NEW step-canvas traffic") independently land on the identical disposition.
Tyre states explicitly in his own document that his §(d) deprecation table's
conditional SUPERSEDE branch "does NOT fire" — re-scope is final, not a live
option still being weighed.
- **Option B (skip-chunk, 5-step ladder) — joint, independently-reached
recommendation.** Tyre states this plainly: "Dudley and I both land here
independently... an evidence argument, not a taste one." The shared reasoning
(chunk/64m was never benched; B is the only skeleton with zero unmeasured
rungs) appears in both documents in the same form, cost-argued rather than
preference-argued.
- **Two-axis cache eviction (staleness vs. storage-budget) reached
independently twice.** Dudley's server-side spec (§(d), the collapsed
one-term `time_since_last_visit > STORAGE_TTL[rung]` formula, replacing his
own round-1 three-term formula) and Stig's client-side spec (§(c), Tier 2's
two independent sweep triggers with no distance term at all) were designed
separately and converged on the same shape. Troblum's ADDENDUM explicitly
names this convergence as evidence the simplification is correct, "not just
convenient."
- **Step-dependent px-per-gridunit ratio, not a flat display ratio.** Stig
proposes it (§(d), grounded in ⑤+⑥ together); Tyre files it directly into
the D-243 gridunit amendment text (§(a.4)) attributed to Stig's measurement
⑥, and Dudley's ladder-table narrative independently confirms the same
policy from a *different* cost driver (server-side colorize is cell-count-
bound, not density-bound — so relaxing to 5×5 wouldn't even help at the
steps where it doesn't need to). Dudley states this as "two independently
measured cost models agreeing on a policy neither one was designed to argue
for" — worth flagging as convergence, not restating as one position echoing
another.
- **Cliff wire shape: sparse `Vec<CliffSegment>`, resolved unanimously.**
Dudley explicitly retracts his own round-1 implicit dense-array framing in
favor of Araminta's sparse-list shape; Tyre's filed amendment text adopts the
same shape and states his own round-1 "new arrays" language was about
rejecting bit-stealing, not endorsing dense arrays — the round-1 tension
(round-1-notes.md §3) is fully closed, three ways, with each agent's own
document stating why they landed where they did rather than silently
adopting a consensus.
- **SQLite rejected in every shape, independently, by both cache-tier owners.**
Restated and reconfirmed in round 2 (Dudley's D-203-shaped resource for the
server global tier; Stig's plain `FileAccess` dir for the client) — not a new
convergence this round but confirmed unchanged from the lead-interview-1
ratification-by-silence.
---
## 3. Tensions — verified against the documents: NONE survive into interview 2
The coordinator's belief that no tensions remain was checked directly, not
assumed. Two items that were live tensions or open questions at the close of
round 1 are confirmed closed by round 2, each with an explicit closing
statement in the record (not merely implied by silence):
- **Round-1 conflict #1 (Dudley vs. Araminta, dense-vs-sparse cliff wire
shape):** closed. Dudley's round-2 document states outright: "Reading her
argument again against my own hydrology numbers, she's right and I was
underspecified, not actually disagreeing." Tyre's filed text adopts the same
shape and states his own prior "new arrays" phrasing was about the bit-
stealing question, not a competing dense-array position. Three-way
resolution with each party's reasoning on record — no residual disagreement
found.
- **Round-1 conflict #2 (Tyre ruling Phase-4-not-Phase-5 for cliffs without an
independent Araminta confirmation):** closed by the population-survey
addendum plus Araminta's own round-2 document, which adopts the ratified
Phase-4 sparse-list shape without objection and builds her final TTL-split
schema directly on it (`cliffs` listed in her static-geometry plane table
with no caveat). Lead-interview-1 ruling 3 already resolved this
("aligns the rarity finding, Araminta's encoding logic, and Tyre's scope
ruling") before round 2 began; round 2's population survey (Troblum's B1,
now RESOLVED) strengthens rather than reopens it.
**What is NOT a tension, despite surface appearance, and should not be treated
as one at interview 2:** Troblum's two remaining open items (S2, S4-residual)
are **not disagreements between agents** — no participant contests them, no
two documents give conflicting answers, and Troblum's own FINAL UPDATE frames
both as measurement/tuning gaps rather than architectural disputes. These
belong in the decision list below as **[decide]** items (does the gap get
closed before filing or deferred), not as **tensions** to relitigate. The
coordinator's framing (list them as decisions, not conflicts) matches what the
documents actually show.
**One asymmetry worth naming, not a tension:** Stig's round-2 §(d) phrases the
step-dependent display-ratio policy from the deep end ("1×1 at the deep/ground
steps... relaxing toward 5×5 only at the shallower steps"), while Dudley's
narrative phrases the same policy from the cost-driver end ("1×1 wherever the
resulting cell count is affordable... reserving the coarser ratio only for the
one place canvas extent is the pressure"). These are the same policy stated
in two directions, not two different policies — Tyre's filed amendment text
(§(a.4)) reconciles both into one canonical statement. Flagging only so this
isn't mistaken for a wording drift at interview 2.
---
## 4. EXACT interview-2 decision list
Cross-checked against Tyre's own explicit "For interview 2, the calls left for
Jeroen" list (`tyre-round2.md`, final section) and Troblum's FINAL UPDATE
scorecard (`troblum-round2.md`). **The coordinator's proposed list is correct
as stated, with one addition Tyre's own document makes explicit that should be
folded in rather than treated as separate — see item 3's note below.**
1. **[ratify] Step ladder: Option B vs. Option C.** Pure pacing choice — every
costed row is byte-identical between the two skeletons (Tyre: "it costs
identically to B row-for-row... District's absence is not a cost saving,
it's purely a pacing choice"). B is the joint recommendation (5 steps,
gradual ÷100-then-÷4-then-÷4-then-÷128 descent); C is 4 steps with a single
blunt ÷400 top jump. Option A (6 steps, including the unmeasured chunk rung)
is ranked last by both agents and is not presented as a live choice for
Jeroen — it is named only to show why it was excluded.
2. **[ratify] D-226 §2 ceiling disposition: re-scope vs. retire.** Both seats
that address this — Tyre's governance framing and Dudley's serving-level
design — say **re-scope** (the rule keeps governing the legacy
`district_window` carrier verbatim; the tagged envelope is a new carrier
outside its scope, not an exemption from it). This is presented as a
one-line ratification, not a live fork — Tyre states his own deprecation
table's conditional retire-branch "does NOT fire."
3. **[ratify] Seed-chaining = function-composition, confirmed as Jeroen's
outline intent.** Dudley's round-2 ruling (independent re-derivation; a
finer step may call a coarser rung's derivation *function*, never read a
coarser step's cached *response*) is **source-verified** by Troblum's
ADDENDUM — not merely argued, but checked directly against
`district_profile.rs:1705-1732` and confirmed to be the exact pattern
every measured cost number in the appendix already exercises. What
remains for interview 2 is narrower than "does the ruling hold" (it
does, on the code) — it is **authorial confirmation**: does "this at the
same time serves as seed information for the deeper cascade" in Jeroen's
original outline mean this function-composition reading, or did he intend
literal consumption of a coarser step's already-computed response bytes?
Troblum frames this precisely as a one-line confirmation item, "very
likely to confirm cleanly," not a re-open — but notes if Jeroen meant the
second reading, Dudley's cost model needs a re-bench before the ladder
table is authoritative. **This is exactly the coordinator's item 3, stated
with the precision both Tyre and Troblum give it — no correction needed.**
4. **[decide] Troblum's two residuals — close before filing, or defer.**
Confirmed as the only two items remaining open on Troblum's own final
scorecard (of seven original findings, five are resolved; these two are
not):
- **S2 — deep-step × high-river-density courses bench.** The one
courses-inclusive-at-real-density number in the whole appendix (T-1178
Cross-check 1, 195.0 ns/cell, 18 courses, District spacing, one body) has
zero coverage at the deepest step (Block/Tile spacing, where the deep-step
83K-cell bench is courses-EMPTY by construction) crossed with high river
density (a BraidedDelta/MeanderReach-class body, not yet sampled).
Troblum's own framing: "not blocking... but it is the one combination in
the whole measured appendix with literally zero data point." His ask is
explicit: "run one more bench... before or shortly after filing; not a
blocker for filing itself" — meaning the decision Jeroen faces is
**run-now vs. defer-to-a-named-follow-up**, not whether the finding is
valid.
- **S4 residual — sim-state TTL phase-cadence.** Araminta's round-2 work
fully resolved *which* fields are sim-state and *why* (per-field
reasoning, not name-inference); what remains is a **numeric** gap — D-228's
"computed once per phase" water-height/glaciation model never states how
long a phase lasts, so the sim-state plane's TTL has no number to size it
by. Troblum's framing: fold into the same tuning-pass deferral Stig/Dudley
already used for their own unresolved constants (acceptable), **or** if
Jeroen wants a concrete number before filing, that's a named Dudley/
Araminta follow-up question at interview 2. Same shape as S2: a
run-now-vs-defer choice, not a contested finding.
**Nothing else requires a Jeroen ruling at interview 2.** Every other item any
agent flagged as open at the close of round 1 (envelope mechanics, cliff wire
shape + Phase-4 scope, region-as-step-0, label de-dup, the c1 shader-vs-CPU
call, TTL-split field assignment) is closed in round 2 with an explicit
closing statement traceable to a specific document and section, verified
above.
---
## Summary for the lead interview
Round 2 delivered a complete, cross-verified filing package — not a set of
independent positions still needing reconciliation. Every one of round 1's
open items closed, most of them multiple ways (region-as-step-0 three-way;
cliff wire shape three-way with each party's reasoning stated; the envelope
ceiling disposition two ways with an explicit "does not fire" statement on
the untaken branch). Five distinct convergences were independently reached by
different agents from different starting arguments — worth Jeroen hearing as
confidence signal, not restated agreement. Troblum's adversarial pass is the
strongest evidence the package is filing-ready: of seven original findings
(two BLOCKING), five are now fully resolved and **verified against primary
source**, not merely re-asserted by the agent being checked — including a
brand-new, population-scale (267/267 real bodies) re-run of the cliff-rarity
claim that came back stronger than the original one-body finding. Exactly
four items need Jeroen's voice at interview 2: two ratifications on facts
already agreed (step count B-vs-C is pure pacing; the ceiling re-scope is
unanimous), one authorial-intent confirmation that the code already supports
(seed-chaining), and two narrow run-now-vs-defer calls on measurement/tuning
gaps that nobody disputes are real but that nobody has treated as
architecture-blocking either.
@@ -0,0 +1,333 @@
---
title: "Body Map Viewer — Stig's Round 1 Position"
description: "Client map component structure, step-cross UX, overlay compositing, and the client cache store decision"
type: workshop
status: active
workshop: body-map-viewer
created: 2026-07-25
owner: Stig
---
# Stig — Round 1 Position
Answering the four questions in my slice. Grounded in the measured appendix
(①–⑤) and the current `client/ui/implant/apps/atlas/` cluster as it exists on
disk today, not as remembered from the T-1143 doc.
## 1. The client map-drawing component
**Kill `_canvas.scale`. There is no more zoom-scaled canvas.** Today's model —
`_canvas.position`/`_canvas.scale` on a Node2D, `_apply_transform()` setting
`_canvas.scale = Vector2(_view_zoom, _view_zoom)`, every nature-overlay
`draw_*` call wrapped in `_zs()`/`_zs_stroke()`/`_zs_ring_radius()` compensation
— is the entire error class named in the brief (§2 of prep-grounding). The
premise "server determines content, client draws map-art" doesn't just permit
retiring that model, it removes the only reason it existed: the scale node was
there to let one held composite serve a continuous zoom range. Stepped zoom
with a server-resolved canvas per step means the client never needs to render
the *same* data at two different implied resolutions — each step gets its own
texture, drawn near enough to 1:1 that "zoom" stops being a client-side
transform of derived geometry at all.
**Structure: two sibling layers under one step-canvas owner, replacing the
single `_canvas` Node2D.**
- **Terrain/classification layer — RTT, texel-exact.** One `ImageTexture` per
held step canvas, built server-side-derived / client-colorized, drawn via
`draw_texture_rect` at the tunable px ratio (1:1 ideal, up to 5×5 px/gridunit
fallback per the resolution tunable). This is **not a new pattern** — it's
`_rebuild_texture_if_needed`/`_build_tile_texture` in
`atlas_window_overlay.gd` generalized from "per-tile mosaic composite" to
"the one and only terrain path." The tile-mosaic code
(`_draw_tile_mosaic`/`_draw_one_tile`/`_tile_texture_cache`) already proves
the RTT-per-cell-block shape works and composites cleanly at orbital rest
state — it's not being invented, it's being promoted to universal.
- **Screen-space annotation layer — unscaled sibling, literal px.** Everything
currently living in `atlas_window_geometry_nature.gd`'s `_zs`-wrapped
`draw_polyline`/`draw_circle`/`draw_arc` calls (rivers, settlement glyphs,
POI rings, mouth markers) moves to a sibling `Node2D` that is **never
scaled**. Positions are world→screen transformed per-frame (cheap — it's a
linear map, not a re-derivation); sizes are constants or class-driven
constants, never divided by a zoom factor. This deletes `_zs()`,
`_zs_stroke()`, `_zs_ring_radius()`, and the entire "did I remember to wrap
this call site" failure mode — by construction, not by discipline.
**What retires:**
- `_canvas.scale`/`_canvas.position` transform model and `_apply_transform()`
as currently shaped (a replacement "step-canvas anchor" concept survives,
see step-cross below, but it's not a continuous scale).
- The `_zs`/`_zs_stroke`/`_zs_ring_radius` compensation family in
`atlas_window_geometry_nature.gd` — wholesale, once the sibling layer lands.
- `select_rung()`'s coverage-ceiling walk (Tyre's call to make formally, but
it's dead the moment the step index replaces it — my component doesn't need
a rung selector, it needs a step index).
- The `AtlasViewer`/orbital-mosaic-vs-window split as two *code paths* — under
the stepped model, global zoom is just step 0, not a structurally different
viewer. (T-1157's dead-goldens problem is a direct consequence of this split
existing today; the redesign should collapse it, not re-target goldens at
the same fork.)
**What survives as-is:**
- `atlas_window_tile_set.gd`'s LRU-by-key cache *shape* (see §4 — it's the
right skeleton for the new client cache, wrong eviction policy alone).
- `_filter_for_granularity_v2()` — NEAREST vs LINEAR sampling filter choice
per rung is still a real question at RTT scale-ratio draw time, independent
of the transform model change.
- `atlas_window_water_clip.gd`, `atlas_marker_overlay.gd`,
`atlas_legend_panel.gd`, `atlas_overlay_bar.gd` — these are compositing/UI
chrome, not the transform mechanism; they consume whatever the new layer
pair exposes and shouldn't need structural rewrites, only call-site updates.
T-1158 (viewer decomposition into input/orchestration/canonical-frame
clusters) is **subsumed, not scheduled separately** — the canonical-frame
state machine T-1158 wanted to extract is the exact thing that changes shape
under stepped zoom (no more continuous zoom floor, no more
`_canonical_fit_zoom()` fitting a float zoom — the "canonical frame" becomes
"step 0"). Extracting the old cluster now would be extracting code about to be
deleted. Recommend cancelling T-1158 with the supersession note and letting
the new component's structure be decided fresh as part of this implementation
(naturally three pieces again — input/pan, step-cross orchestration, and the
two draw layers — but that's a consequence of good decomposition, not a
retained ticket).
## 2. Step-cross experience
**Hold-fetch-swap, not blend-fetch-swap, as the baseline — a morph is a
separate, optional cosmetic layer on top.**
Sequence on a scroll-step:
1. Player scrolls one notch. Compute the new step's data-canvas bounds
(cursor-anchored — the center the new canvas should be requested around is
the world point under the cursor, matching the surviving entry-seam
behavior).
2. If that canvas is already in the client cache (§4), swap immediately —
this is the common case for backtracking (zoom out then back in) and for
revisiting a spot, and it's why the cache matters for *feel*, not just
bandwidth.
3. If not cached, **hold the current step's texture displayed, unscaled,
while the fetch is in flight** — this is the "between-step magnification"
red flag's actual mechanism: showing the coarser canvas magnified to fill
the new step's viewport for the fetch duration. Measurement ① confirms
this window is short: District-class server derive is sub-millisecond
served, and even an uncached 330K-gridunit step canvas is ~75 ms
derive + ~5 ms PNG-encode server-side (measurement ④'s "derivation cost
for context" row) — the hold interval is double-digit milliseconds, not a
visible stall.
4. On arrival: decode, build/update the `ImageTexture` (measurement ⑤:
worst case 8.3M px update is ~4.34.6 ms median, comfortably under one
frame), swap the terrain layer, re-run the annotation layer's
world→screen transform against the new step's bounds.
**ImageTexture upload cost is a non-gating input, confirmed by ⑤.** Every
canvas size the workshop cites uploads in single-digit ms with no observed
frame-budget break, including the frame-delta-spike case. This means the
step-count/canvas-size decision (Dudley's/Tyre's call) can be made on
derivation cost and wire size alone — upload is not a constraint that trades
off against them. One concrete implementation note from ⑤: **prefer
`texture.update()` reuse over fresh `create_from_image()` per step**, not for
raw speed (reuse is marginally *slower* in the raw numbers — 4.34.6 ms vs
3.2 ms median at 8.3M px) but because it avoids per-step Texture object churn
on the RenderingServer side that the microbenchmark doesn't capture. And:
**use L8 wherever a plane is genuinely single-channel** — 49× cheaper than
RGBA8 at every size in ⑤, a free win if any wire field (elevation, a
grayscale classification pass) can ship single-channel before the client
colorizes it.
**The morph/tween is real but strictly cosmetic, and I'd sequence it after
the hold-fetch-swap baseline ships, not with it.** A cross-fade or scale-tween
between the held step-N texture and the arriving step-(N+1) texture, purely
in screen space, never touching derived data — this is squarely inside "GPU is
presentation only." It softens the perceptual jump D-166's amendment has to
own honestly (red flag 1) without pretending to be continuous zoom. I'd
implement it as an optional `CanvasItem` alpha/scale tween gated behind a
toggle, not a hard requirement — if it turns out ugly or distracting at real
step factors, dropping it costs nothing structurally because it never
participates in the data path.
**Client-side cache is what makes repeated step-crossing (the actual common
case — players hunt around a region, not monotonically zoom in once) feel
instant.** Per premise 9: a canvas for a fixed seed never changes, so once
fetched it is valid forever for that exact (body, step, center) key — the
`atlas_window_tile_set.gd` LRU shape already assumes exactly this ("no
freshness check, no TTL, no invalidation path — the only reason an entry
leaves is capacity pressure"). The new cache needs the same discipline **plus**
the sim-state carve-out premise 9 calls out (frozen/flooded needs a shorter
TTL layered on top of the otherwise-permanent geometry entries) — see §4 for
the store shape.
## 3. Overlay compositing under RTT — shader vs CPU (the c1 decision)
**I'd ship CPU `set_pixel` coloring for the terrain layer first, with a
concrete, named follow-up to shader-side compositing once the toggle set
grows — not because shaders are wrong, but because the migration risk right
now is elsewhere and CPU coloring is what's already proven in-tree.**
Reasoning:
- **Today's code already does this and it works.** `_build_tile_texture`/
`_rebuild_texture_if_needed` build the `Image` cell-by-cell via `set_pixel`
from typed field values (`_cell_color`/`_base_cell_color`/`_temp_cell_color`/
`_moisture_cell_color`/`_veg_cell_color`/`_apply_glaciation` — five toggle
overlays already implemented this way). This is the temperature/moisture/
vegetation/glaciation toggle set the question asks about, already shipped
on the CPU path. Moving the *transform model* (RTT-as-universal, no more
scaled canvas) is already the workshop's biggest client-side change; I don't
want to also change the *coloring* mechanism in the same pass without a
measured reason to.
- **The measured numbers don't force the shader answer.** Nothing in ①–⑤
prices CPU `set_pixel` coloring cost at step-canvas scale (330K8.3M
cells) — that's a real gap, and I'd flag it as a follow-up measurement
before committing either way at the deep end of the ladder. What ⑤ *does*
show is that the upload step (which happens regardless of who colors the
pixels) is cheap; the open question is purely "how long does building the
`Image` take in GDScript at 330K+ cells," which is untested. Given that gap,
defaulting to the known-working CPU path and measuring before the largest
canvas sizes ship is the honest sequencing — not picking shaders on the
strength of "should be faster" without a number.
- **Where shaders clearly win, and where I'd schedule the follow-up:** the
moment T-1175's per-vertex river tapering/width-grammar work starts (source
tapering, tributary-join width ramps), that work wants shader-side or at
minimum `Polygon2D`-strip geometry regardless of what the terrain layer
does — `draw_polyline` is single-width by construction and can't taper.
That's a screen-space-annotation-layer concern (§1), not a terrain-raster
concern, and it's already gated on "after the nature layer stands" per
T-1175's own scheduling note. If shader-side terrain compositing is adopted
later (multiple data textures — morphology/elev/temp/moisture/vegetation/
glaciation — sampled and colorized in a fragment shader), the toggle
overlays and T-1175's tapering become the same mechanism for free, which is
a genuine architectural win — I'm not against it, I'm against committing to
it on zero cost data for the actual bottleneck (CPU set_pixel at 8.3M
cells) when a known-good fallback exists.
- **Recommendation for the ticket plan:** ship CPU coloring behind the new RTT
structure for the initial step-ladder cutover (lowest risk, matches proven
code), file a measurement ticket for GDScript `Image.set_pixel` cost at
330K/2.07M/8.3M cells (the missing sixth measurement), and treat the shader
migration as the natural landing spot for T-1175 rather than a
precondition for the ladder itself.
## 4. The client cache store — in-memory vs disk-backed
Answering the three candidate shapes against the fact base (SQLite ships
server-side only; client has gdUnit4 + messagepack, no SQLite addon today).
**My answer: (iii) plain `FileAccess` cache dir + index, not (i) server-side
SQLite and not (ii) godot-sqlite.** In that order of preference, for these
reasons:
**Against (i) server-side SQLite cache DB.** The "client-primary reads as
local-machine-primary" argument is real — a warm local subprocess is fast —
but it quietly relocates a client-side design decision onto the server's
process boundary and turns every cache read into an IPC round-trip instead of
an in-process Godot call. It also risks exactly the thing the asset-pipeline
golden rule and D-227 both warn about: a second SQLite file living next to
`systems.db` invites confusion about which one is canonical, even with a
different filename, because the *mental model* "the server owns the DB" now
has two meanings (canonical snapshot vs cache). D-227's discipline needs to
be re-proven at a new file rather than reusing an already-well-understood
boundary. Not fatal, but it's the shape with the most governance surface area
for the least architectural gain — premise 9 explicitly frames this as a
*client*-side cache question, and routing it through the server subprocess
undercuts the "client-primary" framing it's supposed to answer.
**Against (ii) godot-sqlite addon.** A real client-side store, and if the
cache needed relational queries (joins, filtered scans across many keyed
records) I'd pick this without hesitation. It doesn't — the access pattern is
point lookups by a composite key (`body_id:step:center:granularity_v2`,
extending `atlas_window_tile_set.gd`'s existing `make_key()` shape almost
unchanged) plus a periodic TTL sweep over sim-state-tagged entries. That's a
key-value store with expiry, not a relational workload. Pulling in a new
compiled addon (build/platform surface, version-pin maintenance, another
thing that can fail to load headless in CI) to do a job `FileAccess` +
`Time.get_unix_time_from_system()` already does natively is the kind of
dependency I'd only take if the simpler shape measurably couldn't do the job.
It can.
**For (iii) plain `FileAccess` cache dir + index — the shape:**
- **Directory layout:** one file per cached step-canvas entry under
`user://atlas_cache/<body_id>/`, named by a hash or the same key string
`atlas_window_tile_set.gd` already builds (`make_key()` extended with the
step index) — content is the already-decided wire encoding (PNG-per-field
per measurement ④, the clear winner on size *and* speed), so the disk file
IS the wire payload, no re-encoding for storage.
- **Index:** one small JSON or binary manifest (`user://atlas_cache/index.
<body_id>.dat` or similar) mapping key → `{written_at, last_read_at, kind:
geometry|sim_state, size_bytes}`. Loaded once per body-open, held in memory
as a `Dictionary` — this is the same LRU-touch shape `atlas_window_tile_set.
gd` already implements (erase+reinsert = move-to-MRU), just backed by files
on disk instead of values in the dictionary, and the dictionary now stores
metadata + a `FileAccess` path instead of the raw window `Dictionary`.
- **Two-tier eviction, matching premise 9's split exactly:**
- **Geometry entries (morphology/elev/moisture/vegetation/glaciation/
height, everything D-227's determinism guarantee covers): LRU-evict-only,
no TTL**, same as today's `atlas_window_tile_set.gd` — a canvas for a
fixed seed never changes, so "stale" isn't a concept that applies. Global
step (step 0) entries get a **retention floor** (never evicted by the LRU
sweep, only by an explicit clear/uninstall path) — this is the concrete
mechanism for "always keep the global level" from Jeroen's outline, sized
by red flag 2's number (~1.6 MB/body at 5×5 sampling; even at all ~273
bodies resident that's ~440 MB on disk, which is a completely different
budget conversation than 440 MB in *process memory* — disk is cheap,
RAM/VRAM residency is the thing that needed the ceiling).
- **Sim-state-tagged fields (frozen/flooded, whatever the map-time-axis
ruling lands on): explicit TTL sweep**, a periodic (not per-frame — on
body-open and on a coarse timer) pass over the index removing entries
past their TTL regardless of LRU recency. This is the literal
self-cleaning-records behavior Jeroen asked for ("on disk probably, but
with self cleaning of cache records"), and it's the one place this store
needs logic `atlas_window_tile_set.gd` doesn't have today.
- **Never the source of truth, enforced structurally, not just by
comment:** if the cache directory is deleted, the client's only behavior
change is re-fetching from the server — same guarantee D-227 gives
server-side, now proven at the client tier too. Boot-time behavior:
missing/corrupt index → treat as empty cache, don't crash, don't block
first paint.
- **Sizing from the wire table (④):** at the PNG-per-field encoding
(the clear winner — smallest and fastest at every size), a 330K-gridunit
step canvas is ~638 KB; a full global-tier set across ~273 bodies at the
5×5 fallback resolution is the ~440 MB figure red flag 2 already computed.
That number is **disk-budget-safe on any target platform** (modern
discretionary disk cache budgets for a locally-installed game are routinely
in the multi-GB range) in a way it is not RAM-safe — which is the
strongest single argument for "disk-backed, not memory-only": premise 9's
"always keep global" instruction is only affordable at all if it's a disk
tier, not permanent process residency. An in-memory-only cache would have
to either violate "always keep global" (evict it under memory pressure,
defeating the snappy-navigation goal) or accept the ~440 MB RAM floor red
flag 2 already calls infeasible. Disk removes that tension entirely.
- **In-memory tier still exists, layered on top, not replaced.** The
`atlas_window_tile_set.gd`-style in-process Dictionary cache remains the
hot path for "the step I'm looking at right now and its immediate
neighbors" — a session-scoped, small (current `DEFAULT_MAX_ENTRIES = 24`
is a reasonable starting point, tunable) LRU exactly as it works today.
The disk store is the tier below it: a miss in memory checks disk before
going to the server. Three tiers total, cheapest-first: in-memory Dictionary
→ disk `FileAccess` store → server (which has its own tiers, Dudley's to
answer). This layering is what makes "client-primary reads as
local-machine-primary" actually true without needing the server-SQLite
detour — the disk tier gets you nearly all of that latency win already,
in-process, no IPC.
**Net recommendation:** (iii), layered under the existing in-memory LRU as a
second tier, two eviction policies (retention-floor+LRU for geometry,
TTL-swept for sim-state), sized directly from measurement ④'s table. No new
Godot addon, no second SQLite file, no server round-trip for a client-local
question.
## Open items I'm carrying into round 2
- **The missing sixth measurement:** GDScript `Image.set_pixel` coloring cost
at 330K/2.07M/8.3M cells — needed before the c1 shader-vs-CPU call can be
made with the same rigor as everything else in this brief. I'd rather name
this gap now than let CPU-coloring become an unexamined default at the
largest canvas sizes.
- **Step-count/canvas-size** is Dudley's/Tyre's number to land, but it
directly sizes my annotation-layer redraw frequency and cache key space —
I'll adjust §1/§4 sizing once that lands in the lead interview.
- **Phase-5 reuse question (Jeroen's #4):** I'd scope the map-drawing
component as **Atlas-only for this pass**. The RTT-terrain +
screen-space-annotation split is a reasonable shape for a future in-world
viewport too, but Phase-5 in-world rendering has different constraints (
per-frame redraw, character-relative camera, no step-cross discontinuity to
design around) that I don't want to speculatively design against now — that
would be exactly the kind of later-phase drag the cascade discipline warns
against. Build it right for Atlas; revisit reuse when Phase 5 actually
starts.
@@ -0,0 +1,300 @@
---
title: "Body Map Viewer — Stig's Round 2 Position"
description: "Measurement ⑥ (CPU-colorize cost), the final c1 shader-vs-CPU call, the cache-store spec with Jeroen's storage-eviction amendment, and the px-per-gridunit band recommendation"
type: workshop
status: active
workshop: body-map-viewer
created: 2026-07-25
owner: Stig
---
# Stig — Round 2 Position
Round 1 (`stig-round1.md`) flagged one open item: the c1 shader-vs-CPU call
was made CPU-first but explicitly **pending** the missing `Image.set_pixel`
cost number. Jeroen ruled at lead interview 1 to run it now — done, below,
plus the confirmed cache-store spec incorporating his new storage-eviction
amendment, and the px-per-gridunit band Dudley/Tyre need for the ladder
tables.
## (a) Measurement ⑥ — headline
Full doc: `measurements/t-setpixel-c1.md`. Appendix row added to the brief.
| size | `set_pixel` median | ns/cell | byte-buffer median | ns/cell |
|---|---:|---:|---:|---:|
| 330K | 25.7 ms | 77.5 | 51.0 ms | 153.8 |
| 2.07M | 165.2 ms | 79.7 | 319.2 ms | 154.0 |
| 8.3M | 643.5 ms | 77.6 | 1,261.4 ms | 152.1 |
Flat per-cell rate across the full 25× size range for both paths (no
cliff — same pattern every prior measurement in this appendix found).
Headless-valid: this is pure CPU `Image`/`PackedByteArray` manipulation, no
RenderingServer call in the path, unlike ⑤ which needed a real display to
avoid the headless dummy renderer's faked GPU uploads.
**Two findings, one expected, one not:**
1. Expected: cost is real and scales linearly, not free the way upload (⑤)
turned out to be.
2. Not expected: **`Image.set_pixel` beats a hand-rolled `PackedByteArray`
direct write by ~2×**, at every size. I went in assuming the opposite —
skip the per-pixel method call, skip `Color` object construction, write
raw bytes. Measured, GDScript's own per-element indexed `PackedByteArray`
write (four separate indexed writes per cell in the buffer path) costs
more than `set_pixel`'s single per-pixel call. This is worth stating
plainly for whoever implements the terrain layer: **don't hand-roll the
buffer write as a "faster" alternative in GDScript** — the intuition
that's usually right in a compiled language doesn't transfer here.
## (b) C1 final call — CONFIRMED, CPU-first, with an explicit sizing caveat
Round 1 said CPU-first-pending-the-number. The number is in. **I'm
confirming CPU coloring (`Image.set_pixel`) as the shipped default for the
terrain layer, not flipping to shaders — but the number changes *how* I'd
frame the recommendation, and it sharpens rather than weakens the case for
Dudley's viewport-sized-canvas policy.**
**Why confirm, not flip:**
- **At the sizes the ladder will actually request, colorize is cheap.**
Dudley's round-1 position (§2, "Question 2") is explicit that
viewport-sized canvases at a fixed pixel budget — not the 8.3M-cell figure
— are the real per-step request shape; 8.3M exists in the appendix purely
as the stress-ceiling case answering "does this degrade at scale" (no).
At 330K cells, the size that shape actually produces, `set_pixel` colorize
costs **25.7 ms** — well inside "the player is waiting for a step-cross to
resolve" tolerance, and it's happening once per arrived canvas inside the
hold-fetch-swap sequence (§(c) below refines that sequence's cost
ordering), not per frame.
- **Nothing in ①–⑤ forced shaders architecturally, and ⑥ doesn't either —
it just prices the CPU path honestly instead of leaving it an
unmeasured assumption.** The original round-1 reasoning stands: today's
code already does CPU coloring successfully (`_build_tile_texture`/
`_rebuild_texture_if_needed`, five toggle overlays shipped this way), and
the biggest structural change this workshop makes to the client (RTT as
universal terrain path, killing `_canvas.scale`) is large enough on its
own without also swapping the coloring mechanism in the same pass without
a forcing reason.
- **Where the number DOES change my framing:** round 1 called the gap
"a real gap" but didn't know if it would come back trivial or
meaningful. It came back **meaningful at the stress-ceiling size** (643 ms
at 8.3M is roughly a third of server derive time at that size — not a
rounding error) **and trivial at the realistic per-step size** (25.7 ms at
330K). That's not a wash — it's a data point that argues *for* Dudley's
viewport-sizing policy being load-bearing on the client side too, not just
the D-226(d) governance reason he named. If some future caller ever
requested a literal 8.3M-cell canvas as a real step payload (not the
stress-test shape), CPU colorize alone would eat ~640 ms of the arrival
budget — a real, nameable cost that the viewport-sized-canvas policy
structurally avoids by never generating that request in the first place.
I'd rather land this as "the policy is validated from a second angle" than
as a new risk, since Dudley already ruled viewport-sized canvases as the
policy on governance grounds independent of this number.
- **The shader path is not closed, it's sequenced.** Same as round 1: the
natural trigger for shader-side terrain compositing is T-1175's per-vertex
river tapering work (needs shader/Polygon2D-strip geometry regardless of
what the terrain raster does) landing and, separately, if a future step
count/canvas-size decision in round 2's ladder synthesis pushes the
*realistic* per-step canvas size meaningfully above 330K2.07M (not the
8.3M stress case, an actual steady-state request shape), that's the
trigger to revisit — cite this measurement, don't re-guess.
**Net: CPU coloring ships. File a note on the terrain-layer implementation
ticket: use `Image.set_pixel`, not a hand-rolled buffer write (⑥'s 2×
finding), and treat "does the real step-count/size decision ever make 330K
the *small* end rather than the steady case" as the trigger to reopen c1,
not a speculative future pass.**
## (c) Cache-store spec — final, incorporating Jeroen's storage-eviction amendment
Lead interview 1 ruling 2 (verbatim, captured in `lead-interview-1.md`):
*"we still may also want to evict non global level geometry based on time to
save storage for planets the player visits but never goes back to"* →
**staleness-eviction and storage-eviction are distinct axes.** Geometry never
goes stale (D-227 determinism — re-derivable, byte-identical forever), but
sub-global geometry still gets evicted on **time-since-last-visit** as a
storage-budget policy. The global tier alone is keep-always.
This extends my round-1 two-tier design (retention-floor+LRU for geometry,
TTL for sim-state) with a **third eviction mechanism** for sub-global
geometry specifically — it was previously LRU-evict-only (matching
`atlas_window_tile_set.gd`'s existing behavior); it now also gets a
time-since-last-visit sweep. Three mechanisms, three different problems,
kept structurally separate rather than folded into one formula — this
mirrors Dudley's own multiplicative-TTL design for the server tier
(`BASE_TTL[rung] × time_decay × distance_decay`), except my two extra axes
answer different questions (capacity pressure vs. storage-budget thrift) so
I'm keeping them as two independent sweep passes rather than composing them
into one number, for the same legibility reason Dudley cites for keeping his
formula to one tunable per rung.
### Ratified from round 1, unchanged
- Shape: plain Godot `FileAccess` cache dir + index — no `godot-sqlite`
addon, no second SQLite file. Confirmed by lead interview 1: *"both
rejected SQLite in every shape independently"* (Dudley's server-side
answer converged on the same rejection from a different angle: a plain
D-203-shaped resource, not SQLite, for the global tier). **Composition,
not competition**, per the ruling: Dudley's server-side global tier
(~174 MB PNG-encoded, D-203-shaped) and my client-side `FileAccess` dir
are two tiers of the same cache stack, not alternatives to each other.
- Wire content = the already-decided wire encoding (PNG-per-field, T-1179's
winner) written to disk as-is — no re-encoding for storage.
- In-memory tier (the `atlas_window_tile_set.gd`-style Dictionary LRU)
remains the hot path in front of the disk tier, unchanged.
### Three-tier eviction — the concrete spec
**Tier 1 — Global/step-0 (region-spaced) geometry: retention floor, no
sweep at all.** Never touched by either the LRU-capacity sweep or the
time-since-visit sweep. The only way an entry leaves is an explicit
clear/uninstall action. This is the literal mechanism for "always keep the
global level." Matches Dudley's server-side answer (region-spaced, ~638 KB
PNG-encoded/body, ~174 MB across all ~273 bodies server-side) — the client
disk tier for this rung should be sized the same way, since a client that
has actually visited a body already has the same canvas the server cached,
and re-fetching it from the local warm server subprocess after a client-side
eviction would be needlessly wasteful when disk is this cheap.
**Tier 2 — Sub-global geometry (every step below global): two independent
sweep passes, both storage-motivated, distinct from staleness.**
- **(2a) Time-since-last-visit sweep** (Jeroen's new amendment) — a
periodic pass (triggered on body-open + a coarse background timer, never
per-frame) that walks the index and deletes any sub-global entry whose
`last_read_at` is older than a configurable threshold (proposed starting
point: on the order of days-to-weeks of real wall-clock time, tunable —
not a round-2 architecture call, a tuning pass once this ships). This
answers *"the player visited this body once, three sessions ago, and
hasn't been back"* — the entries are still byte-valid (D-227 determinism
means they're never wrong), they're just not worth the disk space for a
body the player has functionally abandoned.
- **(2b) LRU-capacity sweep** (unchanged from round 1) — if total disk usage
for the sub-global tier exceeds a configured budget, evict oldest-touched
entries first, same erase+reinsert-on-touch mechanism
`atlas_window_tile_set.gd` already implements, just backed by the index
file instead of an in-process Dictionary of raw window data.
- These are **two separate triggers checking two separate conditions**
(age-since-visit vs. total-bytes-over-budget), not one merged policy —
keeping them apart means either one can fire independently (a player who
visits many bodies briefly hits 2b before 2a; a player who stays on one
body for a long single session but never returns to old ones hits 2a
before 2b), and each is independently legible/tunable.
**Tier 3 — Sim-state-tagged planes (frozen/flooded, whatever fields the map
time axis ruling lands on — ruled at lead interview 1, ruling 2, as
"current state via TTL-split"): explicit TTL, staleness-motivated,
structurally separate from tiers 1/2.** These entries carry a real
expiry — re-requested as sim time advances, per Jeroen's original hint made
concrete by the ruling. Distinct index field, distinct sweep condition
(`now > written_at + ttl`), never touched by the LRU-capacity or
time-since-visit sweeps (a sim-state entry doesn't get to live longer just
because disk space is available — it goes stale on its own schedule
regardless of capacity pressure).
### Index file schema (concrete)
One manifest per body, `user://atlas_cache/<body_id>/index.dat` (a small
binary or JSON — binary preferred for parse cost at scale, but this is an
implementation-detail choice, not an architecture one), loaded once on
body-open and held in memory as a `Dictionary` for the session — matching
the same "erase+reinsert = move-to-MRU" idiom `atlas_window_tile_set.gd`
already uses, just now also carrying the two extra timestamp/tag fields
tiers 2 and 3 need:
```
IndexEntry {
key: String # same composite key shape as atlas_window_tile_set.gd's
# make_key(), extended with step index:
# "<body_id>:<step>:<center>:<n>:<granularity_v2>"
file_path: String # user://atlas_cache/<body_id>/<key_hash>.png (or
# one file per field, per the wire contract's own
# per-field-PNG framing — Araminta's call, mirrored
# here, not re-decided)
tier: Geometry | SimState
written_at: int # unix time, set once, never updated
last_read_at: int # unix time, updated on every cache hit (drives 2a/2b)
size_bytes: int # drives 2b's budget accounting without a stat() call
sim_ttl: int? # only present when tier == SimState; null/absent
# for Geometry entries (2a/2b apply, 3 never does)
retention_floor: bool # true only for tier-1 (global/step-0) entries;
# short-circuits both sweep passes unconditionally
}
```
Directory layout: one PNG-per-field file per cache entry (mirrors the wire
encoding exactly — no format translation between "on the wire" and "on
disk"), named by a hash of `key` to avoid filesystem-unsafe characters
(the `:`/`,` composite key string itself isn't a safe filename on every
target platform); the index maps the human-legible key to that hash.
### Sweep triggers
- **On body-open:** load the index; run 2a (time-since-visit) immediately —
cheap (a metadata scan, no file I/O beyond the index itself) and this is
the natural moment ("returning to a body") where stale-by-absence entries
are most likely to exist and least likely to be needed again in the next
few seconds.
- **On a coarse background timer** (not per-frame, not even per-step-cross —
proposed on the order of minutes, tunable): run 2b (LRU-capacity) if
total sub-global bytes exceed budget, and sweep tier 3 for expired
sim-state entries. Both are backgroundable (Rayon-queue-adjacent on the
server side; on the client this is a low-priority deferred call, never
blocking a frame or a step-cross — matches premise 1's "smart precache
allowed, never blocking user output" applied to eviction as much as to
fetch).
- **Never per-frame.** All three sweep mechanisms are explicitly excluded
from the render/input loop — this is bookkeeping, not gameplay-adjacent
work, and belongs nowhere near the 16.6 ms budget ⑤ and ⑥ both care about.
### Per-tier budgets
- **Tier 1 (global):** sized to match Dudley's server-side number directly
— ~638 KB/body PNG-encoded, ~174 MB across all ~273 bodies if the client
has visited every body (an upper bound, not a typical player's actual
footprint). No cap needed beyond that — it's the "keep always" tier by
definition, and 174 MB is a trivial disk allocation on any target
platform.
- **Tier 2 (sub-global):** a configurable byte budget (proposed starting
point: same order of magnitude as tier 1, e.g. a few hundred MB to
low-GB range — a tuning-pass number once real play-pattern data exists,
not an architecture call I'm locking here) that 2b enforces directly, with
2a doing most of the practical work of keeping it under budget without
ever hitting the hard cap in normal play (a player who keeps returning to
the same few bodies never triggers 2a for those bodies' entries; a player
who body-hops constantly generates entries 2a will clear out on its own
schedule before 2b's cap becomes the active constraint).
- **Tier 3 (sim-state):** no byte budget needed — TTL alone bounds it, and
these planes are a small fraction of a step canvas's total field set (per
Araminta's round-1 schema, frozen/flooded reads off the existing
`glaciation`/morphology-water-class fields rather than adding new dense
arrays, so tier 3's actual footprint is small regardless).
### What stays true from D-227 regardless of which tier
Every tier is still an evictable cache, never a source of truth — deleting
the entire `user://atlas_cache/` directory at any time, for any reason
(corruption, manual clear, platform storage pressure), changes client
behavior only by causing re-fetches. No tier, no sweep policy, no budget
number in this spec is allowed to become load-bearing for correctness. This
is the same discipline Dudley's server-side tier commits to explicitly in
his round-1 doc, restated here for the client side as it was in round 1.
## (d) Px-per-gridunit band — sent to Dudley + Tyre
See the accompanying `SendMessage`. Summary of the recommendation for
their ladder tables: **1×1 px/gridunit at the deepest step (Tile,
D-243-pinned per Tyre's snap ruling), relaxing toward the ~5×5 fallback only
at the shallower/coarser steps where canvas extent (not spacing) is what's
growing.** Reasoning grounded in ⑥ + ⑤ together: crisper display costs
nothing extra in upload (⑤ showed upload is flat-cheap regardless of pixel
density, it's a function of byte count not "how many real gridunits per
displayed pixel") and colorize cost (⑥) is driven by **canvas cell count**,
which is already bounded by Dudley's viewport-sizing policy independent of
the display ratio — so there's no cost reason to fall back to 5×5 at the
steps where fidelity matters most (the deep, ground-level steps where the
player is closest to visible detail). The 5×5 fallback earns its keep at the
*shallow* steps instead, where canvas extent needs to cover a large ground
area and cell count would otherwise balloon for comparatively little
perceptual gain (players aren't scrutinizing per-metre detail from orbital
view). Full band recommendation in the message.
@@ -0,0 +1,920 @@
---
title: "Body Map Viewer — Troblum Round 2 (Adversarial Pass)"
description: "Stress-test of the round-1 measured claims and cache/governance proposals against real play patterns — findings ranked by severity"
type: workshop
status: active
workshop: body-map-viewer
round: 2
owner: Troblum
decision_refs: [D-166, D-226, D-227, D-243, D-203, D-225, D-192, D-005]
---
# Troblum — Round 2 Adversarial Pass
Read the brief (incl. the MEASURED appendix), `lead-interview-1.md`, `round-1-notes.md`,
all four round-1 position files, and all four `measurements/*.md` docs in full before
writing this. No `{agent}-round2.md` files existed at the time this was written —
I flag anywhere that matters below.
I went into this expecting to find sloppy extrapolation. I didn't find much of
that — the four measurement docs are unusually disciplined about the
MEASURED/ARITHMETIC distinction, and every round-1 agent cites correctly. What
I found instead is more interesting: **two population-scale claims that
were never actually tested at population scale** (they were tested for
*throughput*, not for the *property being claimed*), one arithmetic units
slip, one real governance-purpose gap in the D-226(d) boundary that survives
the letter of the rule, and a cache/eviction design that has never been
pressure-tested against a play session shaped differently than "look at one
step canvas once." Findings below, most severe first.
---
## BLOCKING
### B1. The "273 bodies" hydrology parallel-throughput bench is the SAME body solved 273 times — it proves nothing about cliff rarity across the population, and the workshop is about to file a Phase-4 ruling that leans on "rare" as a population-level property
**The claim as stated:** T-1177's headline: *"all 273 bodies Rayon-parallel ~0.70.8s"*, cited by Dudley (Q1, Q4), and Tyre's cliff ruling explicitly says *"real planetary heightmaps beyond the one body sampled may differ"* but still files "Phase-4 Atlas scope, not deferred" on the strength of a rarity finding whose own caveat says it's measured on "one real body (GJ1c) plus two synthetic gradients."
**What actually breaks it:** I read the bench source directly
(`server/tests/hydrology_equilibrium_bench.rs:233-260`,
`bench_parallel_273_bodies_at_512x256`). It calls `gj1c_512x256()` **once**,
then solves that **identical elevation array** 273 times in a `par_iter`:
```rust
let (elev, sea_level) = gj1c_512x256();
let body_count = 273usize;
let total_basins: usize = (0..body_count)
.into_par_iter()
.map(|_| {
let result = solve(&elev, 512, 256, sea_level, default_climate());
result.basins.len()
})
.sum();
```
This is a **throughput** measurement (can the Rayon pool solve 273 independent
jobs of this size in under a second — yes) wearing a **population-diversity**
measurement's headline ("all 273 bodies"). It answers "is per-body-open
hydrology affordable" (yes, cleanly) — it does **not** answer, and was never
designed to answer, "does cliff carving stay rare across 273 distinct real
terrains." The 273-basin-count sum reported (`total_basins`) is 273× GJ1c's
own 68 basins — a tell, if anyone checks it, that this is one terrain
replayed, not 273 terrains sampled.
Nobody in round 1 mis-states this — Dudley's text says "512×256 (real GJ1c
working grid)" for the single-body cost row and correctly separates it from
the parallel-throughput row, and Tyre's caveat is honest about the *sampling*
limitation. But the round-1 notes and the lead interview both let "all 273
bodies" stand unqualified as a headline in the appendix table, and the cliff
ruling (already ratified by silence for the representation, and provisionally
ruled Phase-4 by Tyre) is exactly the kind of decision where "how often does
this fire across the real population" is load-bearing for the wire-cost
argument that makes Phase-4-not-Phase-5 attractive in the first place ("nearly
free... because it's a mostly-zero field").
**The mandate's own hint turns out to be directly actionable, and cheaper than
advertised.** The brief's Troblum question asks "is there a cheap pre-filing
check (run the solver across the real 273-body population — the bench exists
and takes ~0.8s)?" — the bench that exists does NOT do this, but a real one is
nearly free to build from what's already checked in:
```
$ find wiki/star-systems -iname heightmap.png | wc -l
267
```
267 real heightmap PNGs are already committed (close enough to "273 inhabited
bodies" that the six missing are very unlikely to change the finding
qualitatively). `gj1c_512x256()` already shows exactly the load/downsample
pattern needed (`load_heightmap_png` + `.downsample(512,256)`); a population
survey is: loop the 267 paths, load+downsample+solve each, count
`cliff_edge.iter().filter(|&c| c).count()` per body, report the distribution
(bodies-with-zero-carves vs bodies-with-nonzero, and the max carved-cell count
seen). At ~24ms/body single-threaded (the GJ1c number) or parallelized across
the Rayon pool the same way the throughput bench already demonstrates,
267 REAL bodies solve in well under the same ~0.70.8s ballpark. This is not a
"more research needed, defer the decision" finding — it's "run this one
already-half-built script before filing the cliff D-record," and it costs
minutes, not a round-3.
**Severity and why BLOCKING, not SERIOUS:** the cliff ruling is about to be
filed as a D-record amendment with "rare, cheap to carry" as its central
argument for Phase-4 inclusion. If the real population survey finds even a
double-digit percentage of bodies with genuine carving (plausible — GJ1c's
`tectonic_class`/`hydrosphere` params are one point in a parameter space that
explicitly includes `Volcanic`/high-relief bodies per D-239 §5's morphology
gates, and D-239 §1 explicitly flags `RIVER_THRESHOLD` as eventually
per-body-class-derived, not the fixed 200 this prototype borrowed), the wire-
cost argument doesn't just weaken, the field stops being "mostly zero" and
Araminta's sparse-list encoding choice (near-free at near-zero occupancy)
needs re-costing at whatever the real occupancy rate turns out to be. Filing
now on the untested assumption risks a silent contradiction the moment
someone actually looks at a mountainous body's Atlas map and it should show
gorges that a "rare" assumption undersized the format for.
**What resolves it:** run the population survey (concrete, minutes of work,
harness 90% exists) before the D-record for the cliff ruling is filed. Report
back: (a) fraction of the 267 real bodies with ≥1 carved cell, (b) max carved-
cell count on any single body, (c) whether any body's carve count is large
enough to threaten the "mostly zero" premise Araminta's sparse-list sizing
assumes. If the finding holds (most bodies still zero, a minority nonzero but
bounded), the existing ruling stands and gets a stronger evidence base for
free. If it doesn't hold, better to know before filing than after.
---
### B2. "Cost does not gate any step in the ladder" is true for derivation but silently assumes independent re-derivation at every step — the one architecture question everyone flagged as unmeasured (seed-chaining vs consuming the coarser output) is exactly the assumption every cost number in the brief depends on, and it is explicitly still open
**What breaks it:** Dudley states this cleanly himself (Q3, "One thing I did
NOT measure..."): every ns/cell number in T-1178/T-1154 assumes each step
**re-derives independently from `(seed, position)`**, not that a finer step
consumes a coarser step's already-computed values as input. This is flagged
as "an open data-flow question for round 2," not resolved by anyone, and it
does not appear in any of the four round-1 documents as a *ruled* item — it's
listed in round-1-notes.md's OPEN-FOR-SYNTHESIS item 8, still open at the time
I'm reading this.
Here's why this is BLOCKING rather than a tidy loose end: **the two candidate
architectures have different scaling shapes under step-thrashing** (my
mandate's own named stress scenario), and nobody has priced the one the
outline's own text seems to prefer. Re-read Jeroen's outline verbatim:
*"This at the same time serves as seed information for the deeper cascade."*
That's not "the coarser step happens to also be re-derivable independently" —
it reads as an intentional data-flow claim that the coarser canvas
**functions as seed input** for the next tier. If that's the intended
architecture (not just Dudley's simplifying assumption for benching purposes),
then:
- **Every measured cost number in ①②③ prices the WRONG architecture.** A
step that consumes the coarser tier's output as an input needs that coarser
tier resident/computed first — which either means step-N+1 has a hard
dependency edge on step-N (serializing what's currently modeled as
independent parallel derivation), or it means the "seed information" framing
is loose language for "informs the RNG stream," which is a different and
much smaller claim than "consumes the values."
- **Step-thrashing (my mandate's named scenario) has opposite costs under the
two models.** Independent re-derivation: zooming out then back in
re-derives step-N from scratch (measured: 63.7ms1.8s depending on canvas
size) — cache-miss cost is a flat per-step number. Consuming-coarser-output:
zooming out then back in either (a) re-uses the still-cached coarser output
as an input, making a re-zoom-in CHEAPER than the numbers in this brief
suggest, or (b) if the coarser tier itself got evicted (see B3/S1 below,
storage-eviction is now a real design axis), the finer tier's re-derivation
chain-reacts backward through however many rungs got evicted, which is a
cost story NOBODY has measured and that gets worse, not better, exactly in
the play pattern the mandate asks me to stress (a player who steps
in-out-in-out while exploring, potentially re-triggering upstream rungs).
**This is not a hypothetical distinction — it changes what "measured, not
extrapolated" means for the whole appendix.** If round 2 or round 3 lands on
the consuming-coarser-output model (which reads as closer to what the outline
actually asked for), literally every ①②③④ cost number needs a footnote at
minimum, and possibly a re-bench, because none of them price a dependency
chain.
**Severity:** BLOCKING because it's not a refinement of an accepted
architecture — it's a fork in the architecture itself that the brief's own
Expected Output 3 ("the step ladder... deepest step's canvas policy") cannot
honestly be finalized without resolving. Filing the D-record for "cost does
not gate any step" without this resolved risks the record citing numbers that
don't describe the shipped system.
**What resolves it:** explicit round-2/round-3 ruling: does a finer step's
derivation (a) call `derive_at_metres(seed, position, finer_spacing)`
independently, treating the coarser canvas as *display-continuity input only*
(Dudley's assumption, what's actually been measured), or (b) literally sample/
consume the coarser step's resolved field values as part of its own derivation
(the "seed information for the deeper cascade" reading)? If (b), a fresh cost
pass is needed before the ladder table in Expected Output 3 can cite the
existing numbers as authoritative for that shape.
---
## SERIOUS
### S1. Evict-then-revisit cost spikes are real and un-costed — Jeroen's storage-eviction amendment interacts with the TTL(detail,time,distance) formula in a way nobody has priced end-to-end
**The scenario (my mandate's #2/#5):** Jeroen's lead-interview ruling adds
storage-eviction on time-since-last-visit as a second, independent axis from
staleness-eviction — *"we still may also want to evict non-global-level
geometry based on time to save storage for planets the player visits but
never goes back to."* Good instinct, but nobody has run the number on what a
revisit costs once eviction actually fires.
**The arithmetic (grounded in T-1179's measured derive+encode numbers):** a
single 330K-cell district-rung canvas costs 74.8ms derive + 5.41ms PNG-encode
= **80.2ms** to regenerate server-side (before wire transfer and client
decode/upload, which measurement ④/⑤ show add a further ~4-10ms each). A
player who explored, say, 20 distinct district-rung windows of a body during
one long-ago visit, gets storage-evicted on time-since-visit, and returns:
**20 × 80.2ms ≈ 1.6 seconds of pure server derive+encode cost concentrated
into the moment they re-open that body's Atlas view** — before wire and
client costs are even added. That's not catastrophic, but it's also not
"snappy re-navigation," which is the entire stated purpose of the "always keep
global" instinct this eviction rule is explicitly carved out from. And this is
the OPTIMISTIC case — District rung. A player who was deep-stepping (Block or
Tile rung, viewport-sized canvases) across many locations before abandoning
the body has a much larger number of small entries to re-derive, and nobody
has counted how many viewport-sized canvases a typical "explore a city" session
generates.
**What nobody has specified, and needs to be specified before this ships:**
1. **Does storage-eviction apply per-entry (fine-grained, LRU-by-canvas) or
per-body (coarse, "haven't opened this body's Atlas in N days, drop
everything but global")?** Stig's `FileAccess` design (round 1, §4) says
"two-tier eviction... geometry entries get LRU + a retention floor at
step-0" — that's per-entry LRU, which means a body a player visits
*occasionally* (not "never goes back to," but not "frequently" either)
could have some of its sub-global entries evicted and others not,
producing a **partial-revisit cost** that's neither the full 20-canvas
number above nor zero — an unmeasured middle case that's actually the
MOST common real pattern (players revisit systems they've been to before,
irregularly, not never/always).
2. **Does the TTL formula's `distance_decay` term fight the storage-eviction
rule?** Dudley's TTL formula (`ttl = BASE_TTL[rung] × time_decay(age) ×
distance_decay(distance_from_focus)`) already decays entries far from
current focus faster. If storage-eviction is a *second*, independently-
timed sweep on top of that TTL, a body the player is CURRENTLY navigating
(high focus, TTL should be long) could still hit a storage-eviction sweep
timed off calendar/session time rather than in-session focus, evicting an
entry the TTL formula would have kept. Nobody has specified which axis
wins, or whether they're the same mechanism wearing two names (round-1
notes doesn't resolve this either — it's absent from the OPEN-FOR-SYNTHESIS
list entirely, which itself is a gap: this should have been item 11).
**Severity:** SERIOUS, not BLOCKING, because the base numbers (80ms/canvas
regen) are cheap enough that even an unoptimized worst case doesn't "catch
fire" — but it's serious because the FEEL goal ("atlas navigation snappy after
first calc," Dudley's own words) is explicitly what this mechanism could
undermine for exactly the play pattern (occasional revisits) that's most
common, and nobody has written down the eviction granularity or the two-axis
interaction rule.
**What resolves it:** round-2/3 synthesis needs to state explicitly: (a)
eviction granularity (per-canvas vs per-body), (b) which axis (TTL distance-
decay vs storage-eviction time-since-visit) is authoritative when they
disagree, (c) a worst-case revisit number using whatever granularity is
chosen, computed the way I did above but for the actual chosen unit.
---
### S2. The courses-inclusive rate is measured ONCE, at light density (18 courses), on ONE body — every step-thrashing/deep-pan cost projection implicitly assumes this generalizes, and the workshop's own document says not to generalize it
**What the measurement actually licenses, read carefully:** T-1178's own text
is explicit and correct about the limits of what it found — I want to confirm
this is NOT itself a finding of extrapolation (the document is honest about
its own scope), but flag that the workshop's *use* of the number downstream
risks over-generalizing what the document itself carefully scoped.
The 195.0 ns/cell "courses-inclusive, real density" number comes from **one**
window on **one** body (GJ1c, district `(7520, -2932)`, 18 courses in a
331,776-cell window) — chosen specifically because it's the densest course
window anyone happened to measure. The document's own H3 finding says the
headline table's square-path numbers are courses-inclusive but SPARSE (3-10
courses), and explicitly warns: *"The courses-inclusive rate at REAL
production course density is covered only by Cross-check 1... cite that
number, not the headline table, for a courses-representative rate."*
That's good discipline — but it means the entire ladder's cost story for a
**river-delta body, a body with many parallel drainage channels, or a densely
riverine biome** (all real terrain classes D-239's morphology gates
explicitly support — BraidedDelta, MeanderReach, AlluvialPlain families) is
projected from **one 18-course sample on one body**, not from a density
distribution across real terrain types. 18 courses in a 331K-cell window is
"nearly 2× the synthetic body's density" per the document's own framing — but
"2× the driest measured case" is not the same claim as "representative of the
densest real case." A body whose morphology leans heavily toward
BraidedDelta/MeanderReach (the exact families D-239 §5 names as real,
selectable outcomes) could plausibly carry courses at meaningfully higher
density than GJ1c's 18-in-331K, especially at the deep Tile/Block rungs where
a dense river network's tributaries are all individually resolvable (the
deep-step 83K bench explicitly EXCLUDES courses entirely, per H2 — so the one
number closest to "the actual deepest, most course-dense scenario" is the one
number this whole measurement set has zero data on).
**Why this matters for step-thrashing specifically (my mandate's named
scenario):** a player panning rapidly around a river delta at Block/Tile
spacing generates many step canvases in quick succession, each paying the
per-cell course cost. The <5% bound is well-established at District spacing
and light-to-moderate density — it has never been checked at Block/Tile
spacing (where course geometry is proportionally a larger fraction of a much
smaller viewport) or at delta-class density (where course COUNT, not just
per-course cost, could be several multiples of GJ1c's 18).
**Severity:** SERIOUS not BLOCKING. The <5% bound has enough headroom
(measured against a ~5ms District-cap baseline) that even a 3-4x density
multiplier at a river-dense body likely stays affordable — this isn't a
"computer catches fire" risk. But it is a real, named gap in the "measured
not extrapolated" claim the whole batch prides itself on, specifically at the
one spot (deep-step, high-density river terrain) where the excluded cost is
least likely to stay proportionally small.
**What resolves it:** one additional bench — deep-step (83K-cell, Block or
Tile spacing) window on a body selected for high course density (a
BraidedDelta or MeanderReach-dominant body, if the catalog can be queried for
morphology-family distribution; failing that, a synthetic river network
authored to be denser than GJ1c's). Cheap to add given the harness already
exists; closes the one density/spacing combination the current appendix has
literally zero coverage of.
---
### S3. The D-226(d) whole-body prohibition survives the LETTER at every single step, but the client-side cache accumulation mechanism has no structural ceiling — the purpose of the rule (no metre-resolution whole-body derivation) is defended only by "nobody will pan that much," not by construction
**The stress test my mandate asked for, run concretely:** Dudley and Tyre's
independently-argued convergence on viewport-sized canvases is correct and I
have no finding against the SERVER-side policy — it holds cleanly, and the
17ms/83K-cell number genuinely is never a whole-body derivation on any single
request. That part clears.
But the mandate specifically asks whether "pan-assembled coverage over time
amounting to whole-body at fine spacing" is a real boundary risk given
client-side cache accumulation (Stig's `FileAccess` store, premise 9). Here's
the number: covering GJ1c's own 512×256-district working-grid extent
(1,048.576 km × 524.288 km — the equirectangular working-grid footprint, a
reasonable proxy for body coverage) with deep-step viewport tiles (216m ×
384m, the measured 82,944-cell shape) requires **~6.6 million tiles**, at
~156 KB PNG-encoded each (interpolated from T-1179's per-cell PNG rate),
totaling **~985 GB** on disk to fully assemble one body at 1m spacing via
client-cached viewport canvases.
That number is obviously never going to happen by accident in normal play —
which is exactly why I'm calling this SERIOUS, not BLOCKING: no player is
going to pan a Tile-spacing viewport across 6.6 million distinct windows.
**But "obviously not by accident" is a practical-infeasibility argument, not a
structural one**, and the round-1 documents (Dudley's and Tyre's both) argue
the viewport-sizing rule as a *governance necessity*, i.e., as the thing that
keeps the design "legal by construction." It is legal by construction
**server-side** (no single request ever asks for whole-body coverage) — it is
legal only by *practical improbability* **client-side**, once a disk-backed,
retain-forever-until-evicted cache is added on top. If a determined player (or
a QA/agent harness doing exactly the kind of automated systematic sweep
D-226(4)'s `AtlasAgentInterface` explicitly builds for) methodically panned a
body at Tile spacing to completeness — slow, but not physically prevented by
any mechanism in this design — the client's own disk cache would, over time,
assemble the exact near-whole-body metre-resolution artifact D-226(d) exists
to forbid. It would just be assembled as N discrete files rather than one
canonical canvas, which is a difference of *packaging*, not of *information
content* — and the rule's stated purpose (avoid a whole-body metre-resolution
planetary map layer existing) is about information content, not file count.
**This is exactly the gap the mandate asked me to name: does accumulation
violate the rule's PURPOSE even if not its letter?** My answer: yes, in
principle, though the practical risk is low given the tile count required.
The more concrete risk isn't "a player does this for fun" — it's the
**agent-navigable QA channel D-226 item (4) already built** (`AtlasAgentInterface`,
`observe`/`act`, described explicitly as turning "human-eyeball review into an
agent-automatable QA sweep across the whole Reach"). An automated sweep is
precisely the actor most likely to do a systematic, exhaustive pan — and if
that sweep's client-side cache is retained (the "always keep global" +
LRU-with-retention-floor design does NOT explicitly exclude an automated
client from accumulating sub-global entries without bound), a QA run against
one body at fine spacing could, over enough wall-clock time, produce the
forbidden artifact as an unintended side effect of testing, sitting quietly
in `user://atlas_cache/`.
**Severity:** SERIOUS. Doesn't block shipping the ladder (the server-side
policy is sound and the practical risk from normal play is genuinely low),
but it's a real gap between letter and purpose that a workshop explicitly
concerned with "not just cost, a governance boundary" (red flag 3's own
framing) should close on paper, not leave to improbability.
**What resolves it:** state explicitly, as part of the D-226(d) amendment
text, that the **prohibition is a per-request/per-derivation constraint, not
an aggregate-storage constraint** — and separately, add an explicit cap on
client-side cache retention at deep rungs (a maximum resident tile count or
disk quota per body at Tile/Block spacing, independent of the "retention
floor at step-0" rule that already exists for the global tier). This turns
the current "improbable in practice" defense into an actual structural
ceiling, which is the same discipline the rest of this workshop already
applies everywhere else (measured numbers, not vibes).
---
### S4. Sim-state determinism for frozen/flooded is real (D-228 already answers "what serves them"), but the TTL-split's staleness boundary depends on a "phase" granularity nobody in this workshop has named, and the water-height mechanism is NOT wired to the map yet
**What I verified clears (good news first, per my mandate's instruction to
report clean findings explicitly):** the "are frozen/flooded actually
deterministic per (seed, sim-time)?" half of my mandate's question 5 checks
out cleanly. D-228 (already-filed, not a round-1 invention) states the
mechanism precisely: water-height is *"a region property computed once per
phase (not per tile, not per frame)... a pure function... recomputed on phase
change"* — a seasonal term phased continuously by latitude plus an optional
tidal term. This is genuinely deterministic given `(seed, region, phase)`
no RNG, no accumulation, matches D-010. **Cleared: sim-state components for
water-height are deterministic per the existing, already-filed D-228 model —
this is not a new risk the body-map-viewer workshop introduces.**
**What is NOT cleared, and is a real gap for the TTL-split ruling (Jeroen's
lead-interview #2):** "recomputed on phase change" begs the question this
workshop needs an answer to and hasn't produced one: **what triggers a phase
change, and at what granularity does the client's TTL need to re-request to
stay non-stale?** D-228's own text names two clock terms (seasonal — a "year
clock," continuous by latitude; tidal — a lunar/day clock, only present with
a moon) but "computed once per phase" doesn't say how long a phase lasts in
real sim-time, and nothing in the four round-1 documents or the measured
appendix touches this. Jeroen's own TTL hint ("maybe shorter ttl on the
climate sim state components") is a directional instinct, not a number — and
it can't be turned into one without knowing the phase cadence.
**The concrete failure mode this produces:** if a phase is short (say, a tidal
term on a body with a fast-orbiting moon — plausible given the system catalog
includes many-moon systems) and the client's TTL is tuned assuming a
seasonal-length phase, a player who holds a view over a coastal/tidal-flat
gridunit across a phase boundary sees **stale flooded/dry state** rendered
past its validity window — not a crash, not data corruption, just a quietly
wrong map for however long the TTL overshoots the real phase cadence. The
inverse failure (TTL too short) re-requests a sim-state plane that hasn't
actually changed, which is wasted wire/derive cost but not a correctness bug
— asymmetric risk, meaning erring toward "too short" is the safe default, but
nobody has stated that as a design rule either.
**Severity:** SERIOUS, not BLOCKING — the determinism substrate is sound
(the good-news half above), and this is a tuning-parameter gap, not an
architecture gap. But it's a real gap: Jeroen explicitly ruled the TTL-split
model as the working answer at lead-interview-1, and the model as ruled has an
unfilled variable (phase cadence) that determines whether it actually holds
in play.
**What resolves it:** name the phase cadence (or the range of cadences across
body types — tidal-locked/fast-moon bodies vs moonless/seasonal-only bodies
plausibly need different `BASE_TTL` values for the sim-state plane, similar
to how Dudley's own `BASE_TTL[rung]` is already per-rung) before finalizing
the TTL formula's sim-state branch. This is a Dudley/Araminta follow-up, not
something I can resolve from the measured appendix — flagging it as an
unanswered input the round-2 synthesis needs, not asking for a re-bench.
---
### S5. The disk-backed client cache has no schema/version field on cached entries — the one place D-192's "client+server always co-ship, no version skew" guarantee genuinely does NOT hold
**What I checked, and why this is a real gap not a nitpick:** D-192 (already
filed, confirmed by direct read) explicitly drops the protocol version
handshake on the rationale that *"our actual deployment is a subprocess: the
Godot client launches the Rust server it was built with. They are always in
sync at runtime."* That's true for the LIVE wire protocol, and it means my
mandate's item 6 ("version skew between client and server during the
transition") is mostly a non-issue for the tagged-envelope migration itself
— there is no live-network deployment where an old client talks to a new
server.
**But Stig's round-1 disk-cache design (§4, ratified by silence at
lead-interview-1 as part of "cache composition... compose, don't compete")
breaks exactly this guarantee, and nobody has named it.** A disk-backed,
self-cleaning, `user://atlas_cache/`-resident cache **persists across game
updates** by construction — that's the entire point of a persistent cache
(survive process restart, survive session boundaries). D-192's "always in
sync at runtime" argument is about the LIVE client-server pair in one running
process; it says nothing about a cache file written by version N of the game
being read back by version N+1 after a patch changes the wire schema (a new
dense field, a changed enum discriminant range, a bumped `SCHEMA_VERSION`-
style change to the payload shape itself). I checked Stig's cache design text
directly (`stig-round1.md` §4) for any versioning discipline on cached
entries — **there is none**: the index schema he specifies is `{written_at,
last_read_at, kind: geometry|sim_state, size_bytes}`, with no field naming
which wire-schema version produced the cached bytes.
**The concrete failure mode:** a game update changes `DistrictWindowLayer`'s
field set (adds the `cliffs` sparse list this very workshop is about to
introduce, or bumps an enum's discriminant range per T-1150's own precedent
of "unknown values fall back, never trusted from the wire"). A player who has
a warm disk cache from before the update opens the Atlas. If the client
blindly deserializes the stale-schema cached bytes as if they were the new
schema (the most likely naive implementation, since the cache's whole selling
point is "skip the fetch, decode from disk"), this is either a hard decode
error (best case — the mismatch is caught) or, worse, a **silent
misinterpretation** of old bytes as new fields (worst case — exactly the
class of bug D-225's own 2026-06-12 amendment was written to prevent for the
LIVE wire, extended here to the DISK format, which nobody has extended the
same discipline to).
**Severity:** SERIOUS, not BLOCKING — this is a real, fixable gap, not an
architecture-breaking one, and the fix is cheap (one extra field). But it's
exactly the kind of "invisible until it's wrong" decision Araminta's own
round-1 opening line warned about, applied to a part of the design that
genuinely does cross a version boundary D-192 was written to assume away
everywhere else.
**What resolves it:** add a schema/version tag to every cached entry (a
`generator_sha`-style stamp, cheap to compute, or simply the game's own
`project.yaml` version string) at write time, and a check at read time —
mismatch = treat as cache miss, re-fetch, don't attempt to decode. This is a
small addition to Stig's already-designed index shape, not a redesign, and it
should land in the same round-2/3 pass that finalizes the cache store rather
than as a later patch once someone hits a stale-cache decode bug in the wild.
---
## NOTE
### N1. Dudley's "~174 MB" global-tier figure is decimal-MB (SI, 1000×1000), not MiB (1024×1024) — internally consistent but worth flagging before it gets budgeted against actual RAM/disk allocation numbers
Checked the arithmetic directly: Dudley's own measured per-body numbers
(1,990,693 bytes raw / 638,382 bytes PNG-encoded at 330K cells, T-1179) times
273 bodies:
- Using MiB (1024²): 273 × 638,382 / 1024² ≈ **166.2 MiB**
- Using decimal MB (1000²): 273 × 638,382 / 1000² ≈ **174.3 MB** ← matches
Dudley's stated "~174 MB" exactly
Not an error — the arithmetic is internally consistent under the decimal-MB
convention, and it happens to also match red flag 2's own pre-measurement
"~440 MB at 5×5" estimate reasonably (both used the same convention, so they
compare correctly to each other). Flagging only because the moment this
number gets used to size an actual memory allocation or `du`-reported disk
budget, the ~8MB gap (166 vs 174) between conventions is exactly the kind of
silent unit-drift that causes a "why doesn't the number match what `du -h`
shows" ticket three months from now. Recommend the filed D-record state the
convention explicitly (SI decimal MB) the first time this number appears.
**No action needed beyond a one-line convention note in the filed record** —
this is the "clean bill" item my mandate asked me to report explicitly when a
claimed number checks out. The ~174 MB figure IS what Dudley's own measured
inputs produce, under a stated convention.
### N2. Region-as-step-0 is still unconfirmed by Araminta (round-1-notes item 9) — I have no `araminta-round2.md` to check against, flagging that this is still open at time of writing
Per the mandate's instruction to note what I didn't see: no `{agent}-round2.md`
files existed in the workshop directory at the time I read it. Tyre's §1e
flagged "Region likely IS the global/step-0 rung, worth confirming" as
carried into round 2. If this is still unconfirmed when synthesis closes, it
directly affects two of my own findings above — S3's viewport-tile-count
arithmetic assumed the deep end of the ladder starts from a District-scale
"global" tier's working-grid extent as the body-coverage proxy, which is a
reasonable stand-in either way, but the exact numbers would shift slightly if
Region turns out not to be step-0. Not a finding against anyone's work, just
noting the input I was missing.
### N3. Cleared: the courses-cost bound itself (<5% at District-cap, the one number actually stress-tested at real density) holds up
Distinct from S2 above (which is about the SCOPE of what's been measured, not
its accuracy) — I directly re-checked the one courses-inclusive-at-real-
density number that exists (Cross-check 1, T-1178): 195.0 ns/cell vs the
synthetic fixture's 192.0 ns/cell is a 1.56% delta, correctly described as
"within 2%." The arithmetic is right, and the <5% District-cap bound
(`+0.090.21ms` against `~5ms`) is 1.84.2%, also correctly inside the stated
threshold. No finding here — citing this as the "clean bill" companion to S2,
since S2 is about what ISN'T covered, not about the one number that is.
---
## Summary table
| # | Severity | Finding | Resolution cost |
|---|---|---|---|
| B1 | BLOCKING | "273 bodies" hydrology bench is 1 body × 273 — no real population-scale cliff-rarity survey exists | Minutes — harness 90% exists, 267 real heightmaps on disk |
| B2 | BLOCKING | Seed-chaining data-flow (independent re-derive vs consume-coarser-output) is unresolved and changes every cost number's validity | Architecture ruling, then possibly a re-bench pass |
| S1 | SERIOUS | Storage-eviction × TTL-distance-decay interaction unspecified; evict-then-revisit costs ~80ms/canvas × N canvases, uncosted at realistic N | Specify eviction granularity + axis precedence |
| S2 | SERIOUS | Courses-inclusive rate measured once, light-ish density, one body — zero coverage at deep-step + high-density-river combination | One more bench, harness exists |
| S3 | SERIOUS | D-226(d) holds by letter at every step but not by purpose against unbounded client-cache accumulation (~985GB to fully tile one body — improbable but not structurally prevented) | Add explicit per-body deep-rung cache cap; state prohibition as per-request not aggregate |
| S4 | SERIOUS | Sim-state determinism substrate is sound (CLEARED) but phase-cadence for TTL sizing is an unfilled variable | Name phase cadence(s) per body class |
| S5 | SERIOUS | Disk cache has no schema/version tag — the one place D-192's "always in sync" guarantee doesn't hold (cache persists across updates) | One field addition to Stig's index schema |
| N1 | NOTE | ~174MB figure is decimal-MB not MiB — internally consistent, state convention explicitly | One-line note in filed record |
| N2 | NOTE | Region-as-step-0 still unconfirmed at time of writing (no round-2 files existed yet) | Informational |
| N3 | NOTE | Courses <5% bound itself is accurate where measured (cleared) | None — clean bill |
Top-line read for the lead interview: nothing here says "computer catches
fire" or "the architecture is wrong." Everything here says "two claims are
resting on measurements that don't cover what they're cited for" (B1, B2) and
"three real interaction effects between mechanisms that were each individually
well-designed haven't been costed together" (S1, S3, S5). All five of the
serious-or-worse findings have cheap, concrete resolutions — none require
walking back a round-1 position, and B1 in particular is likely to make the
cliff ruling's evidence base stronger, not force a reversal, once run.
---
# ADDENDUM (post-round-2-files)
All four round-2 files (`dudley-round2.md`, `araminta-round2.md`,
`stig-round2.md`, `tyre-round2.md`) now exist and have been read in full.
Short pass per the coordinator's four items. Verifying my own findings
against them, not re-litigating what's already settled.
## 1. Finding B2 (seed-chaining fork) — RESOLVED. Dudley's ruling closes it cleanly, and I verified the mechanism against source, not just his prose.
**Dudley's round-2 §(b) ruling: independent re-derivation from `(seed,
position)`; a finer step may call a coarser rung's derivation FUNCTION, never
read a cached RESPONSE.** I checked this against the actual code rather than
taking the argument on faith, because "this closes the cost-shape concern" is
exactly the kind of claim that needs verifying, not just reading.
Read `server/src/atlas/district_profile.rs:1705-1732` directly —
`derive_at_metres` (the function every T-1178/T-1154 bench calls) computes
`region_baseline_c` via `region_profile::region_baseline_at_district(...)`
**inline, every call, with the comment stating explicitly: "No pre-built
cache here — the on-demand path derives the four surrounding region
baselines directly. Pure, deterministic, cheap."** This is not a
generalization from a different code path — it is the *exact* function every
measured ns/cell number in the appendix already exercises. So Dudley's claim
that "my own cost numbers... are validated as the correct model" isn't an
assertion resting on the ruling being correct in principle — the nested-call
cost is *already baked into every measured number*, because the benchmarked
function already does this. **This fully closes my concern: the ①②③ cost
basis is validated, not just argued to be validated.**
**What remains for Jeroen, stated precisely, per the coordinator's ask:**
Jeroen's own outline sentence — *"This at the same time serves as seed
information for the deeper cascade"* — is genuinely ambiguous between two
readings, and interview 2 is where that ambiguity needs to close, not before.
Dudley's ruling asserts his function-composition reading is what Jeroen
*meant*, grounded in the fact that this pattern already ships
(region-baseline-feeds-district). That's a strong circumstantial argument
(it's the only reading consistent with existing shipped code and with D-227),
but it is still an inference about authorial intent, not a confirmation from
Jeroen himself. **The interview-2 item, precisely stated: ask Jeroen to
confirm "seed information for the deeper cascade" meant "the finer
derivation calls the coarser rung's derivation function as a sub-computation,
the way region-baseline-feeds-district already works today" — not "the finer
step consumes the coarser step's already-computed response bytes as a
literal input."** If he confirms, B2 is fully closed with authorial
agreement, not just Dudley's reading of the code precedent. If he meant the
second reading, the cost model needs the re-bench I originally flagged — but
given the mechanism is architecturally forced by D-227 (a response-consuming
model would require solving the cache-dependency-chain problem Dudley's
argument-point 1 lays out, which nothing in the outline text asks for), I
think this is very likely to confirm cleanly. Downgrading this from BLOCKING
to a one-line interview-2 confirmation item, not a re-open.
## 2. The envelope design — attacked directly, one real angle found, one attack that didn't pan out (reporting both, per my mandate).
**Attack 1 (didn't pan out — reporting the clean bill): does `StepCanvasRequest`
risk a structural collision with legacy `AtlasLayerRequest` in the `ShapeProbe`
demux, given `AtlasLayerRequest` has no boolean discriminator (unlike
`star_map`/`city_names`/`browse`)?** Checked directly against
`server/src/bridge/mod.rs:96-152`. `AtlasLayerRequest`'s shape-identity in
`ShapeProbe` is `body_id.is_some() && up_to.is_some()` (`up_to` is a
*required*, non-`#[serde(default)]` field on the struct itself, confirmed by
reading `layer_proxy.rs:510-512`). Dudley's `StepCanvasRequest` struct
carries `body_id` but never `up_to` — so no minimal well-formed
`StepCanvasRequest` payload can satisfy `AtlasLayerRequest`'s two-field
identity check, regardless of try-order placement. Dudley's text also
explicitly extends `ShapeProbe` with a `step_canvas` field and folds it into
the mutual-exclusivity sum — so a payload carrying both `step_canvas` and
`up_to`+`body_id` would be correctly rejected as ambiguous by the existing H1
defensive check, not silently misrouted. **Cleared: the demux design holds
under a targeted collision attack, including the one case (legacy shape has
no discriminator) that looked like the most likely soft spot.**
**Attack 2 (the "no coexistence needed" claim, stress-tested against
version-skew, not just live-wire skew — this is where a real gap was, and
it's already been found and closed by the workshop before I could report
it):** I went looking for exactly the gap I flagged in my original S5
(disk cache persists across a version boundary D-192's "always in sync"
argument doesn't cover) and found `tyre-round2.md` §(a.9) already cites it
verbatim as "Troblum S5" with a complete, record-ready fix (schema/version
tag on every cache entry, tag-mismatch = cache miss, never a decode attempt).
Verified the fix is structurally sound: it correctly scopes the claim (D-192's
live-wire reasoning stands unchanged; only the persistent-cache boundary needs
the new tag) and correctly treats a version mismatch as "just another
eviction case" consistent with D-227's existing discipline, not a new failure
mode. **No further attack needed here — this is closed, and closed well.**
**Attack 3 (genuinely new, not previously flagged by me or found addressed in
round 2): the migration's own sequencing claim — "the server-side sixth-shape
addition and the client-side viewer rewrite don't have to land in the same
PR" — is correct for the LIVE wire (D-192 co-ship holds at every commit
because client+server build together), but says nothing about the
**disk cache accumulated during the OLD viewer's lifetime being read by the
NEW viewer** once the cutover lands.** Concretely: a player has an existing
`user://atlas_cache/` populated entirely by the retiring `AtlasWindowViewer`
(old `district_window`-keyed entries, old key shape
`make_key()` without a step index per Stig's round-1 text). The new stepped
viewer ships, checks the cache directory, and — assuming Stig's `FileAccess`
index is keyed by the *new* composite key shape
(`body_id:step:center:n:granularity_v2`, per his round-2 `IndexEntry.key`
spec) — simply won't find any hits against old-format keys. This isn't a
decode-corruption risk the way S5 was (different key shapes don't collide,
they just miss), so it's lower severity than S5 was — but it does mean the
cutover moment is a **guaranteed 100% cache-cold start** for every player
with pre-existing Atlas usage, not a graceful degrade. Nobody has stated
whether the old `user://atlas_cache/` directory (if the legacy viewer even
had a disk cache — checking Stig's round-1 text, the *old* `AtlasViewer`
only had the in-memory `atlas_window_tile_set.gd` Dictionary LRU, no disk
tier at all, since the disk-backed store is this workshop's own new
proposal) gets cleared, ignored, or orphaned on cutover. **This is a NOTE,
not a SERIOUS finding, once traced fully** — because the legacy viewer never
had a disk cache to begin with (confirmed: Stig's round-1 §1 describes only
`atlas_window_tile_set.gd`'s in-memory Dictionary as the pre-existing cache),
so there's no stale-format directory to orphan; the new disk cache starts
genuinely empty on first use regardless of cutover timing. Downgrading this
from a finding to a **clean bill**: the "no coexistence needed" framing
holds for the disk cache specifically because there was no prior disk cache
to migrate away from. Flagging only so the implementation ticket doesn't
need to write dead migration code for a cache tier that never existed.
## 3. Ladder tables — Option B's zero-asterisk claim: mostly holds, one small asterisk found inside it; D-226(d) letter-compliance and the Region→District seam argument both check out.
**The zero-asterisk claim, checked row by row against what was ACTUALLY
measured this session vs. inherited from an earlier baseline document.**
Option B's five rows: Region (MEASURED, T-1178's 8.3M-cell District-spacing
row reused — see caveat below), District (MEASURED, T-1178), Quarter
(same-band), Block (MEASURED, T-1154's 128m sweep), voxel (MEASURED, T-1154's
83K-cell deep-step bench). Four of five are genuinely, directly measured
THIS session. **The Quarter row is the one soft spot**: T-1154's own text
frames Quarter's per-cell rate as "the existing... Quarter (1.823 µs/cell)
row[s] already in `atlas-zoom-ladder-t1143.md` §7" — i.e., Quarter's per-cell
rate is inherited from an **earlier session's baseline document**, not
re-measured fresh this workshop. T-1154 confirms Block/Tile land in the
*same band* as that inherited District/Quarter baseline, which is a real and
valid cross-validation — but it means Quarter was never put through this
session's own 8.3M-cell parallel sweep the way District and Block were
(checked directly: T-1178's 8.3M square-canvas row is explicitly District
spacing only, `t1178-t1154-derive-bench.md:148`). **This is a small
asterisk, not a real problem** — the same-band confirmation mechanism (flat
~190-220 ns/cell parallel rate holding across every rung this session DID
test at every size) makes it very likely Quarter holds too, and Dudley's own
table already marks it "~1,827 ms (T-1154 same-band confirmation)" rather
than a flat unqualified number, so the workshop's own documentation is
honest about this — I just want it named explicitly as "one row is
same-band-inferred, not this-session re-run at 8.3M," since "zero asterisks"
as a literal claim slightly overstates it. Doesn't change the recommendation
(Option B is still clearly the best-evidenced skeleton) — this is a
precision note, not a reversal.
**D-226(d) letter-compliance at each skeleton's deepest step:** checked all
three options (A/B/C) — every one bottoms out at voxel (1 m), display-ratio-
sized (216×384 m, 82,944 cells), never the fixed 3840×2160/8.3M-cell
canonical shape. This holds the D-226(d) letter identically across all three
skeletons — the deepest-step canvas policy doesn't vary by which skeleton
wins, only the *path* to reach it (how many intermediate steps) varies. No
skeleton-specific compliance risk found; this was already the corrected
convention Dudley applied uniformly per his own note in §(c) ("noted
consistently in all three tables below, not just this one").
**The Region→District seam argument:** Dudley's claim that the ÷100 jump at
step 0→1 "matters for serving... lines up with a real architectural
boundary" (the global/sub-global cache tier split) — checked this against
the actual cache-tier spec (both his §(d) and Stig's round-2 §(c)): yes, Tier
1 (global, keep-always) is Region-spaced exactly, and Tier 2 (storage-
evictable) starts at District. The architectural seam and the display-factor
seam are the same boundary by construction, not a coincidence — this holds
up. **Cleared.**
## 4. TTL-split planes (araminta-round2.md §a) and three-tier cache spec (stig-round2.md §c) against my S1/S5 — both specs now address the concerns; S1 substantially resolved, S5 fully resolved (see item 2 above, already covered by tyre-round2.md §a.9).
**S1 (evict-then-revisit cost spike, TTL-distance-decay vs storage-eviction
precedence unspecified):** Dudley's round-2 §(d) resolves the precedence
question by **eliminating the ambiguity structurally**, not by picking a
winner — his revised formula drops `distance_decay` entirely
(`evict_if: time_since_last_visit(entry) > STORAGE_TTL[rung]`), reasoning
that distance-from-focus is redundant with time-since-visit for a
storage-thrift purpose. This closes the "which axis wins" half of S1
cleanly — there's only one axis left for storage-eviction, so there's
nothing to arbitrate between. **Stig's round-2 §(c) independently confirms
the same simplification** (his Tier 2 spec: "(2a) time-since-last-visit
sweep" + "(2b) LRU-capacity sweep," two independent triggers, no distance
term at all) — two independently-designed specs converging on dropping
`distance_decay` is good evidence the simplification is correct, not just
convenient.
**What S1 asked for that's still not fully delivered: the actual revisit-cost
number.** I computed ~80ms/canvas (74.8ms derive + 5.41ms PNG-encode,
T-1179) × N canvases for a realistic revisit scenario — neither round-2 cache
spec states an expected N (how many canvases a typical explored-then-
abandoned body accumulates) or runs that arithmetic explicitly. Stig's spec
does now name eviction granularity precisely (per-entry, the composite key),
which was the other open half of S1 — so the mechanism is fully specified,
just not the end-to-end cost consequence of a real revisit. **Downgrading
S1 from SERIOUS to NOTE**: the mechanism gaps (granularity, axis precedence)
that made this SERIOUS are now closed; what's left is a tuning-pass number
(expected revisit cost under realistic N), which both specs correctly treat
as implementation-time tuning rather than an architecture question — Stig's
own text explicitly defers the STORAGE_TTL/budget constants as "a tuning
pass once real play-pattern data exists, not an architecture call I'm
locking here." That's the right call; I'd only ask that the ticket plan
name "measure actual revisit-pattern cost against a real play session" as a
post-ship validation step, not a pre-filing blocker.
**S5 (disk cache schema/version tag):** already covered in item 2 above —
`tyre-round2.md` §(a.9) resolves this completely and correctly, citing my
finding by name. Checked Stig's round-2 `IndexEntry` schema
(`stig-round2.md:206-223`) to confirm it does NOT yet carry the tag itself
(it doesn't — the schema shown is pre-amendment) — but Tyre's text is
explicit that "it lands in Stig's `FileAccess` spec at implementation," i.e.
this is correctly sequenced as a record-level requirement now, schema
addition at implementation, not a gap in Stig's document. **Fully resolved.**
## Summary of the addendum
| Original finding | Status after round 2 |
|---|---|
| B1 (population-scale cliff survey never run) | **STILL OPEN** — tyre-round2.md §(a.7) correctly gates the sparse-list cost claim on it running, but it has not been run yet (checked: no new measurement doc, Dudley's round-2 text doesn't mention it). Still the one thing I'd block filing on. |
| B2 (seed-chaining fork) | **RESOLVED**, verified against source (`district_profile.rs:1705-1732` — the nested-call pattern is already what every measured ns/cell number exercises). One precise interview-2 confirmation item remains (does Jeroen's phrasing match this reading), stated above — downgraded to NOTE-level follow-up, not a blocker. |
| S1 (evict-then-revisit spike, axis precedence) | **Mechanism fully resolved** (both specs converge on dropping distance_decay, granularity now per-entry). Downgraded to NOTE: the actual end-to-end revisit-cost number is still unstated, correctly deferred as a tuning-pass/post-ship-validation item by both specs. |
| S2 (courses density coverage gap) | **STILL OPEN** — not addressed in any round-2 document. Deep-step + high-river-density combination remains unmeasured. |
| S3 (D-226(d) letter vs purpose, cache accumulation) | **RESOLVED** — tyre-round2.md §(a.8), same framing, same risk actor (`AtlasAgentInterface`), concrete structural fix (per-body deep-rung cache cap). |
| S4 (sim-state phase cadence unspecified) | **Not directly addressed by name in round 2** — araminta-round2.md §(a) does much of the adjacent work (TTL-split field assignment, static-vs-sim-state reasoning per field) but does not name a phase-cadence number or range. Still open, lower priority than B1/S2. |
| S5 (disk cache version tag) | **RESOLVED** — tyre-round2.md §(a.9), complete and correct fix. |
| N1 (decimal-MB units) | Unchanged, still just a note-in-record-text item. |
| New (envelope demux collision attack) | **CLEARED** — attacked directly, design holds. |
| New (cache-format cutover / stale-key orphaning) | **CLEARED on inspection** — no prior disk cache existed to orphan; downgraded from a worry to a documented non-issue. |
| New (Option B's "zero asterisks" has one small inherited-baseline asterisk on the Quarter row) | **NEW NOTE** — Quarter's rate is same-band-confirmed, not this-session-remeasured at 8.3M; doesn't change the recommendation. |
**Net: of the 7 original findings, 4 are now resolved (B2, S3, S5, and S1's
mechanism half), 2 remain genuinely open (B1, S2) and are the two I'd still
flag as blocking/serious for interview 2, and 1 (S4) is partially addressed
but not closed.** The round-2 team's own adversarial handling of my findings
was accurate and thorough — every fix I checked against source or
cross-spec actually holds, not just reads well.
---
# FINAL UPDATE (post-B1-survey)
Verified `bench_population_survey_all_committed_bodies`
(`server/tests/hydrology_equilibrium_bench.rs:273-`) directly against source,
the same way I verified everything else in this pass — not taking the
coordinator's summary or the measurement doc's prose at face value.
**Confirmed genuine, not a repeat of the original defect:** the new bench's
`par_iter` closure calls `load_heightmap_png(path, &body_id, 0.3)` **inside**
the closure, once per discovered path — a distinct file read, distinct
`body_id` (derived from the parent directory name), and distinct
`small.sea_level` (read from each PNG's own embedded `sea_level` metadata,
not a shared constant) per iteration. This is structurally the opposite of
`bench_parallel_273_bodies_at_512x256`'s defect, where `gj1c_512x256()` was
called once *outside* the `par_iter` block and every iteration reused the
same `elev`/`sea_level` binding. The new bench also carries a `heightmap_paths.len()
> 200` assertion (guards against a silently-empty discovery walk) and a
built-in determinism spot-check (`assert_eq!` on `cliff_edge`/
`channel_depth_scaled` for the top-by-carving outlier, re-solved a second
time) that would fire the moment any future body actually carves. Basin
arithmetic checks out (22,270 total = 21,240 overflow + 1,030 endorheic,
confirmed by direct addition). The measurement doc's `cliff_edge`/
`carved_cells` filter logic in the bench (`BasinOutcome::Overflow` with
`channel_depth_scaled[b.spill_cell] > 0`, `cliff_edge.iter().filter(|&&c| c)`)
matches what the original T-1177 module actually computes — not a redefined
or looser carving criterion invented to force a zero result.
**One precision note, not a discrepancy:** the coordinator's/Tyre's cited
"31187 basins/body" range is the min/max **within the printed top-15 table**
(GJ1075c=31, GJ1e-m1=187), not a separately-stated population-wide min/max —
the measurement doc's own prose illustrates variety with a different example
pair (GJ103c=40 to GJ1e-m1=187) rather than stating the true extremes. Both
are consistent with the same data; this is not an error, just noting the
"31" figure comes from reading the table rather than an explicit population
min/max statement in the doc. Doesn't affect the finding.
**B1 → RESOLVED.** The population-scale claim now rests on real, distinct
evidence (267 real bodies, each independently solved, real terrain, real sea
levels) rather than one body's cost profile standing in for the population.
The zero-carving result is stronger than what was being claimed before my
original finding (100% zero vs. "structurally rare, one-body-verified"), and
the epistemic framing in `tyre-round2.md` §(a.7) is honest about the
remaining limit ("not observed yet ≠ cannot occur," carving arithmetic
stays unit-proven for future bodies) rather than overclaiming "impossible."
This is exactly the outcome I'd hoped running the survey would produce, and
it did.
## Scorecard, final, for interview 2
Of the seven original findings plus the addendum's follow-ups, **two remain
open** and are the only items I'd carry into interview 2:
1. **S2 — courses-inclusive derive cost has zero measured coverage at the
deep-step (Block/Tile spacing) × high-river-density combination.** The
one courses-inclusive-at-real-density number that exists (T-1178
Cross-check 1: 195.0 ns/cell, 18 courses, District spacing, one body) is
accurate where measured (confirmed clean in my original pass), but the
deepest step's own realistic-canvas bench (T-1154's 83K-cell/17ms number)
explicitly excludes courses entirely by construction (H2's
`rect_window_replica` is courses-empty), and no round-2 document adds a
deep-step/river-dense bench. Not blocking (the existing <5% District-cap
bound has enough headroom that a multiple-density increase likely stays
affordable), but it is the one combination in the whole measured appendix
with literally zero data point, at exactly the spot (deepest step, most
individually-resolvable tributary geometry) where the excluded cost is
least likely to stay proportionally small. **Ask: run one more bench —
deep-step canvas on a body selected/authored for high course density —
before or shortly after filing; not a blocker for filing itself.**
2. **S4 residual — sim-state TTL phase-cadence is still an unnamed
variable.** Araminta's round-2 §(a) fully resolved the *field-assignment*
half of my original finding (which fields are sim-state vs static, and
why, stated per-field rather than inferred from name — `glaciation`/
`flooded` sim-state, `temp_dc` static, with a genuinely sharp
non-arbitrary argument for each). What remains unresolved is the
**numeric half**: D-228's "computed once per phase" water-height/
glaciation clock model doesn't state a phase duration, and nothing in
round 2 names one (or a per-body-class range) for sizing the sim-state
plane's actual TTL. The determinism substrate is sound (confirmed clean
in my original pass) — this is purely a missing tuning input, the same
category Stig/Dudley both correctly deferred their own STORAGE_TTL/
budget constants as "a tuning pass once real data exists." **Ask: either
fold this into the same tuning-pass deferral (acceptable, and consistent
with how the other unresolved constants in this workshop were handled),
or if Jeroen wants a concrete number before filing, that's a Dudley/
Araminta follow-up question at interview 2, not something the measured
appendix can answer as-is.**
Everything else — B1 (population survey, now run and resolved), B2
(seed-chaining, resolved and verified against source), S1 (eviction
mechanism, resolved; end-to-end cost number correctly deferred as tuning),
S3 (D-226(d) accumulation gap, resolved with a structural fix), S5 (cache
version tag, resolved with a complete fix), N1 (units note), N2 (informational,
now moot — all round-2 files exist), N3 (clean bill, unchanged) — is closed
or was never a live concern. The workshop's round-2 handling of every
finding I could verify against primary sources held up exactly as
documented, with no gap between what was claimed fixed and what the code/
doc actually shows.
@@ -0,0 +1,469 @@
---
title: "Body Map Viewer — Tyre's Round 1 Position"
description: "Governance amendment ratification, gridunit↔D-243 reconciliation, determinism boundary, and cliff ruling — against the measured ①–⑤ appendix"
type: workshop
status: active
workshop: body-map-viewer
created: 2026-07-25
---
# Tyre — Round 1 Position
*cracks knuckles* — the measurements came back clean. All four of the things I
flagged as "could kill this" in the implications pass (hydrology cost, derive
throughput at scale, wire size, deep-step cost) landed as **GO**, not
**maybe**. That changes my tone here: I'm not hedging architecture around
unknowns anymore, I'm ratifying specific amendment text against specific
numbers. Four questions, in the brief's order.
---
## 1. Governance delta — ratified as concrete amendment texts
The implications pass named five amendment targets. I said "surface as a
workshop question, don't pre-decide" for the sharpest one (gridunit↔D-243) —
that's §2 below. The other four are ready to write as filing text now; the
measurements didn't change their *direction*, only confirmed their necessity
and gave me the numbers to cite.
### 1a. D-166 corollary — repoint, don't delete
**Current text (2026-07-21 amendment):** *"Display at every rung samples the
derivation at canvas resolution (the ladder is a continuous field, not a
stack of fixed display rasters)."*
**Amendment text (proposed):**
> **Amended 2026-07-25 (body-map-viewer workshop — stepped ladder).** The
> zoom mechanism is superseded from continuous to **stepped** (T-1143 ruling
> 3, itself superseded — see §1b). The corollary's intent — no magnified
> interpolation of a coarser composite; no undersampling below the source
> floor — is **preserved, re-expressed per-step**: each step's data canvas is
> a derivation sampled at that step's native gridunit spacing (never coarser
> than the step's own floor, never finer than the display's own tunable
> ratio, measurement ④'s 1×1-to-5×5 px/gridunit band). Display *within* a
> step holds that step's canvas at a fixed, texel-exact ratio (measurement ⑤:
> upload cost 0.034.6 ms across every canvas size tested, never a frame-budget
> risk) — the "continuous field" framing is retired as literally false (it never
> was continuous once server-side per-step canvases replaced client
> `_canvas.scale`), but the guarantee it protected is intact, just discretized.
> The one honest new gap this creates — magnification of the *held* canvas in
> the interval just before a step-cross — is a **named, bounded** exception,
> not a silent violation: see §1e below and Red Flag 1. The corollary's
> anti-oversample/anti-undersample clause now reads: *"a gridunit is never
> derived coarser than its step's floor and never displayed finer than the
> tunable ratio; between-step magnification is bounded by construction to one
> step interval and is the ladder's only sanctioned display-time scaling."*
### 1b. T-1143 ruling 3 — superseded for zoom transport
**Amendment text (proposed):**
> **Superseded 2026-07-25 (body-map-viewer workshop).** T-1143 ruling 3
> ("continuous cursor-anchored zoom... wheel-zoom carries the view from the
> orbital frame down through regional granularities continuously") is
> superseded for the **transport mechanism**: zoom is **stepped** — scroll
> clicks through discrete gridunit-spacing levels, one server-canvas fetch
> per crossed step boundary. What SURVIVES from ruling 3, unchanged: (a)
> cursor-anchored centering, (b) edge-scroll pan, (c) the HARD condition that
> a full zoom-out resets to the canonical orbital frame and location. A
> client-side morph/tween between held step canvases is an *investigation
> item* — cosmetic interpolation of already-arrived data while the next
> step's canvas is in flight, never re-derivation, never a second source of
> positional truth (see §3, determinism boundary). Ruling 3's "progressive
> capped-density tiling riding the generalized `district_window` carrier, no
> forced tagged-envelope migration" clause is **also superseded** — see §1d,
> the tagged envelope is no longer avoidable and this workshop names it
> expected scope rather than something to keep dodging.
### 1c. `select_rung` / T-1143 §2–§5 rung model — replaced
**Amendment text (proposed):**
> **Superseded 2026-07-25.** The shipped `WindowGranularity` enum
> (Quarter/District/Region, coverage-ceiling-walked via `select_rung` keyed
> on `MAX_COVERAGE_M`) is replaced by the **stepped gridunit ladder**: a
> discrete step index (§3, the step ladder) selects gridunit spacing
> directly, not via a coverage-walk over a fixed rung enum. Rungs-as-
> derivation-granularity survive conceptually — every step still names an
> absolute-metre spacing — but the selector is gone, replaced by "which step
> is the viewport currently on." `resolve_window_granularity`'s whitelist
> discipline (T-1150: finer-than-district integer multiples only, unknown
> values fall back, never trusted from the wire) is the right *shape* for the
> step index's own validation and should be reused, not reinvented.
### 1d. D-226 T-1124 §2 windowed-family ceiling — tagged-envelope migration triggered
**This is the one measurement ④ makes unambiguous, not marginal.** The
brief's own framing ("likely triggers") undersells what T-1179 found: PNG
per-field, the *best* candidate encoding measured, is **21× the ~30 KB
ceiling at the smallest step canvas (330K gridunits) and 563× at the
largest (8.3M)**. Bit-packed and RLE are worse (55×–1,731×). There is no
encoding trick in the measured set that gets a step canvas anywhere near
"exactly one windowed field" territory — this isn't "the ceiling might need
raising," it's "the payload is two to three orders of magnitude past a
ceiling sized for a 4,096-cell window." T-1156's precedent (river skeletons
riding the whole-body family, explicitly *not* triggering the migration) and
T-1170's precedent (courses riding the windowed payload as *content*, not a
*new field*, also not triggering it) both worked because they stayed inside
the existing one-field shape. A step canvas cannot — its cell count alone
(330K8.3M vs. the 4,096-cell design point) is off by two to three orders of
magnitude before encoding is even considered.
**Amendment text (proposed):**
> **Amended 2026-07-25 (body-map-viewer workshop, superseding T-1143 ruling
> 2's "no forced tagged-envelope migration").** The windowed-family ceiling's
> **purpose** — prevent uncorrelated concurrent windowed queries needing
> per-field request correlation — survives untouched. Its **current
> mechanism** (exactly one windowed field, `district_window`, sized for a
> ≤4,096-cell served window) cannot carry a step canvas: measurement ④
> (T-1179) shows the best available encoding at 21×–563× the ~30 KB reference
> across the three measured canvas sizes (330K/2.07M/8.3M gridunits), with no
> encoding in the measured set closing that gap. The **tagged-envelope
> migration D-225 deferred** (`decode_inbound`'s shape-based demux → a
> required marker field / tagged envelope) is hereby the sanctioned path:
> the step-canvas payload becomes its own tagged message type on the existing
> IPC stream (not a second socket, not a `district_window` growth), landing
> D-225's 2026-06-12 constraint ("the next inbound message type must
> introduce a tagged envelope") on its intended target. Framed as **expected
> scope, executing a planned deferral** — tier: challenging but doable, not a
> risk. The ceiling's rule ("exactly one windowed-query field") is
> re-scoped: it now governs the *legacy* `district_window` carrier only (kept
> alive for whatever windowed traffic doesn't migrate — TBD in round 2's wire
> contract), while the tagged step-canvas envelope is a **new, separate**
> carrier the ceiling rule does not apply to by construction (it isn't a
> second windowed field on the old shape — it's the new shape).
### 1e. river-courses-t1170.md carrier rule — survives, terminology repointed only
No change to the rule's substance. I said in the implications pass this was
"robust to the pivot... cut on the axis the server relocation doesn't touch"
and the measurements don't disturb that: rule (iii) — "rung-indexed invented
detail rides the windowed payload" — still holds; "windowed payload" now
names the **tagged step-canvas envelope** (§1d) rather than the legacy
`district_window` field, but the *content* discipline (courses are payload
content, not a new query field) transfers unchanged. Ruling (i)/(ii) (skeleton
rides whole-body; continuous per-metre fields ride the window) are also
unaffected — Region-rung skeleton still needs the presentation-frame clip
T-1170 §3g describes until Region itself steps onto the ladder (§3, step
ladder — Region likely IS the global/step-0 rung, worth confirming in round
2's synthesis with Araminta).
**Amendment text (proposed):** none required beyond a terminology note —
"windowed payload" in `river-courses-t1170.md` is glossed as "the per-step
data canvas payload (formerly `district_window`, now the tagged step-canvas
envelope, §1d)" at the top of that document, so a future reader isn't
misled by the retired field name.
---
## 2. Gridunit ↔ D-243 reconciliation — the load-bearing call
Called this "the single most load-bearing vocabulary reconciliation" in the
implications pass and refused to pre-decide it. Now I have the numbers.
**Ruling: gridunit spacing snaps to D-243 rung spacings. It does not float
freely per viewport/step.**
### The argument
**Why free-floating breaks first:** if gridunit spacing were `viewport_px ÷
tunable`, two different monitor resolutions (or the same monitor at a
different step-transition moment) would derive **different absolute-metre
sample spacings at the same step**. That's not a cosmetic difference — the
derivation is `derive_at_metres(seed, body, position, spacing)`, and D-227's
whole determinism/cache-validity model rests on *the same inputs producing
the same output forever*. A gridunit spacing keyed to the requesting
client's viewport size means the server-side cache key must include viewport
dimensions (or the cache is wrong for the next client), which contaminates
D-227's "evictable cache, never source of truth, always valid on
recompute" cleanliness with a presentation-layer parameter. It also breaks
the client-side retention premise (premise 9): "a canvas for a fixed seed
never changes" stops being true if the canvas's own grid depends on the
window you happened to have open when you fetched it.
**Why snapping is free, not a compromise:** D-243's ladder already gives us
six absolute-metre rungs below the elastic seam (voxel 1 m, chunk 64 m,
block 128 m, quarter 512 m, district 2,048 m, region ~205 km) — measurement
③ (T-1154) independently confirms block and tile spacing cost the *same*
per-cell rate as District/Quarter (~1.8 µs/cell single-thread, no cutoff
discount below block). There's no derivation-cost reason to invent a
seventh, viewport-relative spacing value — the fixed rungs are already cheap
at every depth the ladder needs. **A step's gridunit spacing IS one of
D-243's absolute rungs** (or, at the very deepest step, the D-243 voxel/tile
itself, 1 m — already the ladder's own floor). Snapping doesn't cost
anything the ladder wasn't already going to pay.
**What DOES float, and this is where premise 5's "tunable" language was
doing real work:** the **px-per-gridunit display ratio** — 1×1 ideal down to
~5×5 acceptable — is the free parameter, and it's a client-side presentation
concern, not a derivation-side one. A 4K monitor and a 1080p monitor
requesting the *same step* get the *same absolute-metre canvas* (same
gridunit spacing, same derived content, same cache entry, shareable) — they
just display it at different px-per-gridunit ratios, which is a pure texture
scale-to-viewport operation, not a re-derivation. This is precisely the
canonical-vs-viewport question (red flag 3) wearing a different hat: the
canvas's *metre extent* should be sized to cover the largest viewport the
step needs to fill at that step's D-243 spacing (measurement ③'s "realistic
deep-step canvas," 216×384 m at 1 m spacing, 82,944 cells, 17 ms parallel —
already computed as a fixed metre-extent shape, not a viewport-px-shape),
and any monitor smaller than that extent just doesn't need all of it.
### Concrete ruling
- **Step → D-243 rung mapping is 1:1 or a documented fixed multiple**,
chosen in round 2's step-ladder synthesis (question 3 below feeds it). No
step invents a spacing D-243 doesn't already name.
- **Gridunit = "the per-step data cell," and its metric size is always a
D-243 absolute-metre value** — the vocabulary clarification I flagged in
the implications pass resolves cleanly: gridunit is not a *new* spatial
unit, it's a *role* ("the cell a given step's canvas is sampled at")
played by whichever D-243 rung that step is pinned to. At the deepest step,
gridunit and tile (D-243's voxel) coincide exactly — which is exactly what
"10 px per tile" in the outline already assumes.
- **The px-per-gridunit ratio (1×1 to 5×5) is a display-side tunable**,
decoupled from spacing. It governs texture-to-viewport scale and canvas
*extent* sizing, never canvas *spacing*.
- **D-243 gridunit vocabulary entry (filing text):**
> **gridunit (D-243 amendment, additive).** The per-step data-canvas cell.
> Not a new spatial rung — a *role* name for whichever D-243 absolute-metre
> rung (voxel through region) a given zoom step is pinned to. Gridunit
> spacing is always one of the ladder's fixed metre values; it never floats
> with viewport size or display resolution. The **display ratio**
> (screen-px per gridunit, 1×1 ideal, ≥5×5 acceptable per the body-map-viewer
> workshop) is the free, client-side, viewport-dependent parameter — kept
> terminologically and architecturally separate from gridunit spacing
> itself.
This is the "that's actually easier than it sounds" moment I was hoping for
going in: the reconciliation isn't a compromise between two systems, it's
recognizing gridunit was never a competing unit — it's D-243 wearing a
per-step hat, with exactly one genuinely free parameter (display ratio) that
was never a spatial-ladder concern in the first place.
---
## 3. The determinism boundary — where the client may interpolate without creating a second truth
D-010 principle 4 (server owns simulation/derivation state) and D-227
(derive-don't-store, pure function of seed+position) together draw this line
sharply, and the T-1170 course-invention ruling already demonstrates the
right shape in miniature — I want to generalize it rather than invent a new
rule.
### The rule
**The client may interpolate/tween *only* values that are already fully
resolved server-side and arrived on the wire as concrete endpoints or
control geometry.** It may never invent a sample the server hasn't computed,
and it may never smooth/blend *across* a boundary the server treats as
discontinuous (a step boundary, a rung's own truncation floor, a cliff edge).
Concretely, three buckets:
**(A) Legal client interpolation — presentation-only, zero determinism risk.**
- **Step-cross morph/tween** (premise 3, explicitly an investigation item):
animating from the held (arrived, resolved) coarser-or-adjacent canvas
texture toward the newly-arrived finer canvas texture. Both endpoints are
server truth; the client is blending two *already-true* images for visual
continuity during the fetch window. This is a pure `Tween`/shader
cross-fade — no new sample is invented, and the final resting state is
always the server's canvas, never a lerped hybrid held as "the" data.
- **River/road tweening between wire-carried control points**: T-1170's
Stage A/B course points ship as `Vec<(i32,i32)>` world-metre polyline
stations. The client draws a smooth curve *through* those points (spline
interpolation between server-given anchors) — this is cosmetic curve
fitting of a fully-specified polyline, not invention of new geometry. The
server has already decided where the river bends; the client is just not
drawing it as a jagged polyline of straight segments between stations.
This is exactly what T-1175 (per-vertex tapering, Polygon2D strips) is
scoped to do, and it's legal by this rule as written today.
- **Texture-to-viewport scaling** (the display ratio from §2): resampling a
fixed-spacing canvas to fit a monitor's px-per-gridunit ratio is a GPU
presentation resize, not a derivation. NEAREST/LINEAR filter choice
(already precedented per-rung in `_filter_for_granularity_v2`) lives
entirely in this bucket.
**(B) Illegal client interpolation — would create a second source of truth.**
- **Inventing a sample the server never computed** — e.g., a client-side
guess at what lies between two arrived gridunits at a *finer* spacing than
the server sent (upsampling terrain detail client-side). This is exactly
the T-1143 error class (magnified interpolation of a coarser composite)
the whole workshop exists to kill. The corollary amendment (§1a) forbids
it structurally — there is no client code path that samples "between"
gridunits at invented resolution, because the render baseline draws the
canvas 1:1 (or at the tunable ratio) via RTT, texel-exact by construction.
- **Smoothing across a step boundary**: blending step-N's canvas with
step-N+1's canvas as if they were one continuous field (rather than
cross-fading two discrete textures per bucket A) would reintroduce exactly
the composite-magnification problem — the boundary between two
differently-sampled canvases is real (different gridunit spacing on each
side), and pretending otherwise fabricates data.
- **Client-side course/terrain re-derivation** — the T-1170 ruling already
rejected this explicitly (1a: "Client-side GDScript invention is rejected
on the determinism surface... a byte-exact two-language mirror... is a
standing liability"). This generalizes to every invented-detail field:
hydrology (channel_depth/cliff_edge, §4 below), coast crinkle, vegetation
massifs — all CPU/Rust server-side only, per the workshop's own locked
premise 2. GPU is presentation only (the c2 pre-empt from the implications
pass, unchanged).
**(C) The one genuinely gray case — flagged, not resolved here.** Named-
feature *label placement* (settlement name text, POI glyphs) when two
adjacent gridunits at a coarse step both nominally "contain" the same named
feature — is a small client-side de-duplication/placement heuristic (avoid
drawing the same city name twice at a seam) legal presentation logic, or does
it risk client and server disagreeing about "which gridunit owns this
feature's label"? My read: legal, IF the server always ships feature
*identity* (a stable id) rather than the client inferring identity from
proximity — then client-side de-dup is deciding *where to draw one already-
identified thing*, not deciding *what things exist*. This is Araminta's
named-feature-encoding call (question 1) to confirm in round 2; I flag it
here because it's adjacent to the determinism boundary but is really a wire-
schema question (does the id ship) more than an interpolation question.
### Why this generalizes cleanly
Every "legal" case in bucket A shares one property: **the interpolation
input set is closed and server-supplied** (two full textures, a finite list
of wire-carried points). Every "illegal" case in bucket B shares the mirror
property: **the interpolation would need to invent a new sample outside
that closed set**. That's the same test D-227 already applies to
derive-don't-store generally ("cache is a bonus, never truth") — I'm not
proposing a new principle, I'm stating the existing one precisely enough
that "tween rivers/roads" (Jeroen's outline phrase) has an unambiguous
yes/no per case instead of being a vibe.
---
## 4. Cliff/multi-height ruling — building on Dudley's measurement
T-1177's finding changes the shape of this decision more than I expected
going in. Two things I flagged as open in the implications pass are now
answered by data, not architecture judgment:
1. **The representation question** (min/max height pair vs. dominant height +
channel-depth + cliff-edge flag) is **settled by what the solver actually
emits** — `HydrologyResult` already produces `channel_depth_scaled` and
`cliff_edge` per cell, directly, no re-derivation needed. Dudley's own
argument against min/max (loses the transition *shape* — is the drop a
point or does it span the gridunit — and requires synthesizing two heights
from one number the solver never computed) is correct and I'm not
relitigating it. **Ruling: dominant-height + channel_depth + cliff_edge is
the adopted representation.** This was "the first option red flag 4
named" and it wins because it's the one that requires zero invented
derivation downstream — the wire field is a direct carry of solver output.
2. **The frequency question changes the stakes.** I framed red flag 4 as "the
data model needs to represent gorges because hydrology produces them."
T-1177's honest finding — **zero carved cells across all three
production-scale benches (512×256, 768×432, 8.3M)**, with a clearly
articulated structural reason (priority-flood's true-minimum-rim property
makes single-basin carving mathematically impossible; genuine carving
needs a rare two-independently-sealed-basins-plus-single-cell-corridor
geometry) — means gorge cells are a **rare, not routine**, occurrence at
the scales this system actually runs at. That doesn't remove the need for
the field (the model must be able to represent what the algorithm
produces when it does fire, and "structurally rare" isn't "never" — real
planetary heightmaps beyond the one body sampled may differ), but it
substantially weakens the wire-cost argument for worrying about it: this
is a mostly-zero, sparse, `#[serde(default)]`-safe optional field, not a
dense per-cell cost driver.
### Ruling, stated for filing
**Phase-4 Atlas scope, not deferred to Phase-5.** Two reasons this call
goes *into* scope rather than out:
- **It's nearly free once carved.** The representation is a direct carry of
existing solver fields — no new derivation, no new invented-detail
category, and (per the rarity finding) usually a no-op (`channel_depth:
0`, `cliff_edge: false` for the overwhelming majority of gridunits). This
isn't "cliffs are hard, defer them" — it's "cliffs are already computed,
carrying them costs approximately nothing."
- **Deferring it to Phase-5 in-world geometry with only a "steep"
classification would silently violate D-227's determinism-critical
framing** of hydrology as a *settled equilibrium*: if the Atlas map shows
a lake with a smooth shoreline where the settled hydrology solver actually
computed a carved overflow channel, the map is showing something other
than what "settled hydrology" means. The whole point of red flag 4 in the
implications pass was "hydrology can't be settled as producing gorges
while the data model can't represent one" — now that the solver
demonstrably does produce them (even if rarely), showing a flattened
version on the map is exactly that contradiction, just less frequently
triggered.
**Wire shape (for Araminta's round-1 payload schema, ④):**
```
elevation: existing dominant-height field, unchanged for the non-gorge case
channel_depth: u16 (quantized), 0 for the overwhelming majority of gridunits
cliff_edge: bool (or a bit folded into an existing classification byte)
```
**What this does NOT resolve, staying honest about scope:**
- Whether `channel_depth`/`cliff_edge` ride the *tagged step-canvas envelope*
(§1d) as two more per-cell array fields, or get folded into a bit of an
existing field — Araminta's wire-schema call, not mine, but I'd steer
toward "new arrays" over "steal a bit" given how sparse the data is (a
sparse-friendly encoding, e.g. PNG-per-field per measurement ④'s own
finding that DEFLATE handles near-constant fields exceptionally well —
`morphology`/`vegetation` at 0.0% run density in the RLE table are the
same shape channel_depth's near-all-zero distribution would have —
actually *helps* this field cost less than its raw byte width suggests).
- The client-side render treatment (cliff-face style transition vs. gradient)
— Stig's call, not architecture's; I note only that `cliff_edge` gives the
map-art function exactly the signal it needs to make that choice without
inventing anything (bucket A interpolation, §3: the client is styling a
server-supplied boolean, not deciding where a cliff is).
- Whether Block/Tile rungs (where gorge-scale features would actually be
visible at all, per T-1154's spacing) are the step ladder's floor or an
intermediate step — that's the step-count/step-ladder synthesis, §3 of the
brief's expected outputs, round 2 territory.
**One honest caveat to carry into round 2 (Troblum's adversarial pass should
stress this):** T-1177 sampled hydrology on exactly one real body (GJ1c) plus
two synthetic gradients. "Structurally rare at production scale" is a
strong, well-argued finding, not a survey across the ~273-body population.
If a body with genuinely different terrain character (e.g., heavily
tectonic, high-relief, many small nested basins) produces carving far more
often than GJ1c did, the "usually a no-op" cost argument softens — still
correct architecturally (the field costs nothing when zero, something
reasonable when nonzero), but the "rare" framing shouldn't be read as
"provably rare across the whole body population." Doesn't change my ruling;
does mean I wouldn't cite "rare" as load-bearing for anything beyond the
wire-cost argument above.
---
## Summary — my four calls, plainly
1. **Governance delta**: five amendment texts drafted and ready to file
(§1a1e) — the D-166 corollary repoint, T-1143 ruling-3 supersession, the
`select_rung` replacement, the tagged-envelope migration as **triggered,
not merely likely** (measurement ④ makes this unambiguous — 21×–563× over
the ceiling), and the T-1170 carrier rule surviving with a terminology
gloss only.
2. **Gridunit ↔ D-243**: **snaps, does not float.** Gridunit is a per-step
*role* played by a D-243 absolute-metre rung, never a viewport-derived
spacing. The only genuinely free parameter is the display ratio
(px-per-gridunit), which is a client-side presentation concern cleanly
separated from spacing.
3. **Determinism boundary**: client may interpolate only within a closed,
server-supplied input set (two arrived textures, wire-carried control
points) — never invent a sample outside it, never smooth across a step or
rung-truncation boundary. Three concrete legal cases, two illegal, one
gray case flagged for Araminta.
4. **Cliff ruling**: dominant-height + `channel_depth` + `cliff_edge`,
**in Phase-4 Atlas scope** (not deferred to Phase-5) — nearly free given
T-1177's direct-carry representation and rarity finding, and deferring it
would contradict "settled hydrology" itself. Caveat: rarity is measured
on one real body, not the population — don't over-lean on it for anything
beyond wire cost.
Feasible across the board. The measurements did their job — nothing here is
extrapolated, and nothing surfaced a reason to walk back the "architecturally
cleaner than what ships today" read from the implications pass. The tagged-
envelope migration is the one genuine chunk of new work or this list, and
it's exactly the size we already knew it would be.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,424 @@
---
title: "Workshop Outcomes"
description: "Final outcomes: D-255 (new) + 12 amendments, the six-level Atlas step ladder (Global opener + Region-District-Quarter-Block-Chunk), the tagged-envelope wire contract, cache-tier architecture, the lakes/hydrology sourcing fix, and the full deprecation sweep."
type: workshop
status: archived
workshop: body-map-viewer
agent: qatux
round: 0
created: 2026-07-25
decision_refs: [D-010, D-166, D-169, D-170, D-192, D-223, D-225, D-226, D-227, D-228, D-239, D-243, D-253, D-255]
---
# Body Map Viewer Workshop — Outcomes
**Workshop:** Body Map Viewer (the stepped map handler)
**Dates:** 2026-07-25 (round 1) 2026-07-23 (final ratification and filing)
**Rounds:** 2 core rounds + two lead interviews, extended with a post-ratification
addendum round (the lakes/hydrology gap Jeroen raised after reviewing the
as-built briefing) — the workshop was kept open across all three review passes
rather than closed on partial convergence, per standing team practice.
**Participants:** Dudley, Araminta, Stig, Tyre, Troblum (round 2 + adversarial),
Qatux (documenting), SI (reviewing ticket outputs)
**Decisions produced:** D-255 (new) + 12 amendments (D-166, D-192, D-225, D-226
×4, D-227 ×4, D-243, river-courses-t1170.md note)
**Compiled by:** Qatux
---
## 0. Workshop trace
| Stage | Document | Status |
|---|---|---|
| Brief | [body-map-viewer-workshop-brief.md](body-map-viewer-workshop-brief.md) | complete |
| Prep: outline, clarifications, implications | [jeroen-outline.md](jeroen-outline.md), [clarifications.md](clarifications.md), [tyre-implications.md](tyre-implications.md) | complete |
| Pre-workshop measurements ①–⑤ | `measurements/t1177-hydrology.md`, `t1178-t1154-derive-bench.md`, `t1179-wire-table.md`, `t1180-imagetexture.md` | complete |
| Round 1 positions | dudley/araminta/stig/tyre-round1.md | complete |
| Round 1 synthesis | [round-1-notes.md](round-1-notes.md) | complete |
| Lead interview 1 | [lead-interview-1.md](lead-interview-1.md) | complete |
| Measurement ⑥ (post-interview-1) | `measurements/t-setpixel-c1.md` | complete |
| Hydrology population survey (post-interview-1) | `measurements/t1177-hydrology.md` §POPULATION SURVEY | complete |
| Round 2 synthesis | dudley/araminta/stig/tyre-round2.md | complete |
| Round 2 adversarial pass | [troblum-round2.md](troblum-round2.md) (incl. ADDENDUM + FINAL UPDATE) | complete |
| Round 2 synthesis notes | [round-2-notes.md](round-2-notes.md) | complete |
| **Lead interview 2** (ladder-bottom narrowing, seed-chaining ruling) | rulings folded into [dudley-interview2-response.md](dudley-interview2-response.md), [tyre-round2.md](tyre-round2.md) §(a.8b)/§(a.10) | complete |
| Chunk-64m bench, Option D, S2 courses-density bench, S4 TTL formula | [dudley-interview2-response.md](dudley-interview2-response.md) §14 | complete |
| **Post-ratification review** (Global/Region rung split, lakes gap) | [dudley-interview2-response.md](dudley-interview2-response.md) POST-RATIFICATION ADDENDUM + LAKES CONVERGENCE | complete |
| Lakes convergence (message-crossing + resolution) | [araminta-round2.md](araminta-round2.md) §(e), [dudley-interview2-response.md](dudley-interview2-response.md) §5/LAKES CONVERGENCE, [tyre-round2.md](tyre-round2.md) §(a.11) | complete |
| Final governance package | [tyre-round2.md](tyre-round2.md) (final, §(a)(d)) | complete |
| As-built plain-language briefing | [architecture-briefing-final.md](architecture-briefing-final.md) | complete |
| Filing (D-record + 12 amendments) | this document §2 | **complete — D-255 claimed** |
| Deprecation sweep / Clerk audit | this document §5 | complete |
| Ticket plan | this document §6 | complete (12 DQR + 10 ticket dispositions, drafted by Tyre, SI review per standing workflow) |
| This document, final | — | **complete** |
---
## 1. Interview rulings — the full sequence
Three review passes ruled on this workshop's open items, in order. Each is
recorded here as process history — the corrections across passes are not
noise; they are exactly the kind of thing this document exists to make
visible rather than silently overwrite.
### Interview 1 (post round 1)
Full record: [lead-interview-1.md](lead-interview-1.md). Ratified by silence:
tagged-envelope migration triggered; viewport-sized canvases below the
canonical tier; the T-1177 cliff representation (`elevation` + `channel_depth`
+ `cliff_edge`); cache composition (server global tier + client `FileAccess`
dir, compose not compete). Ruled: step ladder deferred to round 2's concrete
tables; map time axis = current-state-via-TTL-split with the staleness-vs-
storage eviction-axis distinction; cliffs sparse + Phase-4 scope; round 2 GO
with measurement ⑥ run in parallel.
### Interview 2 (post round 2) — two rulings that reworked the round-2 package
1. **Ladder bottom narrowed from tile/voxel (1 m) to chunk (64 m).** Jeroen,
verbatim: *"the actual tile level rung seems unusable. maybe replace with
64?"* — reasoning: a full-screen Atlas view at 10 px/tile shows ~192×108 m
of ground, which is in-world viewport content (Phase 5), not map content.
This is a **deliberate narrowing of the D-226(d) floor's interview-1
opening, not a reversal of it** — block and chunk remain legal Atlas rungs
under the viewport-sized carve-out; tile/voxel re-close, exactly as item
(d) originally required, now scoped to the tier where the opening was
never meant to reach. Dudley benched the never-measured chunk rung in
response (`dudley-interview2-response.md` §1) and built **Option D** — the
adopted ladder.
2. **Seed-chaining ruled the OPPOSITE of Dudley's round-2 default.** Jeroen
answered **NO** to Dudley's round-2 "independent re-derivation, coarser
rung called only as a nested function" ruling — he meant the finer step
genuinely **consumes** the coarser step's *resolved* output. Reconciled
without weakening D-227 via the **cache-accelerated pure function** model
(§2 below; `tyre-round2.md` §(a.10)).
Both rulings triggered new measurement work (the chunk-64m bench, the S2
courses-density bench at chunk/block, the S4 TTL-cadence formula) rather than
being filed as bare architectural pronouncements — consistent with this
workshop's standing discipline of ruling from real tables, not principles.
### Post-ratification review (after the as-built briefing)
Jeroen reviewed [architecture-briefing-final.md](architecture-briefing-final.md)
(the plain-language as-built writeback he requested) and raised two further
items:
1. **Global-tier byte math was wrong.** Round 2's ~174 MB figure had priced
the canonical always-kept tier as a full District-spacing canvas (8.3M
cells/body). Jeroen's own review caught that the opener is a **variable,
region-grid canvas** (one gridunit per region, not per District cell) —
correcting this produced a **rung-identity split**: rung 0 = **Global**
(the variable, always-kept opener) and rung 1 = **Region** (the largest
*fixed*, evictable rung) — previously conflated as one "step 0 = Region"
tier. Recomputed against the real 267-body population: **~8.85 MB
PNG-encoded, all bodies** (not ~174 MB — roughly 20× smaller).
2. **The lakes gap.** Jeroen's direct question: *"when filling basins to find
an overflow, is that body flagged as lake or flooded or something? do we
draw lakes on the map?"* This surfaced a real gap (§4 below) and produced
a genuine message-crossing between Araminta and Dudley, resolved
convergently — documented in full as process history in §4.
---
## 2. Governance record — filing package (final)
**D-255 claimed** (`architecture` domain). One new record + twelve amendment
texts, all record-ready as drafted in `tyre-round2.md` §(a).
| # | Record | Type | Source text |
|---|---|---|---|
| 1 | **D-255 — Body-map-viewer stepped render architecture** | NEW | tyre-round2.md §(a.0) |
| 2 | D-166 corollary — per-step repoint (owns between-step magnification) | AMEND | §(a.1) |
| 3 | D-226 T-1143 ruling 2/3 — tagged-envelope + continuous-zoom superseded | AMEND | §(a.2) |
| 4 | `select_rung` / rung model — replaced by the discrete step index | AMEND | §(a.3) |
| 5 | D-243 — gridunit vocabulary entry (additive; Atlas floor = chunk) | AMEND | §(a.4) |
| 6 | D-226 T-1124 §2 windowed-family ceiling — tagged-envelope triggered, re-scoped | AMEND | §(a.5) |
| 7 | D-227 — map-time TTL-split + staleness/storage eviction-axis distinction | AMEND | §(a.6) |
| 8 | river-courses-t1170.md carrier rule + cliff sparse-list | AMEND (note) | §(a.7) |
| 9 | D-226(d) whole-body prohibition — per-request framing + client-cache accumulation cap | AMEND | §(a.8) |
| 10 | D-226 T-1143 ruling 1 — floor narrowed to chunk (tile/voxel re-close) | AMEND | §(a.8b) |
| 11 | D-227/D-192 — persistent client-cache schema/version tag | AMEND | §(a.9) |
| 12 | D-227 — seed-chaining as "cache-accelerated pure function" | AMEND | §(a.10) |
| 13 | D-227 — lakes sourced from settled hydrology; endorheic cue via outflow-course presence | AMEND | §(a.11) |
**Records confirmed surviving untouched** (stated explicitly per Tyre's list,
so the Clerk audit does not over-rewrite them): D-010 (four principles — the
server relocation *reinforces* principle 4); D-166 cascade + phase gating
(still Phase-4 Atlas); D-169/D-170 (implant UI, unchanged); D-223
(`atlas_city_names`, unchanged); D-239 (the frozen 17-zone morphology +
vegetation/glaciation vocabulary — the lakes fix explicitly does **not**
touch it, reusing the existing `MorphologyZone::Lake` discriminant and
routing the endorheic cue through `courses` precisely to avoid an 18th zone).
**D-record text:** the full, final amendment text for every item above is
authoritative in `tyre-round2.md` §(a.0)(a.11) — this table is an index with
citations, not a duplicate copy, per the project's single-source-of-truth
convention for governance text (`governance/README.md`). Sync into
`governance/decisions/architecture.md` via `pql decisions claim D architecture
"Body-map-viewer stepped render architecture"` (already reserved as D-255)
followed by `pql decisions sync`.
---
## 3. Architecture summary (as-built)
The full plain-language writeback is [architecture-briefing-final.md](architecture-briefing-final.md)
— Jeroen's own outline, rewritten in the same style and brevity to reflect
what actually shipped. Condensed here:
- **Three locked premises, unchanged since the brief:** server-side content
determination (per-gridunit data canvases per zoom step), client as a pure
map-art function (colorize/style/annotate, never invent geometry), stepped
zoom (discrete rungs, cursor-anchored, no continuous zoom-scaled canvas).
- **The Atlas step ladder — six levels, final (Option D):**
| Rung | Level | Spacing | Canvas | Cost (parallel, measured) |
|---:|---|---:|---|---:|
| 0 | **Global** (opener, canonical, always-kept) | 1 gridunit/region (variable, ~205 km) | body's own region grid, ~18K cells avg | ~1621 ms/body; ~8.85 MB PNG all 267 bodies |
| 1 | Region | 204.8 km | 3840×2160 fixed | 1,827 ms |
| 2 | District | 2,048 m | 3840×2160 fixed | 1,827 ms |
| 3 | Quarter | 512 m | 3840×2160 fixed | ~1,827 ms (same-band) |
| 4 | Block | 128 m | 3840×2160 fixed | ~1,827 ms (measured) |
| 5 (deepest) | **Chunk** | 64 m | 3840×2160 fixed | 1,7241,733 ms (measured) |
Bottom-out rule: **1 screen px per 64 m gridunit, no magnification margin**
(the old 10×-per-tile legibility margin does not apply — a chunk-scale
feature is already legible at 1×1). Tile/voxel (1 m) is Phase-5 in-world
content, never Atlas-mapped. Every fixed row is directly measured; rung 0
is measured too (Dudley's `bmv_global_tier_bench.rs`).
- **Wire:** tagged-envelope carrier (`StepCanvasRequest`/`StepCanvasResponse`,
a sixth discriminated `Inbound` variant extending an already-five-times-
proven pattern in `server/src/bridge/mod.rs`). 8 dense fields (6 static + 2
sim-state) PNG-per-field, 2 sparse lists (`courses`, `cliffs`)
MessagePack-native. No `water` field — lakes ride the existing
`MorphologyZone::Lake` (§4).
- **Cache:** three tiers — client in-memory LRU → client disk `FileAccess`
(two independent sweep axes: staleness for sim-state, storage-budget for
everything else) → server (Tier 1 rung-0 resident resource, ~8.85 MB all
bodies, keep-always; Tier 2 sub-global geometry, storage-evicted on
time-since-last-visit). Sim-state TTL bound to 1× the field's own fastest
driving sim-clock bucket (tidal-or-seasonal for flooded, seasonal for
glaciation) — a formula, not a fixed number, confirmed cost-affordable at
1× with no multiplier needed.
- **Client render:** RTT terrain layer, CPU `Image.set_pixel` colorize
(confirmed cheap at realistic canvas sizes by measurement ⑥, ~78 ns/cell
flat) + unscaled screen-space annotation layer, replacing `_canvas.scale`
and the entire `_zs()`/`_zs_stroke()`/`_zs_ring_radius()` compensation
family by construction.
- **Seed-chaining:** cache-accelerated pure function — a finer rung's
derivation may read a resident coarser rung's continuous baseline as an
*optimization* (falling back to fresh derivation when not resident); the
①②③ benched costs remain valid as the cache-cold worst-case ceiling. A
mandatory cache-hit == cache-miss byte-identical determinism test is the
correctness gate.
- **Lakes:** sourced from `HydrologyResult`'s settled equilibrium, riding the
existing `MorphologyZone::Lake` — zero new wire fields, zero vocabulary
change (§4).
---
## 4. Process history — the lakes arc (documented in full, not condensed away)
This is recorded at length deliberately: it is a clean example of the
workshop's own discipline working correctly under a genuine, in-flight
message-crossing, and the coordinator specifically asked that it be
preserved as process history rather than silently resolved into a single
final answer.
**The catch.** After reviewing the as-built briefing, Jeroen asked directly
whether basin-filling from settled hydrology gets flagged as a lake on the
map at all. Checked against the real code (not assumed): the gap was more
specific than a missing field. `MorphologyZone::Lake` (discriminant 1)
**already exists** in the frozen 17-zone vocabulary — the gap was a missing
**data source**. Today's `Lake` emission comes from a crude heightmap
threshold (`ocean_fraction_q >= 60`, a bilinear sample of the raw
below-sea-level mask) with **zero connection** to `HydrologyResult`'s settled
basins — a gap the code's own comment already flagged as known, just never
connected to the solver this workshop's own measurement ① is about.
**The crossing.** Dudley answered the coordinator's routed lake-schema
question with a proposal for a **new `water: Vec<u8>` dense field**
(`{None, Lake, Sea}`), reasoning from first principles about wire cost and
vocabulary freezing. Independently, in flight, Araminta specced her own
`water: Vec<u8>` field with the same shape and posed three questions back to
Dudley (endorheic-carrier sizing, pipeline slot, struct reconciliation).
Dudley answered those three questions fully (visible in his document as §13
under "LAKES CONVERGENCE," including a full crossover-cost bench comparing a
sparse `Vec<LakeBasin>` list against the dense `water` field across the real
267-body basin-size distribution).
**The resolution.** Later the same day, both Dudley and Araminta
**independently discovered the better answer and converged on it without
either conceding to the other**: fold lake/sea classification into the
**existing** `MorphologyZone::Lake` discriminant instead of adding any new
field at all. Araminta reached this by re-verifying Dudley's own code-read
directly against source (`district_profile.rs:556-565`, `generator.rs:1198-
1223`, `features.rs:104-105`) and recognizing her own original objection (that
folding into morphology would require widening a frozen vocabulary) was
built on a false premise — the vocabulary entry already existed; only the
data source was missing. Dudley reached the same place from his own
post-ratification code investigation. Both documents mark the earlier
`water`-field material explicitly as **SUPERSEDED-BY-CROSSING** rather than
silently deleting it — Dudley's document states plainly: *"it isn't one of
us conceding to the other, it's convergent verification."*
**What survived the crossing unmodified:** the pipeline/projection ruling
(sample `HydrologyResult`'s continuous `filled_scaled` field bilinearly per
rung — never project discrete basin-cell membership, which would produce a
blocky, non-refining lake edge, exactly the magnified-coarser-composite
artifact D-166's corollary forbids). This reasoning is agnostic to which
field carries the result, so it applied unchanged to both the abandoned
`water` field and the final morphology-fold.
**Final, converged answer (D-227 amendment §(a.11)):**
- Lakes ride the **existing** `MorphologyZone::Lake` — zero new wire field,
zero vocabulary change. The fix is a **data-source correction**:
`derive_morphology_zone` sources `Lake` from settled hydrology's continuous
`filled_scaled` field (sampled bilinearly, the same mechanism
`ocean_fraction_q`/`sea_level` already use), falling through to today's
heuristic where no basin exists.
- Lake geometry is **static** (a pure function of seed + terrain, cached
indefinitely-fresh), explicitly distinct from Araminta's sim-state
`flooded` plane — a lake's existence doesn't flicker on the seasonal TTL.
- **Endorheic-vs-overflow cue: outflow-course presence, zero wire bits, zero
new zone.** Dudley's original crossover-cost analysis (dense-bit vs.
sparse-list) was correctly identified by the coordinator as not actually
answering this question — both of those carriers were foreclosed once the
morphology-fold landed. The real choice was reasoned fresh: an overflow
basin's guaranteed non-empty `outlet_path` becomes a visible exit river in
`courses`; an endorheic basin's absence of one is the honest "no drain"
signal. Three reasons this wins over a dense vocabulary bit: proportionality
(endorheic is 4.63% of real basins — too rare to justify a permanent 18th
`MorphologyZone` arm every consumer inherits forever), the outlet-wiring
work is required regardless (an overflow lake must show its exit river to
be hydrologically honest — the cue is a free side effect, not a feature
built to carry it), and the inference is definitionally sound with no
misfire case (every `Overflow` basin has a guaranteed outlet path, every
`Endorheic` basin has none — tested directly:
`overflowing_basin_has_nonempty_outlet_path`).
- **Honest sequencing stated plainly, not glossed over:** until the
basin-outlet→D8 wiring ticket ships (pre-cleared as additive by T-1170
Ruling 7b's reserved `TERMINAL` sentinel), the map shows lakes but not the
drains-vs-closed distinction. This is not a design gap — it is two
distinct, sequenced deliverables, and the outcomes record states which one
ships first rather than implying the distinction arrives automatically the
day the sourcing fix lands.
- The originally-proposed cliffs-based endorheic cue was independently ruled
out by Araminta after directly re-checking the population survey: zero
carved cells across all 267 real bodies means a cliff-wired cue would never
fire in practice.
---
## 5. Deprecation sweep
**DQR records** (13 dispositions — the 12 amendments in §2 plus D-192's
cross-reference note; full text in `tyre-round2.md` §(d)):
| Record | Disposition |
|---|---|
| D-166 corollary (2026-07-21) | AMEND — repointed per-step, not deleted |
| D-226 T-1124 §2 ceiling | AMEND — re-scoped to the legacy `district_window` carrier only; re-scope is **final**, the conditional supersede branch does not fire |
| D-226 T-1143 ruling 2 | SUPERSEDE — tagged-envelope migration now triggered |
| D-226 T-1143 ruling 3 | SUPERSEDE for transport (stepped); cursor-centering/edge-scroll/full-reset survive |
| D-226 T-1143 §6 `select_rung`/`MAX_COVERAGE_M` | SUPERSEDE — replaced by the discrete step index |
| D-226 T-1143 ruling 1 (item-(d) floor opened) | AMEND/NARROW — opening now lands at chunk (64 m); tile/voxel re-close |
| D-243 item (5) vocabulary | AMEND (additive) — `gridunit` role name; Atlas floor bottoms at chunk |
| D-225 (tagged-envelope deferral) | SATISFY (note) — constraint discharged by this workshop's envelope |
| D-226 item (d) whole-body prohibition | AMEND — per-request/per-derivation framing + client-cache accumulation cap + the chunk-floor narrowing |
| D-227 (derive-don't-store) | AMEND (additive ×4) — TTL-split/eviction axes; persistent-cache schema tag; seed-chaining cache-accelerated model; lakes sourcing fix |
| D-192 (no version handshake) | CROSS-REF note — the co-ship guarantee does not extend to the persistent disk cache |
| river-courses-t1170.md (Ruling 1c) | AMEND (note) — terminology repoint + `cliffs` as rule (iii)'s second vector member |
**Surviving untouched:** D-010, D-166 cascade/phase gating, D-169/D-170,
D-223, D-239 (see §2 for the explicit no-touch statement on each).
**Tickets** (10 dispositions):
| Ticket | Disposition |
|---|---|
| T-1176 | CLOSE as delivered — this workshop is its output |
| T-1158 | CANCEL with supersession note — the canonical-frame cluster it wanted to extract is exactly what the stepped model deletes |
| T-1175 | RE-SCOPE, unblocked — the c1 blocker (measurement ⑥) is discharged; tapering lands on the new screen-space annotation layer |
| T-1157 | RE-SCOPE as the stepped mechanism's capture harness — the dead-goldens problem it targets is a direct consequence of the split the stepped model collapses |
| T-1174 | KEEP unchanged — a derivation-correctness bug orthogonal to the render pivot, priority raised (more derive paths now share the same position math) |
| T-1153 | stays `done`; shipped code (`_view_zoom`, continuous-zoom transport) on the `_canvas.scale` retirement path |
| T-1152 | stays `done`; `compute_tile_grid()` mosaic superseded by the rung-0 Global canvas |
| T-1153/T-1152 test suites | retired with their code; replaced by re-scoped T-1157 |
| **NEW — lake morphology sourcing** | CREATE — source `MorphologyZone::Lake` from `HydrologyResult`'s `filled_scaled`, self-contained, no wire change |
| **NEW — hydrology basin-outlet → D8 wiring** | CREATE — thread overflow basin outlets into `RiverNetwork`/`courses`; pre-cleared additive by T-1170 Ruling 7b; this is the ticket the endorheic cue ships on |
**`_canvas.scale` retirement path** (explicit list for the implementation
ticket, full text `tyre-round2.md` §(d)): `_canvas.scale`/`_canvas.position`/
`_apply_transform()`; the `_zs()`/`_zs_stroke()`/`_zs_ring_radius()`
compensation family; `select_rung()`/`MAX_COVERAGE_M`/`compute_tile_grid()`;
the `AtlasViewer`/orbital-mosaic-vs-window split (collapses to "the Global
opener is rung 0, one path"); `_view_zoom`/`_canonical_fit_zoom()` (replaced
by the discrete rung index + the rung-0 canonical frame).
**Clerk consistency audit:** the known-conflicts starting set above (13 DQR +
10 ticket dispositions) is the complete, agent-produced sweep. A systematic
audit pass beyond this named set is the standing pre-dismissal check per the
brief's Expected Output 2 — run against the filed `governance/decisions/
architecture.md` text once D-255 and the twelve amendments are synced, to
catch any conflict the delta did not explicitly name.
---
## 6. Measurement summary
| # | Ticket | Headline | Doc |
|---|---|---|---|
| ① | T-1177 | Hydrology viable; cliff repr. = elevation+channel_depth+cliff_edge; population survey: 0/267 real bodies carve | measurements/t1177-hydrology.md |
| ② | T-1178 | Parallel throughput flat 190220 ns/cell, 330K8.3M cells | measurements/t1178-t1154-derive-bench.md |
| ③ | T-1154 | Block GO; original deep-step (voxel) bench superseded by chunk bench below | measurements/t1178-t1154-derive-bench.md |
| ④ | T-1179 | PNG-per-field wins; 21×–563× over 30KB cap → tagged envelope triggered | measurements/t1179-wire-table.md |
| ⑤ | T-1180 | Texture upload non-issue, 0.034.6ms | measurements/t1180-imagetexture.md |
| ⑥ | (round 2) | CPU colorize 77.5 ns/cell flat; beats byte-buffer ~2×; c1 = CPU-first, confirmed | measurements/t-setpixel-c1.md |
| interview-2 | (Dudley) | Chunk (64m) bench: 1,7241,733 ms parallel full-canvas; Option D adopted | dudley-interview2-response.md §1 |
| S2 | (Dudley, Troblum-requested) | Courses cost at chunk/block: +38%87% (vs. District's <5%) — real, traced mechanism, still affordable | dudley-interview2-response.md §3, filed as addendum in measurements/t1178-t1154-derive-bench.md |
| S4 | (Dudley + Araminta) | Sim-state TTL formula: 1× fastest driving sim-clock bucket, confirmed cost-affordable at 1× | dudley-interview2-response.md §4 |
| global-tier recheck | (Dudley, post-ratification) | Rung-0 corrected: ~8.85 MB PNG all 267 bodies (not ~174 MB) — Global/Region rung split | dudley-interview2-response.md POST-RATIFICATION ADDENDUM §6 |
| lake basin distribution | (Dudley, post-ratification) | Real population: 22,270 basins, dense wins 100% vs. sparse for lake carrying | dudley-interview2-response.md LAKES CONVERGENCE §1 |
---
## 7. Adversarial pass disposition (Troblum)
Final scorecard (`troblum-round2.md` FINAL UPDATE): of seven original
findings (2 BLOCKING, 5 SERIOUS), **all seven are resolved** by the time of
filing:
| Finding | Resolution |
|---|---|
| B1 (population-scale cliff survey never run) | RESOLVED — 267/267 real bodies surveyed, 0 carve |
| B2 (seed-chaining fork unresolved) | RESOLVED — interview-2 ruling + Dudley's source-verified cache-accelerated model |
| S1 (evict-then-revisit costing) | RESOLVED (mechanism) — both cache specs converge on dropping `distance_decay`; end-to-end revisit number correctly deferred as tuning |
| S2 (courses-density coverage gap) | RESOLVED — Dudley's chunk/block courses bench, real finding, filed |
| S3 (D-226(d) letter vs. purpose) | RESOLVED — per-request framing + client-cache accumulation cap, §(a.8) |
| S4 (sim-state phase cadence unnamed) | RESOLVED — Dudley+Araminta joint formula, 1× confirmed affordable |
| S5 (disk-cache version tag) | RESOLVED — §(a.9), complete fix |
N1 (units convention), N2 (informational), N3 (clean bill) — all closed or
never live concerns. No finding required walking back a round-1 or round-2
position; every fix Troblum checked against primary source held up exactly
as documented.
---
## 8. Diagrams
Pending — to be created via `/d2-diagram` in `docs/diagrams/architecture/`
following the standard post-filing diagram pass:
- Step-ladder diagram (Global opener → Region → District → Quarter → Block →
Chunk, with the variable/fixed and canonical/evictable seams marked).
- Cache-tier diagram (three tiers, the two eviction axes, the rung-0/rung-1
seam).
- Wire-envelope diagram (tagged `StepCanvasRequest`/`Response` vs. legacy
`district_window`, the re-scoped ceiling boundary).
- Lakes data-flow diagram (`HydrologyResult``filled_scaled` bilinear
sample → `MorphologyZone::Lake``courses` outlet presence → endorheic
cue), given the arc's own complexity documented in §4 above.
**Not yet started** — first post-filing task, per standing practice
(diagrams follow filed decisions, not the reverse).