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>
953 lines
56 KiB
Markdown
953 lines
56 KiB
Markdown
---
|
||
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.
|