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>
310 lines
19 KiB
Markdown
310 lines
19 KiB
Markdown
---
|
||
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.
|