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>
19 KiB
title, description, workshop, round, author, status
| title | description | workshop | round | author | status |
|---|---|---|---|---|---|
| Body Map Viewer — Araminta Round 1 | 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) | body-map-viewer | 1 | Araminta | complete |
Araminta — Round 1
Answering my three questions from the workshop
brief. 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_ids (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
330K–8.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>andcliff_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_edgeimplied by list membership) parallel tocourses— 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 sparsecliffslist. 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
cliffslist). 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 newcliffs) + the existingcoursesname resolution now needs a whole-bodyriver_nameslookup alongside the existingatlas_city_namessettlement 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 (~1–2 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.