Files
settled-reach/docs/workshops/body-map-viewer/araminta-round2.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

953 lines
56 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 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 4–9×
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
"~17–18% 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 4–9× 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.09–0.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
4–9× 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 ~1–2 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.