Files
settled-reach/docs/workshops/body-map-viewer/araminta-round1.md
T
jpmschweitzerandClaude Fable 5 29c22cb728 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>
2026-07-24 10:57:45 +02:00

310 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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
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>` 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 (~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.