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

19 KiB
Raw Blame History

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> 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.