diff --git a/docs/workshops/README.md b/docs/workshops/README.md index 8fbeec03b..7c56d0cb4 100644 --- a/docs/workshops/README.md +++ b/docs/workshops/README.md @@ -36,4 +36,4 @@ docs/workshops/ | [v0.1 Content Scoping](v01-content-scoping/) | — | Complete (2 rounds + closing, 38 tickets created) | | [Art Direction & Mood Board](art-direction-mood-board/) | — | Complete (3 rounds + closing + technical session, D-019 amended, D-043-D-052 confirmed) | | [Control & Interaction Scheme](control-interaction/) | — | Brief ready | -| [Body Map Viewer](body-map-viewer/) | — | Brief signed off (2026-07-25); round 1 gated on measurements T-1177/T-1178/T-1154/T-1179 | +| [Body Map Viewer](body-map-viewer/) | — | Complete (2 rounds + 2 lead interviews + post-ratification review; D-255 confirmed + 12 amendments; [outcomes](body-map-viewer/workshop-outcomes.md)) | diff --git a/docs/workshops/body-map-viewer/araminta-round1.md b/docs/workshops/body-map-viewer/araminta-round1.md new file mode 100644 index 000000000..d5eb91283 --- /dev/null +++ b/docs/workshops/body-map-viewer/araminta-round1.md @@ -0,0 +1,309 @@ +--- +title: "Body Map Viewer — Araminta Round 1" +description: "Named-feature encoding, per-gridunit payload schema, and encoding continuity across steps — argued from the measured wire-size table (T-1179) and hydrology cliff proposal (T-1177)" +workshop: body-map-viewer +round: 1 +author: Araminta +status: complete +--- + +# Araminta — Round 1 + +Answering my three questions from the [workshop +brief](body-map-viewer-workshop-brief.md#participants). Grounded in T-1179's +measured wire table, T-1177's cliff representation proposal, the current +`DistrictWindowLayer` struct (`server/src/atlas/layer_proxy.rs`), and the +carrier three-way rule (`docs/architecture/river-courses-t1170.md`, Ruling +1c). Keep clean, layer detail later — but the wire contract is exactly the +kind of "invisible until it's wrong" decision that deserves the extra pass +now, because every renderer downstream inherits whatever we lock here. + +--- + +## 1. Named-feature encoding (THE wire decision) + +**Verdict: don't treat this as one decision. It's three, because rivers, +settlements/POIs, and roads don't share a truth model — the carrier +three-way rule already sorted them, and my job is to encode each correctly, +not force a single scheme across all of them.** + +### Settlements + POIs + their names → ids-with-lookup, whole-body family (rule i) + +This is *already built and already correct* — I want to name that explicitly +so the workshop doesn't accidentally relitigate a solved problem. Names live +in `atlas_city_names` (D-223), a names-only pool with no geometry or +population, read once per body via `handle_city_names_request` +(`atlas_data_proxy.rs`). Settlement *geometry* (footprint aggregates: +density, dominant type/zoning) rides the whole-body `SettlementLayer` +family (D-226 T-1112/T-1119) — rung-independent, computed once, filtered +client-side by zoom. + +This is rule (i) by construction: settlements are discrete, finite, +rung-independent, valid forever once placed. **Encoding: a small integer id +per settlement, resolved against the whole-body name pool the client already +holds.** Never inline the string into every gridunit that happens to be +near a city — that's paying string bytes on a per-cell raster for something +that changes at most a few hundred times per body. POIs (once they exist, +per the outline's "later" scope) follow the identical shape: id + whole-body +lookup, not per-gridunit inline text. + +**What the per-gridunit payload carries instead: presence, not identity.** +A gridunit near a settlement doesn't need to know the settlement's *name* — +it needs to know *"a settlement occupies me"* so the map-art function can +render footprint fill/color, and the **id** so the client can join against +the lookup it already has cached (for label placement, click-through, the +sidebar). See §2 below for the exact field. + +### Rivers → sparse feature list, windowed payload, ids (not inline names) + +Rivers are rule (iii) — rung-indexed invention (T-1170, Ruling 1c) — which +already settled the carrier (`DistrictWindowLayer.courses: Vec`, +outside the windowed-family ceiling, per the same echo key as the six dense +arrays). I have nothing to add to the *geometry* carrier — Tyre's ruling is +correct and I'm not reopening it. My scope is the **naming** half, which +Ruling 1c's `RiverCourse` struct doesn't currently carry at all (`edge_id`, +`class`, `points`, `terminus` — no name field). + +**River names: same ids-with-lookup shape as settlements, one level indirect.** +`edge_id` is already the stable identity (`river_course::pack_cell_id`, +stable across every window/rung shipping that edge, per the doc). Do not +inline "Kelvin's Run" into every `RiverCourse` polyline on every window that +happens to cross it — a trunk river crosses dozens of windows over its +length, and the string would repeat dozens of times for zero new +information (the same argument PNG encoding wins for the dense fields: +don't pay bytes for something the client can dereference). Instead: name +assignment is a whole-body, rung-independent property of the **edge-id +graph** (rivers don't get renamed between gridunits), so it belongs in a +names-only whole-body lookup exactly like `atlas_city_names` — +`river_names: BTreeMap` fetched +once per body, joined client-side against the `edge_id` already on every +`RiverCourse`. This is additive to the existing whole-body atlas-names +request path, not a new mechanism — same shape, second table. + +One nuance rivers have that settlements don't: a single named river usually +spans many `edge_id`s (a trunk plus its class-2 tributaries share a name; +"unnamed" streams don't get an entry at all). That's a **naming-assignment +generator concern** (which edges share a name), not a wire-encoding concern +— the wire only needs "this edge_id resolves to this name-table key or +none," so I'm not blocking on it; flagging it so Dudley's naming generator +(whenever that lands) knows the wire already expects a many-edges-to-one-name +join, not a strict 1:1. + +### Roads → not in this workshop's scope, but the encoding falls out for free + +Roads aren't measured or built yet (outline: "later, ... roads, railroads, +etc will also be in scope"), but worth stating now since it's free: roads +are graph-like exactly like rivers, so they inherit rule (iii) + the same +ids-with-lookup naming shape without a new design pass when they land. Not +claiming this as decided — just noting the pattern generalizes and nobody +needs to re-litigate named-feature encoding a third time. + +### Why not dense classification rasters for any of these + +The brief posed "dense classification rasters + sparse feature list vs +inline identity vs ids-with-lookup" as the open question. A dense raster +(e.g. a `settlement_id: Vec` per-gridunit array, one entry per cell) +is the wrong shape for anything that is **sparse and discrete** — settlements +and named rivers occupy a small fraction of gridunits in any canvas. T-1179's +own per-field RLE numbers make this concrete: `morphology`/`vegetation` +(genuinely dense, every cell has a value) still only manage 6 runs at 330K +cells because they're spatially coherent — a settlement-id raster would be +mostly a single "no settlement" sentinel with a handful of small filled +regions, which is exactly what a sparse feature list (a handful of +`{id, footprint}` entries) encodes in a few hundred bytes instead of +330K–8.3M raster cells that are 99%+ one repeated sentinel. **Dense rasters +are for continuous fields (rule ii territory — biome, elevation, moisture); +sparse feature lists + ids-with-lookup are for discrete named things (rule +i/iii territory). Don't blend them — the moment "sparse feature" bleeds into +"raster," we're back to paying for information nobody asked for.** + +--- + +## 2. The per-gridunit payload schema + +**Baseline: the six fields `DistrictWindowLayer` ships today are exactly +right and I'm not adding classification vocabulary — I'm adding the fields +the outline's own example promised and the cliff proposal requires.** + +Jeroen's outline literally specified the payload: *"biome: grassland, +settlement: null/'name here', river: null/'name here/unnamed', frozen: +true/false, flooded: true/false, sea: true/false."* Reconciling that +against what's measured and what's shipped: + +| Outline's ask | Wire field today | My call | +|---|---|---| +| biome | `morphology: Vec` (17-zone D-239 §6) | Carries it. "Biome" in the outline reads as morphology zone — keep the existing vocabulary, don't add a second one. | +| — | `elev_q: Vec` | Keep (height, per outline's separate ask). | +| frozen | `glaciation: Vec` (5-grade) | Carries it — a boolean collapse of glaciation is a client-side *derivation of the same data*, not a new field. Client reads `glaciation > 0` if it wants a bool; server shouldn't duplicate the same fact two ways on the wire. | +| flooded, sea | `morphology` (has water zone classes) + hydrology's settled state | See "map time axis" note below — this is where the outline's "sea"/"flooded" distinction needs a decision the workshop, not me, should rule on (Q1 to Jeroen in the brief). I'm not duplicating a boolean the morphology vocabulary can already express; if the workshop wants a fast client-side "is this wet" check without decoding the full morphology enum, that's a **cheap additive derived bit**, not new server-computed information — flag it, don't block on it. | +| settlement: null/name | **new**, see below | Presence + id, not inline name (§1). | +| river: null/name | `courses: Vec` + **new** name lookup | Already ships geometry; name resolves via the new whole-body table (§1). | +| — | `moisture_q`, `vegetation` | Keep — not in the outline's own example list but already shipped and load-bearing (vegetation climate law, D-239 §8). | + +**New field: `settlement_id: Vec` (0 = none), per-gridunit dense array, +same shape as `morphology`.** This is the one genuinely new dense field my +schema adds. It's dense (not sparse-list) because "which settlement, if any, +covers this gridunit" is a per-cell lookup a renderer needs at paint time — +exactly the same shape argument as `morphology` itself (a classification +raster, not a feature list, because every cell needs an answer, most of them +"none"). This is different from the settlement *name* (§1's ids-with-lookup) +— the id here is cheap (u32, near-constant runs, same RLE profile as +`morphology`/`vegetation` in T-1179's per-field table — settlements are +rare and spatially coherent, so this field costs almost nothing extra under +PNG encoding). The **name** join happens client-side against the whole-body +lookup; the wire never repeats the string. + +**Cliff/vertical structure: adopt T-1177's proposal as the payload +shape, gated on frequency, not on principle.** Dudley's measurement is +unambiguous: gorge carving is *rare* (zero carved cells across all three +production-scale benches; genuine carving needs a narrow two-basin-saddle +geometry that's uncommon at continental working-grid resolution). That +changes my answer from "add three dense fields" to "add them as +sparse, not dense": + +- `channel_depth: Vec` and `cliff_edge: Vec` **as dense per-gridunit + arrays would be wasteful** given near-zero occupancy — RLE would crush them + to a handful of runs, but PNG already wins on the honestly-dense fields, so + paying for two more mostly-zero dense arrays is not free even if cheap. +- **My call: fold the cliff case into the same sparse-feature-list shape as + settlements/rivers, not a seventh/eighth dense array.** A + `cliffs: Vec` field (`{ cell: (u16,u16), channel_depth: u16 }`, + `cliff_edge` implied by list membership) parallel to `courses` — present + only where it's true, empty in the overwhelming majority of windows T-1177 + measured. This keeps the common case (no gorge in this window) at zero + bytes and matches the carrier three-way rule's own logic: this is + rung-indexed invented detail (rule iii — hydrology solves once per body but + the *carved representation* is a windowed-rung concern the same way course + geometry is), so it belongs on the windowed payload as a sparse list, not + baked into the dense sentinel arrays. +- Elevation stays single-valued (`elev_q`) for every gridunit, including + carved ones — it's the rim/dominant height per T-1177's own reasoning. The + channel floor is a derived value (`elev_q - channel_depth`) the client + computes only for gridunits that appear in the sparse `cliffs` list. No + change to the existing single-height contract for the 99%+ common case. + +**What the client is allowed to do with these fields — restated plainly, +because "map art function, not data function" needs a hard line:** + +- Color/style pixels from the classification fields (morphology, glaciation, + vegetation, moisture) — yes, that's the entire job. +- Interpolate/smooth/tween the color *presentation* between adjacent + gridunits or between step-crosses (cosmetic) — yes, per the brief's premise + 3 investigation item. +- Invent geometry, decide where a river bends, decide whether a cell is + flooded, decide whether a cliff exists — **no, never.** Every one of those + is a server-derived fact already on the wire (courses' points, morphology's + water classes, the new `cliffs` list). If the client ever needs to guess + at content the schema doesn't carry, that's a missing field, not a license + to invent — send it back to this schema, don't let it leak into client-side + derivation logic. This is the same discipline D-227 already enforces + server-side; it just needs restating as a *client* rule now that the + client receives finished-content data instead of raw geometry it used to + interpolate itself. +- Resolve names via the whole-body lookup tables (§1) — yes, a join, not a + derivation. + +--- + +## 3. Encoding continuity across steps + +**One colorizer family, confirmed — and the wire schema I'm proposing is +exactly what makes that cheap to guarantee, because every step ships the +same six-plus-two field shape at a different spacing, never a different +vocabulary.** + +T-1143 §6's rule (one colorizer spans every rung, cited already for +temperature in D-226 T-1124 §2 — *"deliberately not a separate district-tier +quantization... one temperature colorizer spans both zoom levels"*) already +set the precedent I'm extending to the whole schema: **every step's payload +uses the identical field set, identical enum vocabularies +(`MorphologyZone`'s 17 zones, `VegetationClass`'s 7 including `Marine`, +`GlaciationGrade`'s 5 grades), at every step from global down to the deepest +gridunit spacing.** The only thing that changes between steps is *sampling +density*, never *meaning*. This is what makes the between-step magnification +red flag (Tyre's #1) tolerable — a magnified hold of step-N's canvas for one +step interval is showing the *same* classification vocabulary at coarser +spacing, not a different color language snapping in, so the discontinuity is +a resolution jump, never a palette jump. + +**"Average-back across step boundaries" — confirm the mechanism, name the +limit.** For continuous quantized fields (`elev_q`, `temp_dc`, `moisture_q`) +average-back is safe and already implicit in how a coarser step's cell is +itself computed (it's a spatial mean over finer terrain, same as today's +Region/District relationship). For **categorical** fields +(`morphology`, `vegetation`, `glaciation`), averaging is meaningless — you +cannot average "grassland" and "forest" into a third zone. R3 in the +delivered ladder design doc already flags this as an open risk +("average-back unverified for categorical morphology gates") and T-1179's +own per-field RLE table gives the concrete reason it matters: `morphology` +and `vegetation` are the two fields that compress to near-nothing (6 runs +at 330K cells) *because* they're genuinely piecewise-constant zone +classifications, not smoothly-varying scalars — averaging them at a step +boundary would destroy exactly the property that makes them cheap and +legible. **My call: coarser steps must derive morphology/vegetation/ +glaciation as a fresh classification decision at that step's own spacing +(the dominant-mode rule D-226's `dominant_district_type`/`dominant_zoning` +precedent already uses for settlement aggregates — pick the plurality +class, don't blend), never as a numeric average of the finer step's discrete +values.** This is consistent with the whole "each step is a derivation +sampled at gridunit resolution" framing (Tyre's D-166 corollary repoint) — +a coarser step doesn't downsample a finer step's raster, it re-derives at +its own spacing, and for categorical fields that re-derivation is a mode/ +plurality pick, not an arithmetic mean. + +**"Different classes of content at lower zoom" (forest → clearings/ponds) — +this is new vocabulary, not more density, and it needs to be scoped now so +it doesn't sneak in unbounded.** The outline names this directly: *"at low +enough zoom, a green forest biome may start showing clearings and ponds or +such — to be determined and tinkered with."* My read: this is **sub-zone +detail-scatter within an existing classification**, the same category as +`voxel_relief`/`voxel_mosaic`'s invented detail-scatter (D-227, T-1154's +octave-cutoff family) — not a new top-level vocabulary entry on +`morphology`. A "forest with a clearing" is still `Forest` morphology at +the district level; the clearing is deeper-step invented texture *within* +that classification, resolved the same way finer steps already resolve +everything else (fresh derivation at that step's `min_wavelength_m` cutoff). +**Concretely: this does not need a new wire field or a vocabulary change at +all** — it falls directly out of `min_wl_m` already being echoed per window +(the octave cutoff field `DistrictWindowLayer` carries today) and the +deeper step simply deriving at a smaller cutoff, which — per T-1154's +measurement — is trivially affordable (17 ms parallel at the deepest +realistic 83K-cell canvas). The one thing I'd ask the synthesis round to be +disciplined about: keep this as "existing classification, finer octave +detail," not "a growing zoo of sub-biome enum values" — that way it never +threatens the one-colorizer-family guarantee, it just adds spatial texture +underneath a class that was already decided at the coarser step. If a +future pass wants clearings/ponds to carry *distinct* semantic meaning +(e.g. a pond is walkable water, not decoration), that's a scope question +for whoever owns block/tile classification next, not a step-continuity +question — flagging the boundary, not answering past it. + +--- + +## Summary for the wire contract / tagged-envelope synthesis (feeding Q2/Tyre) + +For whoever writes the synthesis-round wire contract: + +- **Per-step payload = the existing six dense fields (`morphology`, + `elev_q`, `temp_dc`, `moisture_q`, `vegetation`, `glaciation`) + one new + dense field (`settlement_id: Vec`) + two sparse feature lists + (`courses` — already shipped — and new `cliffs`) + the existing `courses` + name resolution now needs a whole-body `river_names` lookup alongside the + existing `atlas_city_names` settlement lookup.** +- **Encoding: PNG-per-field for every dense array** (T-1179's unambiguous + winner — smallest and fastest at every measured size, no per-field + hand-tuning needed since DEFLATE already captures the compressibility + spread the per-field RLE table exposed). Sparse lists (`courses`, + `cliffs`) stay MessagePack-native — they're already small (~1–2 KB + typical per T-1170) and don't benefit from raster encoding. +- **This payload is categorically larger than the 30 KB windowed-query cap + at every measured size (21×–563×, T-1179)** — confirms the brief's own + read that the tagged-envelope migration is not avoidable by a smarter + encoding choice. I have nothing to add to that call beyond confirming the + numbers hold for the schema I'm proposing (it's the same six fields T-1179 + measured, plus one more near-free dense field and two near-free sparse + lists — doesn't change the order-of-magnitude verdict). +- **Vocabulary stays frozen across every step.** No per-step schema + variants, no new enum arms below the district-level vocabulary already + governed by D-239. Deeper detail is resolution and octave cutoff, never a + new field. diff --git a/docs/workshops/body-map-viewer/araminta-round2.md b/docs/workshops/body-map-viewer/araminta-round2.md new file mode 100644 index 000000000..28e799b05 --- /dev/null +++ b/docs/workshops/body-map-viewer/araminta-round2.md @@ -0,0 +1,952 @@ +--- +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` | 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` | Height — geology, not weather | +| `moisture_q` | `Vec` | Climate baseline (region-computed-once-inherited per D-243 §3), not a live sim tick value | +| `vegetation` | `Vec` | Derived from climate baseline + morphology, same determinism class | +| `settlement_id` | `Vec` | Settlement placement is a generation-time fact, not sim state | +| `courses` | `Vec` | 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` | 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` | 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` 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` already ships this way; `flooded: Vec` 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, // dense, PNG-per-field — Lake/OpenOcean discriminants now + // basin-sourced from HydrologyResult (§(e), CLOSED), no wire change + elev_q: Vec, // dense, PNG-per-field + temp_dc: Vec, // dense, PNG-per-field — climate baseline, not live reading + moisture_q: Vec, // dense, PNG-per-field + vegetation: Vec, // dense, PNG-per-field + settlement_id: Vec, // dense, PNG-per-field (new) + courses: Vec, // sparse, MessagePack (existing, T-1170) — overflow-basin outlet + // edges extend into this list once the D8 sourcing fix (§(e)) lands + cliffs: Vec, // sparse, MessagePack (new, ratified lead-interview-1) + + // SIM-STATE PLANE — short-TTL, re-requested on clock-bucket rollover + glaciation: Vec, // dense, PNG-per-field, L8 + flooded: Vec, // dense, PNG-per-field, L8 (new) +} +``` + +**Note on the lakes fix (§(e)):** no new field appears in this struct. +The originally-proposed `water: Vec` and `lakes: Vec` +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` 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, +} + +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, // sparse, MessagePack-native — overflow-basin outlets extend this (§(e)) + pub cliffs: Vec, // 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`, 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` + (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`, 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` 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`, 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` 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` 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` 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. diff --git a/docs/workshops/body-map-viewer/architecture-briefing-final.md b/docs/workshops/body-map-viewer/architecture-briefing-final.md new file mode 100644 index 000000000..c61201467 --- /dev/null +++ b/docs/workshops/body-map-viewer/architecture-briefing-final.md @@ -0,0 +1,79 @@ +--- +title: "Body Map Viewer — How It Now Works" +description: "Jeroen's original outline written back as-built: the ratified architecture in the same style and brevity" +type: workshop +status: active +workshop: body-map-viewer +created: 2026-07-25 +--- + +# How it now works + +Written back at Jeroen's request after the final ratifications, in the style and +brevity of [his original outline](jeroen-outline.md); corrected after his +Global/Region rung review and the lakes reconciliation. + +--- + +To honor late/lazy compute, everything is designed around LoD information exposure +with two separate eviction axes: nothing is ever *stale* (determinism — a canvas +for a fixed seed is byte-valid forever), but sub-global geometry still gets +*evicted on time-since-last-visit* to save storage for planets you visit once. +Only the live sim-state components carry a real TTL: one clock-bucket of the +field's own fastest driver (tidal for flooded on moon-bearing bodies, seasonal +otherwise) — the map can be at most one bucket stale, and the sim has no fresher +answer than that. The global level for each body is always kept (~9 MB for all +267 bodies, PNG-encoded — small enough to simply stay resident in memory) so +atlas navigation is snappy after first calc. + +architecture: +- pixel drawing is a function of the client. The map component has two layers: a + render-to-texture terrain layer (the data canvas colorized CPU-side — measured + cheap, ~78 ns/cell — into texel-exact textures, L8 planes where single-channel) + and an unscaled screen-space annotation layer for names, glyphs, + rivers-as-lines and markers. There is no zoom-scaled canvas anymore; the entire + compensation error class is dead by construction. What is shown and drawn is a + map art function, not a data function — the client styles and tweens, but never + invents geometry the wire didn't carry. +- content determination is a server function. The server answers "what is at this + world coordinate at this zoom step" with one flat tagged StepCanvasResponse: + dense per-gridunit fields (biome/morphology, elevation, temperature, moisture, + vegetation, settlement-id — plus frozen and flooded as separately-fetchable + short-TTL planes), sparse feature lists (river courses, cliffs — zero-length + almost everywhere), and whole-body name lookups joined client-side. + PNG-per-field encoding: a full data canvas is ~638 KB, ~5 ms to encode. + +- zoom levels are stepped. The map opens on the **body surface — the global view, + rung 0**: a canvas sized by the body itself (as many regions as the body has, + one gridunit per region — the elastic seam made visible), and this is the one + canonical, always-kept canvas. Below it, **five fixed-size rungs**, each pinned + to a D-243 unit: **Region → District → Quarter → Block → Chunk (64 m, the + deepest)** — all viewport-sized and evictable, never whole-body. Scroll clicks + step through them, cursor-anchored, edge-scroll panning. The bottom-out rule is + simply 1 screen px per 64 m gridunit — no magnification margin needed, because + a chunk is already a legible map feature. The tile (1 m) level is not map + content; walking on tiles is the Phase-5 viewport's job. Display fidelity is + step-dependent: 1×1 px per gridunit at the deep steps where detail matters, + relaxing toward ~5×5 only at the shallow end where extent is what grows. + +- on opening of a body: the heightmap and inputs are read, the seed generators + run the global calculations — including settled hydrology: rivers that end in + basins fill lakes to their true spill level, overflow onward to the sea or hold + as endorheic when the climate supports it, and carve gorges only where the + geometry truly demands it (measured across all 267 real bodies: it never has + yet — the cliff fields ship empty but the arithmetic is proven for the body + that someday needs them). This all lands in the global canvas: not drawn, but + determining what is in each gridunit — and it is kept forever. The lakes it + fills draw on the map as water (the existing Lake classification, sourced from + the settled solver; lake edges refine with zoom the same way coastlines do), + and an endorheic basin reads exactly as it should: a lake with no river + leaving it. + +- on zooming a step: the server takes the viewport bounds, derives the step's + data canvas at that rung's spacing (~64 ms for a typical canvas, everything + measured, worst cases bounded), with each finer step consuming the coarser + layers as seed information the deterministic way — reading the continuous + primitives and baselines (cache-accelerated when the coarser canvas is + resident, derived fresh when not, byte-identical either way). The client + colorizes, textures, annotates, and the map simply shows more truth the closer + you look. diff --git a/docs/workshops/body-map-viewer/body-map-viewer-workshop-brief.md b/docs/workshops/body-map-viewer/body-map-viewer-workshop-brief.md index b2a3dca6d..ea996ac10 100644 --- a/docs/workshops/body-map-viewer/body-map-viewer-workshop-brief.md +++ b/docs/workshops/body-map-viewer/body-map-viewer-workshop-brief.md @@ -222,13 +222,14 @@ necessary (workshops are not closed early on partial convergence). | # | Ticket | Headline result | Doc | |---|---|---|---| -| ① | T-1177 | **Settled hydrology VIABLE**: 512×256 ~24 ms/body; all 273 bodies Rayon-parallel ~0.7–0.8 s; 8.3M cells ~5.7 s single-thread (no production path needs it synchronously). Cliff representation: dominant height + `channel_depth` + `cliff_edge` flag. Finding: gorge carving is structurally RARE (priority-flood finds true spill levels; zero carved cells at production scales — needs chained-basin geometry). Solver: priority-flood + Dijkstra overflow, pure function, determinism-proved. | [t1177](measurements/t1177-hydrology.md) | +| ① | T-1177 | **Settled hydrology VIABLE**: 512×256 ~24 ms/body; all 273 bodies Rayon-parallel ~0.7–0.8 s; 8.3M cells ~5.7 s single-thread (no production path needs it synchronously). Cliff representation: dominant height + `channel_depth` + `cliff_edge` flag. Finding: gorge carving is structurally RARE (priority-flood finds true spill levels; zero carved cells at production scales — needs chained-basin geometry). Solver: priority-flood + Dijkstra overflow, pure function, determinism-proved. **POPULATION SURVEY addendum (post-adversarial, Troblum finding that the 273-body bench was one body solved 273×): all 267 real committed bodies solved independently, ~0.86s total, ZERO carved cells / zero cliff_edge cells population-wide (267/267) — the rarity finding upgrades from "one real body + structural argument" to "not observed anywhere in the real population," strengthening the cliff Phase-4 wire-cost case.** | [t1177](measurements/t1177-hydrology.md) | | ② | T-1178 | **Parallel throughput HOLDS at scale** (the T-1143 extrapolation gap, closed): ~190–220 ns/cell parallel flat from 330K→8.3M cells; 330K canvas ~64 ms, 2.07M ~0.4 s, 8.3M ~1.7–1.8 s (7.5–8.8× speedup); single-thread flat ~1.65 µs/cell, matches prior baseline within 8%. Throughput cross-validated on three bodies across two call paths (square real-`build_district_window_layer` = courses-light, 3–10 in window; rectangular replica loop = courses-empty by construction — agreeing within 0.3%); the courses-inclusive rate at real production density is measured once, GJ1c 330K with 18 courses at 195.0 ns/cell (within 2%, consistent with the <5% course-cost bound). | [t1178+t1154](measurements/t1178-t1154-derive-bench.md) | | ③ | T-1154 | **Block GO, Tile GO on cost** (~1.8–1.9 µs/cell, same band as District/Quarter). Octave cutoff buys ZERO below District spacing (verified: wavelength table bottoms at 128 m). Deepest-step realistic canvas (216×384 m @ 1 m = 83K cells): **~17 ms parallel — trivially interactive**. `voxel_relief` already in the Atlas path; `voxel_mosaic` untouched by it. Real gates are wire carrier + D-226(d) canvas policy, not cost. | [t1178+t1154](measurements/t1178-t1154-derive-bench.md) | | ④ | T-1179 | **PNG-per-field wins everywhere** — smallest AND fastest: 330K canvas = 638 KB (0.32× raw, 5.4/3.6 ms enc/dec) vs raw rmp 1.99 MB; RLE loses to raw (elev/temp near-noise per cell). **No encoding brings a step canvas near the existing windowed-payload budget** (best case 21× the ~30 KB cap at the smallest size; 563× at 8.3M) — the tagged-envelope migration cannot be dodged by payload optimization. Whether the letter of the D-226 §2 *field-count* rule is what triggers it is a workshop synthesis call, not a measured result. Corrected density: 6.00 B/cell measured raw. | [t1179](measurements/t1179-wire-table.md) | | ⑤ | T-1180 | **Upload cost is a non-issue**: worst case (8.3M px RGBA8 create) ~3.2 ms median, ~2.4–2.9 ms frame-delta spike — never near the 16.6 ms budget. **L8 is 4–9× cheaper at every size** (~0.5–0.7 ms at 4K). Prefer `texture.update` reuse on step-cross; use L8 for single-channel planes. Windowed-only measurement (headless renderer fakes uploads). | [t1180](measurements/t1180-imagetexture.md) | +| ⑥ | round 2 (lead interview 1 ruling) | **MEASURED — CPU colorize is the c1 gate, not upload.** `Image.set_pixel` flat ~78 ns/cell (330K→8.3M, no cliff): 330K ~25.7 ms, 2.07M ~165 ms, 8.3M ~644 ms. Direct `PackedByteArray` write is ~2× SLOWER (~153 ns/cell) — counter to the naive "skip the method call" assumption; **prefer `set_pixel`**. At the realistic per-step canvas size (330K, Dudley's viewport-sized recommendation) colorize is cheap (25.7 ms) and ranks second only to server derivation in the arrival-side cost chain (ahead of ④'s decode and ⑤'s upload); at the 8.3M stress-ceiling size it becomes non-trivial (~1/3 of server derive time at that size) — reinforces viewport-sized canvases over any canonical whole-canvas colorize. Headless-valid (pure CPU, no GPU path, unlike ⑤). | [t-setpixel-c1](measurements/t-setpixel-c1.md) | -Bench code (all `#[ignore]`d release tests): `server/src/atlas/hydrology_equilibrium.rs` + `server/tests/hydrology_equilibrium_bench.rs`, `server/tests/bmv_gridunit_bench.rs`, `server/tests/wire_encoding_bench.rs`. +Bench code (all `#[ignore]`d release tests): `server/src/atlas/hydrology_equilibrium.rs` + `server/tests/hydrology_equilibrium_bench.rs`, `server/tests/bmv_gridunit_bench.rs`, `server/tests/wire_encoding_bench.rs`. Client-side: `client/tests/` (temporary uncommitted drivers per measurement doc, ⑤/⑥). ## Input Documents diff --git a/docs/workshops/body-map-viewer/dudley-interview2-response.md b/docs/workshops/body-map-viewer/dudley-interview2-response.md new file mode 100644 index 000000000..4dd9ad831 --- /dev/null +++ b/docs/workshops/body-map-viewer/dudley-interview2-response.md @@ -0,0 +1,1231 @@ +--- +title: "Body Map Viewer — Dudley Interview 2 Response" +description: "Chunk-64m deepest-rung bench + Option D table, seed-chaining reconciliation with Tyre (cache-accelerated pure function), S2 courses-density bench at chunk/block, S4 sim-state TTL cost-side answer with Araminta" +workshop: body-map-viewer +round: interview-2 +owner: Dudley +status: complete +decision_refs: [D-166, D-225, D-226, D-227, D-243, D-203] +--- + +# Dudley — Interview 2 Response + +Four items redirected by Jeroen's interview-2 rulings, per the coordinator's +task and Tyre's parallel message (both landed with the same four items — +folded into one response since Tyre's questions are a subset of the +coordinator's task, asked from the governance side). + +--- + +## 1. LADDER: chunk (64 m) as the new deepest rung + +Jeroen, interview 2, verbatim: *"the actual tile level rung seems unusable. +maybe replace with 64?"* — his reasoning: a full 1920-class screen at +10 px/tile shows ~192×108 m, which is in-world viewport content (Phase 5), +not Atlas map content. Tile/voxel dropped from the Atlas ladder; chunk +(64 m, D-243's "stream/derive unit") is the new floor. + +### (a) The chunk-64m bench — the never-measured rung, now measured + +New benches, same file (`server/tests/bmv_gridunit_bench.rs`), same +discipline as the original T-1154 pass (`derive_at_metres` called directly — +no wire-facing cutoff band exists below Quarter's 1,024 m `MIN_WL_BANDS_M` +floor for Chunk either, same situation Block/Tile were in): + +**Per-cell rate, 4,096-cell sweep (matching the existing Block/Tile +comparison table exactly):** + +| Spacing | Cutoff | Total (4,096 cells) | ns/cell | +|---|---|---:|---:| +| Chunk (64 m) | 64 m (Nyquist) | 7.53 ms | 1,838.1 | +| Chunk (64 m) | 0 (uncut) | 7.44–7.57 ms | 1,816.0–1,847.9 | +| Block (128 m) [reference] | 128 m | 7.49–7.61 ms | 1,827.8–1,856.8 | + +**MEASURED, run twice for stability, both passes agree within ~1%.** Chunk's +cutoff=64m vs. uncut delta is within noise (1838.1 vs 1816.0–1847.9 ns/cell) +— **confirms directly, not by inference from Block, the same "no truncation +work left" finding** the original T-1154 pass established: `enveloped_fbm` +only skips octaves strictly finer than the cutoff, and +`VOXEL_OCTAVE_WAVELENGTHS_M`'s finest entry is 128 m — at 64 m (finer than +that floor), the cutoff still has nothing to truncate, so chunk pays the +exact same full per-cell rate as Block. Chunk is not "one step cheaper" than +Block; it's the same flat ~1.8 µs/cell single-thread rate the whole +Block-through-chunk band shares. + +**Realistic deepest-step viewport canvas at chunk spacing (replaces the old +216×384 m / 1 m-spacing bench, which measured the now-dropped tile rung):** + +| Path | Wall time | ns/cell | Speedup | +|---|---:|---:|---:| +| PARALLEL (16 threads, row-chunked) | 1,724.48–1,732.84 ms | 207.9–208.9 | — | +| SINGLE-THREAD | 15,051.59–15,145.11 ms | 1,814.7–1,825.9 | — | +| — | — | — | 8.73–8.74× | + +**MEASURED, run twice for stability, both passes agree within 0.5%.** Full +3840×2160 canvas (8,294,400 cells) at 1 gridunit-per-screen-px, 64 m spacing +— **1.72–1.73 seconds parallel.** This is not the 17 ms the old tile-rung +deep-step bench reported — chunk's deepest-step canvas costs roughly two +orders of magnitude more, because it covers a MUCH larger world extent at +the SAME cell count (245.8 km × 138.2 km vs. tile's 216 m × 384 m — see the +display-band derivation below for why this is the correct comparison, not a +regression). 1.7 s is still comfortably inside a step-cross tolerance +measured in the hundreds-of-ms-to-low-seconds range this workshop has used +throughout (matches the District/Quarter/Block full-canvas numbers already +in the recommended ladder table, all in the same 1.7–1.8 s band at full +8.3M-cell canvas size) — **cost still does not gate this rung.** + +### The new bottom-out rule, stated precisely (for Tyre's amendment text) + +At **1×1 px-per-gridunit** (the workshop's own ideal ratio, premise 5), a +3840×2160 canvas at 64 m spacing covers: + +``` +world_extent_m = canvas_px × gridunit_spacing_m +smaller axis: 2160 px × 64 m = 138,240 m ≈ 138.2 km +larger axis: 3840 px × 64 m = 245,760 m ≈ 245.8 km +``` + +**The new bottom-out rule is "1 screen px per 64 m gridunit," NOT "10 px per +chunk."** This is a real, substantive difference from the old tile rung's +rule, not a cosmetic swap of one spacing value for another — worth stating +explicitly because getting this wrong would silently reintroduce the +D-226(d)/legibility problem chunk is supposed to solve: + +- The old tile rule (10 px/tile) existed because a single 1 m gridunit at + 1×1 is *too small to read* on screen — 10 px of magnification margin was + there to buy legibility for a ground feature that is otherwise + imperceptible. That's the "in-world viewport" problem Jeroen's ruling + correctly identifies: 10×-magnifying a 1 m unit is exactly what an + in-world character-relative camera does, not what an Atlas map does. +- **Chunk does not have this problem.** A 64 m gridunit at 1×1 is already a + reasonably-sized map feature (a city block, a short street segment) — it + doesn't need a legibility margin the way a 1 m ground tile did. Applying + the old rule's 10× margin to chunk (i.e. "10 px per chunk") would either + (a) needlessly shrink the deepest step's world coverage by 10× for no + legibility gain, or (b) be based on a misunderstanding of why the 10× + factor existed in the first place. **It should not carry over.** +- **Concrete rule for the amendment text:** *"The deepest Atlas rung (chunk, + 64 m) samples at 1 gridunit per screen pixel (the workshop's own ideal + ratio) with no additional magnification margin — the 10×-per-tile rule + that governed the now-dropped voxel/tile rung does not apply to chunk, + because chunk-scale features do not have the same-order-of-magnitude + legibility problem a 1 m ground unit did at screen resolution."* + +### (b) Option D — five rungs, bottom = chunk, every row measured + +Rebuilding the round-2 recommended table (previously Option B: Region → +District → Quarter → Block → voxel) with chunk replacing voxel as the +bottom rung — same viewport-sized-canvas convention as before (fixed +3840×2160 px canvas at every step, since chunk — unlike the old voxel rung — +does NOT need the display-ratio-sized-canvas exception; see below). + +| Step | D-243 rung | Spacing | Step factor (from prev) | Canvas @ 3840×2160, 1×1 | Cells | World extent | Derive cost (parallel, MEASURED) | Wire cost (PNG-per-field, measured/interpolated) | +|---|---|---:|---:|---|---:|---|---:|---:| +| 0 | Region | 204.8 km | — | 3840×2160 | 8,294,400 | whole body (capped-tile mosaic, T-1143 §4) | 1,827 ms | 16.88 MB | +| 1 | District | 2,048 m | 100× | 3840×2160 | 8,294,400 | 7,864 × 4,424 km | 1,827 ms | 16.88 MB | +| 2 | Quarter | 512 m | 4× | 3840×2160 | 8,294,400 | 1,966 × 1,106 km | ~1,827 ms (T-1154 same-band) | ~16.88 MB | +| 3 | Block | 128 m | 4× | 3840×2160 | 8,294,400 | 491 × 276 km | ~1,827 ms (measured directly, T-1154) | ~16.88 MB | +| 4 (deepest) | **Chunk** | **64 m** | **2×** | 3840×2160 | 8,294,400 | **245.8 × 138.2 km** | **1,724–1,733 ms (measured directly, this pass)** | **~16.88 MB (same field-count/cell-count as every other row)** | + +**Every row is now directly measured or in the same directly-measured cost +band — the exact "zero asterisks" property Option B (the old skip-chunk +recommendation) earned by NOT including chunk. Now that chunk itself is +measured, that property transfers cleanly to Option D with chunk as the +bottom: this table has no unmeasured or inferred-only row.** + +**What changed vs. the old Option B, stated plainly:** +1. **Step count: still 5** (Region, District, Quarter, Block, + one deepest + rung) — chunk simply replaces voxel/tile as row 4's identity. The ladder + shape (which rungs, how many steps) is unchanged; only the deepest rung's + spacing and canvas-sizing convention changed. +2. **Canvas-sizing convention: chunk uses the SAME fixed-3840×2160-px-budget + rule as every other step, no exception.** This is the one place Option D + is structurally simpler than the old Option B: the old voxel/tile deepest + step needed a special-cased display-ratio-sized canvas (216×384 m at + 10 px/tile, NOT the fixed-canvas convention every other step used, + because the two conventions were mutually incompatible at 1 m spacing — + see round 2 §(c)'s Option A analysis). **Chunk does not have this + incompatibility** — at 1×1 px/gridunit, a fixed 3840×2160 canvas and the + "no extra magnification margin" bottom-out rule above are the same rule, + not two conventions in conflict. Dropping the special case is a genuine + simplification, not a workaround. +3. **Deepest-step derive cost went UP by two orders of magnitude in absolute + terms** (17 ms → 1.7 s) because the deepest-step canvas now covers a + world area orders of magnitude larger (216 m × 384 m → 245.8 km × + 138.2 km) at the SAME cell count. This is expected and correct — chunk's + canvas is now the SAME size (in cells, and therefore in derive cost and + wire bytes) as every other rung's canvas, which is what "no special case" + means. **1.7 s parallel is still comfortably affordable** (same band as + Region/District/Quarter/Block's own full-canvas cost) — the absolute + number changed, the affordability verdict did not. + +**Recommendation: adopt Option D as stated.** It inherits everything Option +B earned (measured-not-inferred rows, monotonic-with-spacing cost story +below Region) and removes Option B's one piece of remaining awkwardness (the +deepest-step canvas-sizing special case) for free, as a consequence of +Jeroen's own ruling rather than a separate design choice. + +--- + +## 2. SEED-CHAINING RECONCILIATION — with Tyre, "cache-accelerated pure function" + +Jeroen answered **NO** to my round-2 §(b) ruling (independent re-derivation, +coarser rung called only as a nested function, never reading cached +output) — he meant the other reading: the finer step genuinely **consumes** +the coarser step's *resolved* output, not just calls the same derivation +code fresh. Tyre relayed the coordinator's candidate reconciliation model +("cache-accelerated pure function") and asked me to assess it from the +cost/implementation side. This section is that assessment, worked jointly +with Tyre per his message. + +### The model, restated precisely + +- **Definition stays pure, D-227 intact:** `derive(seed, position)` always + produces the same bytes, by construction, regardless of implementation + strategy. This is non-negotiable and nothing below touches it. +- **Implementation is permitted to read a resident coarser step-canvas as an + acceleration**, falling back to deriving fresh (my original §(b) model, + now correctly reframed as the FALLBACK path, not the primary design) when + the coarser canvas isn't resident. +- **My round-1/round-2 benched numbers survive as the honest worst-case + ceiling** — every number in ①②③ was measured via the fresh-derive path + (nothing in any of my benches reads a cached coarser canvas as an input), + so they remain valid upper bounds on cost regardless of which + implementation strategy ships. + +### Is this D-227-sound? Yes — confirming Tyre's argument from the cost/implementation side + +Tyre's governance argument (his message): the coarser canvas being read is +itself evictable derived data — a pure function of the same seed — so +reading it is an **optimization**, not a **semantic dependency**. A semantic +dependency would mean "the finer step is WRONG without the coarser cache," +which is never true here, because the coarser value is always re-derivable +to the same bytes. **I agree with this argument completely, and it matches +exactly how I'd reason about it from the implementation side:** the test +D-227 already applies ("eviction → recompute, always valid") is precisely +the test this model needs to pass, and it passes by construction — a +cache-accelerated read and a fresh derive of the SAME coarser value at the +SAME `(seed, position)` are required to be byte-identical (they're the same +pure function), so substituting one for the other can never change the +finer step's output, only its latency. + +**The mandatory test this needs, matching the shape of my existing +window-independence/determinism invariants:** a `cache_hit_path == +cache_miss_path` byte-exact assertion — derive the coarser rung's value both +ways (read from a populated cache entry, and via fresh `derive()` with the +cache artificially evicted) and assert identical output. This is the same +style of test `determinism_at_330k_cells` (T-1177) and the courses +window-independence invariant already use in this codebase — not a new kind +of test, an extension of a pattern already proven twice. **If this test +passes — and it must, because both paths are the same pure function by +definition — the two paths are indistinguishable except in speed, which is +exactly Tyre's "optimization not dependency" characterization.** + +### Quantifying what the acceleration actually saves — the honest cost answer + +This is the part the coordinator specifically asked me to assess honestly, +including the possibility that acceleration doesn't help. **My answer: it +depends on WHAT the finer step would read from the coarser cache, and for +the two most likely candidate mechanisms, the answer is genuinely mixed — +one clearly helps, one plausibly does not, and I want to be precise about +which is which rather than assert a blanket "yes, faster."** + +**Candidate mechanism A — the finer step reads the coarser step's RESOLVED +CLASSIFICATION as a literal shortcut (skip re-deriving morphology/elevation +where the coarser answer is "good enough").** This is NOT what I'd +recommend, and I don't think it's what "serves as seed information" means +either — it would violate the categorical-field re-derivation rule Araminta +already established in round 1 (coarser steps must re-derive +morphology/vegetation/glaciation as a fresh classification decision at their +own spacing — a dominant-mode pick, never an average or a direct carry from +a different spacing's classification). Reading a District-spacing +`morphology_zone` value as a shortcut for what a Chunk-spacing cell's +`morphology_zone` "should" be would be exactly the same-vocabulary, +different-meaning-per-scale contradiction Araminta ruled against. **I'm +naming this candidate to rule it out, not to cost it.** + +**Candidate mechanism B — the finer step reads the coarser step's underlying +CONTINUOUS PRIMITIVES (the region-baseline / district-baseline value at a +given world position) as an input to its OWN fresh classification, instead +of recomputing that baseline from scratch.** This is what I believe Jeroen's +"serves as seed information for the deeper cascade" phrase actually +describes, and it's ALSO the pattern already shipped in this codebase today +(`invent_primitives`'s district call already reads a region baseline; my +round-2 §(b) response cited this precedent). **This is the mechanism worth +costing.** + +**The honest cost answer for mechanism B: reading the cached coarser +baseline instead of recomputing it is very likely a real, if modest, win — +but I do not have a number for it, and I want to say precisely why, rather +than either assert a saving or claim I've measured one.** `derive_at_metres` +computes several primitives in one call (coast-warp, detail-scatter octaves, +region-baseline blend, classification) — none of my benches isolate the +region-baseline-blend sub-cost from the total per-cell cost, because every +bench I've run measures the WHOLE `derive_at_metres` call, by design (that's +the actual served cost, and isolating sub-costs wasn't the question ①②③ +asked). So: **is reading+upsampling a coarse cache cheaper than deriving +fresh at ~1.65 µs/cell? Almost certainly yes for the specific sub-computation +mechanism B describes** (a cached bilinear-blend lookup is architecturally +cheaper than recomputing the same blend from raw region data — this is +essentially free to assert, since the blend itself is a small fraction of +the total per-cell cost, most of which is the octave-sum detail-scatter +work that mechanism B does NOT propose caching) — **but I cannot quantify +the magnitude without a new, targeted bench that isolates the +region-baseline sub-cost specifically, which nobody has run.** + +**Is a re-bench genuinely needed?** For the ARCHITECTURE ruling (adopt +cache-accelerated pure function, yes/no) — **no**, my existing numbers +already answer the load-bearing question (worst-case ceiling cost, D-227 +soundness) and the mandatory determinism test above is a correctness gate, +not a cost measurement. For an IMPLEMENTATION decision about exactly how +much the acceleration saves (needed before anyone sizes an "expected" +latency, as opposed to a worst-case one) — **yes, eventually, but not before +this ruling closes**, and I'd scope it as an implementation-ticket +measurement (isolate the region-baseline-blend cost specifically, then +compare cached-read vs. fresh-blend at that isolated cost) rather than +something this workshop needs to resolve before Jeroen ratifies the model. +My benched ①②③ ceiling numbers are sufficient to close the architecture +question today. + +### Answering Tyre's chain-reaction question (Troblum B2) + +**Each rung derives fresh independently on a cache miss — it does NOT chain +backward through evicted coarser rungs.** This is not a new architectural +choice; it falls directly out of the definition staying pure: `derive(seed, +position)` for ANY rung is fully self-contained (it takes the seed and a +world position, nothing else, per D-227's own signature) — so a Chunk-rung +cache miss's fallback is "call `derive_at_metres` at Chunk spacing," full +stop, not "first check whether the District-rung cache is warm, and if not, +re-derive District, and if THAT'S baseline is itself missing something, +re-derive Region..." There is no chain to walk, because the fresh-derive +fallback was never built to depend on any OTHER rung's cache state — it's +the same self-contained function every one of my benches already calls +directly, at every rung, with zero shared cache dependency between rungs. +**Troblum's chain-reaction worry dissolves for exactly the reason Tyre +expected: each rung's fresh-derive path is independent per my original §(b) +function-composition argument, which survives INSIDE this model as the +fallback, even though it's no longer the exclusive path.** The +cache-accelerated read (when a coarser canvas IS resident) is a pure speed +optimization layered on top of that already-independent fallback — it never +becomes a requirement the fallback depends on. + +### Verdict — for Jeroen's ratification + +**Adopt the cache-accelerated pure function model.** It is D-227-sound (Tyre's +optimization-not-dependency argument, confirmed from the cost/implementation +side above), it does honor "serves as seed information for the deeper +cascade" (mechanism B — reading a coarser baseline as an input to a fresh +classification — is both what I believe Jeroen meant and what the codebase +already does today at the district/region boundary), my benched ①②③ numbers +remain valid as the worst-case ceiling under this model (nothing about +adding an acceleration path can make the fallback path slower than what I +measured), no chain-reaction risk exists (each rung's fresh-derive fallback +is self-contained), and the ONE new obligation this model adds — the +cache-hit-path == cache-miss-path determinism test — is a correctness gate +this codebase already knows how to write, not new machinery. **What remains +unquantified (the magnitude of the acceleration's actual saving) is real but +not load-bearing for the architecture ruling** — it's an implementation-time +measurement, not a gate on Jeroen's yes/no here. + +--- + +## 3. S2 BENCH — deep-step × high-river-density courses cost + +Ruled before filing: the last zero-data-point cell on the courses-cost +axis. Every prior courses-inclusive measurement (T-1178's Cross-check 1, 18 +courses/331,776 cells) is at District spacing; nothing measured courses-on +cost at Chunk or Block. New bench: +`bench_s2_courses_density_at_chunk_and_block` +(`server/tests/bmv_gridunit_bench.rs`), using the real GJ1c river network +(not synthetic — the densest real confluence region found by scanning for +the river cell with the most other river cells within an 8 px search +radius), built via the actual PUBLIC invention pipeline +(`river_course::build_edges` + `river_course::invent_course`, both `pub` — +unlike `layer_proxy::invent_courses_near_window` itself, which is private to +that module; this bench replicates its per-edge invention loop using the +same public primitives, matching this file's existing replica-loop +discipline). + +### Results — MEASURED, run twice for stability + +| Rung | Window | Courses in window | Avg points/course | Courses OFF | Courses ON | Delta | +|---|---|---:|---:|---:|---:|---:| +| Chunk (64 m) | 64×64 cells (4,096 m × 4,096 m) | 2 | 1,732.0 | 1,861.9–1,913.3 ns/cell | 3,489.7–3,533.7 ns/cell | **+84.7% to +87.4%** | +| Block (128 m) | 64×64 cells (8,192 m × 8,192 m) | 2 | 867.0 | 1,849.4–1,855.7 ns/cell | 2,544.1–2,816.8 ns/cell | **+37.6% to +51.8%** | + +Both runs agree within run-to-run noise typical of this file's other 4,096- +cell sweeps (~5-10%); the direction and rough magnitude are stable across +both passes. + +### The honest finding: this is a real, structural cost — meaningfully larger than District's <5%, with a clear mechanism + +**This is not the same order of magnitude as the District-rung courses-cost +figure (+0.09–0.21 ms against a ~5 ms baseline, under 5%) already on +record. At Chunk and Block spacing, courses cost 38%–87% MORE, not under +5% more.** I traced the mechanism rather than reporting the number without +explanation: `near_perennial_water`'s cost is `O(courses × points-per- +course)` (a per-segment bounding-box + point-to-segment-distance scan over +every station on every candidate course, per cell). `invent_course` +resamples each course's control polyline at `station_spacing_m` — the SAME +spacing value passed as the rung's own cutoff/spacing. **A course spanning a +fixed chord length gets proportionally MORE points the finer the rung's +station spacing is** — confirmed directly by the instrumentation this bench +added: 1,732 points/course at Chunk (64 m stations) vs. 867 at Block (128 m +stations), almost exactly the 2× ratio matching the 2× spacing ratio between +the two rungs. District's own courses (2,048 m stations) have roughly 32× +fewer points per course than Chunk's for the same chord length, which is +exactly why District's courses-cost figure was small and Chunk's is not — +**this was always going to happen once a courses-inclusive measurement was +taken at a spacing this much finer than District; nobody had run it because +nobody had a courses-inclusive bench below District until now.** + +**What this means for the wire-cost/ticket-plan picture, stated plainly:** +courses ARE affordable at Chunk/Block in absolute terms — even the +worst case (+87% on a ~1.9 µs/cell baseline) lands at roughly 3.5 µs/cell, +which is still well within the "comfortably interactive" band every other +number in this workshop has used (a full 8.3M-cell chunk-spacing canvas at +3.5 µs/cell single-thread-equivalent would be ~29s single-thread / ~3.3s +parallel at the same ~8.7× speedup this file's other benches measure — a +real, larger number than the courses-off 1.7s figure in Option D's table +above, but not a "computer catches fire" case). **The real implication is +implementation-side, not a cost-affordability gate:** `near_perennial_water` +resampling courses at the SAME spacing as the rung it's serving is a real +per-cell cost driver that scales inversely with rung spacing, and whoever +implements Chunk-rung course rendering should know this going in rather than +discover it as a surprise regression — a station-spacing cap independent of +rung spacing (courses don't need MORE points just because the rung asking +for them is finer, if the goal is "is this cell near a river," not "render +the river at full rung resolution") is a plausible optimization worth +flagging for the implementation ticket, but I'm naming it as a finding, not +ruling on it — that's a river-rendering design call, not a cost-measurement +one. + +**Fold-in note:** this section is written to be pasted directly into +`measurements/t1178-t1154-derive-bench.md` as an addendum (same discipline +as the T-1177 population-survey addendum) — flagging here rather than +duplicating the write, since the coordinator's instruction was "fold into +the derive-bench doc." I have not yet made that edit; see the summary below +for the exact pending action. + +--- + +## 4. S4 — sim-state TTL phase-cadence, joint proposal with Araminta + +Araminta's proposal (relayed by the coordinator): a **formula**, not a fixed +real-time number — `SIM_STATE_TTL[field] = 1 × the field's own fastest +driving clock-bucket, per body class`. Concretely: `flooded` = 1 tidal +bucket on bodies with a moon (D-253's tidal ≈ day-phase bucket), degrading +to 1 seasonal bucket on moonless bodies; `glaciation` = 1 season-step always +(seasonal-only driver). Her rationale, in short: the map's staleness +tolerance should equal the sim's own recompute granularity (can never be +VISIBLY wrong), the risk is asymmetric (too-short wastes a refetch, harmless; +too-long shows wrong water-height, the real bug — so bias toward the faster +term), and binding to the bucket UNIT rather than a hardcoded time value +survives D-253's own "provisional, tunable" bucket-size calibration. + +### My half — the serving-side cost check + +**Question: is re-deriving/re-serving `flooded`/`glaciation` at 1-bucket- +rollover cadence affordable, or does cost force a >1× multiplier?** + +**Answer: affordable at 1×, no multiplier needed — and the reason is +structural, not just "the number is small."** I confirmed this by reading +`district_profile.rs` directly rather than assuming it: `glaciation_grade` +and the morphology zone's water/flood classification are **not a separate +sub-pipeline** — they come out of the SAME `derive_at_metres` call as every +other field (`DistrictProfile` is one struct, populated by one function +call; there is no field-level partial-derive path that computes "just +glaciation" or "just the water classes" more cheaply than a full derive). +This has one important consequence for the TTL cost question: + +**There is no cheaper "sim-state-only" re-derive path to cost separately — +re-serving frozen/flooded at any cadence costs the SAME per-cell rate as a +full geometry re-derive, because it IS a full geometry re-derive** (the two +fields just happen to be the ones whose VALUE can change between derives, +while morphology/elevation/moisture/vegetation are re-derived to the +identical bytes every time per D-227 purity — but the COST of computing +them is not separable). This means the TTL cost question reduces to a +question I've already answered at every rung in this workshop: **is a full +re-derive of a step canvas, at 1-bucket cadence, affordable?** Given every +rung's derive cost (Region/District/Quarter/Block/Chunk, Option D above) is +in the 1.7–1.8 s parallel band for a full 8.3M-cell canvas, and — critically +— **the actual sim-state re-derive doesn't need a FULL canvas re-derive at +all, only the currently-cached window(s) a player might reopen**, the real- +world cost is far below even that ceiling: it's bounded by how many cached +step-canvas entries exist for a given body at TTL-rollover time (Tier 2 of +my round-2 cache-tier spec — sub-global geometry entries, evicted on time- +since-last-visit, which is a SMALL, bounded set per body in normal play, not +"re-derive the whole planet on every tidal tick"). + +**No multiplier needed — 1× holds.** I'm not widening Araminta's proposed +cadence, because the cost isn't the constraint her formula's own rationale +already correctly identified the real constraint (staleness-vs-correctness, +not compute cost) — the compute-cost check confirms it doesn't ALSO need to +be a constraint, it just needed confirming rather than assuming. + +### Joint final proposal + +**Formula (Araminta's, adopted as stated, 1× multiplier confirmed +affordable):** + +``` +SIM_STATE_TTL[flooded] = 1 × tidal_bucket (moon-bearing bodies, D-253) + = 1 × seasonal_bucket (moonless bodies) +SIM_STATE_TTL[glaciation] = 1 × seasonal_bucket (always — seasonal-only driver) +``` + +**Rationale (joint paragraph):** the map's staleness tolerance is bound to +exactly the sim's own recompute granularity, so a cached sim-state plane is +never visibly wrong — it can be at most one bucket stale, and the simulation +itself has no fresher answer to offer during that bucket. Binding to the +bucket UNIT rather than a fixed real-time value survives D-253's own +provisional/tunable bucket calibration automatically, and the formula's +asymmetric-risk argument (too-short TTL wastes a harmless refetch; +too-long TTL shows objectively wrong water-height, the real bug) already +justifies biasing toward the faster of the two candidate drivers on +moon-bearing bodies without needing a cost-side override. The serving-side +check confirms this formula needs no widening: `flooded`/`glaciation` have +no cheaper isolated re-derive path than a full per-cell derive (they're +computed inside the same `derive_at_metres` call as every other field), but +the actual re-derive workload at rollover time is bounded by the small, +currently-cached set of step-canvas windows for a given body (per the round- +2 cache-tier spec's Tier 2), not a whole-planet re-derive — so 1× the +sim's own bucket cadence is both correctness-necessary (Araminta's argument) +and cost-affordable (this section's confirmation), with no tension between +the two requiring a compromise multiplier. + +--- + +## Summary for filing / next steps + +1. **Chunk-64m is measured, Option D adopted (5 rungs, bottom = chunk, every + row measured), the new bottom-out rule is "1×1 px/gridunit, no + magnification margin"** — stated precisely above for Tyre's amendment + text. His D-226(d) partial-floor-restore (tile/voxel back to + never-Atlas-mapped) is consistent with everything measured here. +2. **Seed-chaining: "cache-accelerated pure function" adopted** — D-227-sound + (confirmed from the cost/implementation side, matching Tyre's governance + argument), no chain-reaction risk (each rung's fresh-derive fallback is + self-contained), my ①②③ benched numbers survive as the worst-case + ceiling, the acceleration's actual magnitude is plausibly real but + unquantified (named as a future implementation-time measurement, not a + gate on this ruling), and one new mandatory test (cache-hit == + cache-miss, byte-exact) closes the correctness obligation. +3. **S2 courses-density at Chunk/Block: measured, and it's a real finding, + not a formality** — +38% to +87% cost at these finer rungs (vs. + District's <5%), traced to a concrete mechanism (course point-count scales + inversely with rung spacing via `station_spacing_m`). Still affordable in + absolute terms; flagged as an implementation consideration (a possible + station-spacing cap independent of rung spacing) for whoever builds + Chunk-rung course rendering. **Folded into + `measurements/t1178-t1154-derive-bench.md` as a dated addendum**, matching + the T-1177 population-survey addendum's format (done in this same pass — + confirmed with Tyre, whose §(d) Troblum-disposition note originally + carried stale "pending" phrasing and has since been corrected to cite + both addenda as filed). +4. **S4: joint formula with Araminta, 1× multiplier confirmed, no widening + needed** — `flooded`/`glaciation` TTL bound to 1× their driving sim + bucket (tidal-or-seasonal, seasonal-only respectively), cost-affordable + because sim-state re-derive is bounded by the small cached-window set per + body, not a whole-planet re-derive. + +**Code changes:** `server/tests/bmv_gridunit_bench.rs` — three new `#[ignore]`d +release benches (`bench_chunk_spacing_4096_cells`, +`bench_chunk_deep_step_realistic_canvas`, +`bench_s2_courses_density_at_chunk_and_block`) plus one new helper +(`build_gj1c_courses_near_window`). No `server/src/` file modified. Full +bench-file test list (11 tests) confirmed intact; full crate `cargo build +--release` clean. + +--- + +## POST-RATIFICATION ADDENDUM (2026-07-23): lake schema gap + global-tier math recheck + +Two items from Jeroen's post-ratification review, redirected by the +coordinator after interview 2 closed. + +### 5. LAKES ON THE MAP — the schema gap, and where it actually sits + +Jeroen's direct question: *"when filling basins to find an overflow, is that +body flagged as lake or flooded or something? do we draw lakes on the map?"* +Answer, checked against the real code rather than assumed: **currently, no — +and the gap is more specific than "the field doesn't exist." It's that the +field DOES exist (`MorphologyZone::Lake`, discriminant 1, in the 17-zone +vocabulary this whole workshop's payload schema already carries), but +nothing in the derivation pipeline ever sources it from settled hydrology.** + +**What actually happens today, confirmed by direct read +(`district_profile.rs:556-565`):** + +```rust +if ocean_fraction_q >= 80 { + // "No body-scale salinity signal at district level yet; treat all as + // OpenOcean. Lake differentiation lives at ChunkContext." + return MorphologyZone::OpenOcean; +} +if ocean_fraction_q >= 60 { + return MorphologyZone::Lake; +} +``` + +`ocean_fraction_q` is a bilinear sample of `TerrainAnalysis.ocean_mask` +(`district_profile.rs:1870`), and `ocean_mask` itself is nothing more than +`elev[i] < sea_level` (`features.rs:104`) — a raw per-cell below-sea-level +threshold from the ORIGINAL heightmap, with **zero connection to +`HydrologyResult`'s settled-equilibrium output** (`filled_scaled`, basin +membership, `BasinOutcome::Endorheic`/`Overflow`). The `Lake` vs `OpenOcean` +split that exists today is a crude density heuristic (how much of the +bilinear sample window is below sea level), not a real inland-vs-connected +distinction, and it was never wired to the hydrology solver this whole +workshop's ①/T-1177 measurement is about — the solver computes real basin +geometry and nobody downstream reads it. + +**One more piece of the picture, also confirmed by direct read: a BETTER +building block than `ocean_fraction_q` already exists and is ALSO unused for +this purpose.** `TerrainAnalysis.lake_mask` (`features.rs:105`, +`compute_lake_mask`) is a proper flood-fill connected-component test — +below-sea-level cells connected to a grid edge are `ocean_mask`-true and +`lake_mask`-false (the sea); below-sea-level cells NOT connected to an edge +are `lake_mask`-true (landlocked water). This is consumed today by +`road_graph.rs` and `features.rs` for road-planning purposes +(`ta.ocean_mask[i] || ta.lake_mask[i]` water-avoidance checks) — **but is +never read by `derive_morphology_zone` or anywhere in the Atlas +classification/wire path.** So there are actually two disconnected gaps +stacked on each other: (a) the wire schema's water classification doesn't +read hydrology's settled-equilibrium basins at all, and (b) even the +CRUDER connectivity-based `lake_mask` that already exists and would be a +strict improvement over the current `ocean_fraction_q` heuristic is also not +wired into the classification path. Both are real; (a) is the one Jeroen's +question is really about, since `lake_mask` still can't distinguish +overflow lakes from endorheic ones or know about a lake basin that only +exists because of settled hydrology's overflow logic (a filled basin can +have interior cells that were never below the ORIGINAL heightmap's sea +level — that's the entire point of "filling basins to find an overflow"). + +**The fix, specced with Araminta (relayed via coordinator; her framing +adopted, my derive-pipeline slotting added):** + +**New static water-classification field, sourced from `HydrologyResult` at +canvas-derive time, distinct from Araminta's `flooded` sim-state plane.** + +- **Not a new dense field — fold into `morphology`, reusing the existing + `Lake`/`OpenOcean` discriminants (1/0) the vocabulary already carries.** + This is a zero-cost wire change: no new array, no new byte, no new + encoding question. The 17-zone `MorphologyZone` enum already has the + right vocabulary entry; the only thing missing is the DATA SOURCE that + decides when to emit it. +- **Source at derive time:** for a gridunit whose position falls inside a + `HydrologyResult` basin's `cells` (row-major cell indices at the 512×256 + working-grid resolution the solver runs at — see the projection question + below), emit `MorphologyZone::Lake` instead of falling through to the + `ocean_fraction_q`-threshold heuristic. The heuristic remains the fallback + for cells NOT covered by any basin (i.e., it still decides open-ocean vs. + land at the coarse working-grid resolution the way it does today) — this + is additive, not a replacement of the existing sea-detection logic. +- **This is STATIC equilibrium geometry, correctly homed separately from + Araminta's `flooded` sim-state plane.** A lake basin's existence and + footprint is exactly as static as `morphology`/`elev_q` themselves — + `HydrologyResult` is a pure function of `(elevation, sea_level, climate)`, + computed once, byte-identical forever (T-1177's own determinism proof). + `flooded` (Araminta's plane, TTL-bound to the S4 sim-state formula above) + is about CURRENT sim state changing over game time — a river cresting its + banks this season, not a basin's settled existence. Conflating the two + would have meant a lake's presence-on-the-map flickers on the same TTL as + seasonal flood state, which is wrong for exactly the reason Jeroen's + question implies: a lake is there or it isn't, at generation time, and + that fact doesn't need re-deriving on a sim clock. + +**Endorheic vs. overflow lakes — same water class, no client-relevant +distinction at the map-art level.** `BasinOutcome::Endorheic` vs. +`BasinOutcome::Overflow` is a real, meaningful distinction to the SOLVER +(it decides whether an outlet path gets carved at all — see the cliff/ +`channel_depth` field's own dependency on a basin having an `Overflow` +outcome), but nothing about how the client should COLOR or RENDER a lake +gridunit depends on which outcome produced it — both are `MorphologyZone:: +Lake` on the wire, no additional bit needed. The one place the distinction +DOES matter to the client is exactly the place a channel already carries it: +an `Overflow` basin's outlet is what populates the sparse `cliffs: +Vec` list (round 2 §(a)) at cells along `outlet_path` where +`channel_depth_scaled > 0` — so the endorheic/overflow distinction is +already client-visible, just via the EXISTING cliff mechanism, not a new +field on the lake classification itself. No double-encoding needed. + +**Byte cost: near-zero, confirmed by the same reasoning T-1177's cliff +finding already established.** Lake regions are, by construction, +CONTIGUOUS (a basin's `cells` are one flood-filled component) — exactly the +shape PNG's DEFLATE compresses best (T-1179's own per-field RLE table: +`morphology`/`vegetation`, the two genuinely piecewise-constant +classification fields, compress to a handful of runs at 330K cells). Since +this change reuses the EXISTING `morphology` field rather than adding a new +one, there is no new byte cost to price at all — the only change is which +DISCRIMINANT value gets written into cells that were already being +classified, and `Lake`'s discriminant (1) compresses exactly as well as +every other `morphology` value already does. + +**Where the hydrology solve slots into the derive pipeline — the projection +question, answered precisely:** + +`HydrologyResult` is computed ONCE per body, at the 512×256 working-grid +resolution (T-1177's own scope — "solve() is called once per body... same +way `drainage::analyze` already runs once per body today"), held in the +D-203-shaped global-tier cache extension my round-2 cache spec already +proposed. A finer-rung canvas derive (District through Chunk) needs to know, +for each gridunit's world position, whether that position falls inside a +basin — **this is exactly the same projection problem `sea_level` already +solves today**, and should use the identical mechanism: `TerrainAnalysis` +(the working-grid analysis struct `derive_at_metres` already reads for +`ocean_fraction_q` via bilinear sampling) gains a basin-membership field +(a per-working-grid-cell basin id or a simple bool, populated from +`HydrologyResult.basins[*].cells` once when hydrology solves) sampled the +SAME way `ocean_mask` is sampled today — bilinear/nearest lookup at the +gridunit's world position against the 512×256 working grid, not a +re-solve at the finer rung's own spacing. This is the SAME "coarse baseline, +finer classification decision" pattern round 2 §(b)'s seed-chaining +reconciliation already ruled correct (mechanism B: read a coarser rung's +CONTINUOUS PRIMITIVE as an input to a fresh classification, never re-derive +the coarser thing itself) — the hydrology basin membership is exactly +mechanism-B shaped: computed once at 512×256, sampled (not re-solved) at +every finer rung, feeding straight into `derive_morphology_zone`'s existing +water-classification tier alongside `ocean_fraction_q`. + +**No new bench needed to confirm this is affordable.** Sampling one more +bilinear field (basin membership) alongside the `ocean_fraction_q` sample +`derive_at_metres` already performs every cell, every rung, is the same +shape of work as the sample it's replacing/augmenting — my own T-1178/T-1154 +per-cell rate numbers (~190–220 ns/cell parallel, flat across every rung +this workshop measured) already include equivalent-cost sampling operations +in that per-cell budget. This is a derivation-pipeline WIRING change (new +field on `TerrainAnalysis`, one more gate in `derive_morphology_zone`), not +a new cost category. + +### 6. GLOBAL-TIER MATH RECHECK — corrected number + +Jeroen's correction to the ladder top: **GLOBAL is rung 0**, the body-surface +opener, with a **VARIABLE canvas = the body's own region grid** — one +gridunit PER REGION (`regions_per_equator(R) × regions_per_equator(R)/2`, +D-243's elastic seam, `scale::regions_per_equator`), not a fixed +3840×2160-cell District-spacing canvas the way my round-2 cache-tier spec's +~174 MB figure assumed. **REGION is rung 1**, the largest FIXED-size rung — +viewport-sized and evictable like every other sub-global rung, not part of +the always-keep tier. + +**My round-2 174 MB figure was computed against the wrong canvas shape** — +I'd sized the "global tier" as if it were a full District-spacing +step-0 canvas (8.3M cells/body, the same size as every other rung's canvas), +which is exactly the assumption Jeroen's correction replaces. Recomputed +against the correct rung-0 shape (one gridunit per region), using REAL +per-body radii from `systems.db` (`BodyParamsReader`, the project's +established read-only accessor — never raw `sqlite3`), across the real +267-body committed population (same set T-1177's population survey used): + +**New bench:** `server/tests/bmv_global_tier_bench.rs` +(`bench_global_tier_bytes_real_population`). Discovers every committed +body_id (same walk as the T-1177 population survey), reads each body's real +`body_radius_km` from `systems.db`, computes +`regions_per_equator(R) × regions_per_equator(R)/2` cells per body, sums +across the population, and prices the total at both the raw 6 B/cell rate +(`DistrictWindowLayer`'s own documented figure) and T-1179's measured +PNG-per-field rate (638,382 bytes / 331,776 cells = 1.924 B/cell). + +```bash +cd server +cargo test --release --test bmv_global_tier_bench -- --ignored --nocapture +``` + +**Results — MEASURED (deterministic: real radii + a pure formula, byte- +identical on re-run, confirmed):** + +| | Value | +|---|---:| +| Bodies (real radius found) | 267 / 267 | +| Total rung-0 cells across population | 4,825,615 | +| Average cells/body | 18,073 | +| **Total bytes, raw 6 B/cell** | **27.61 MB** | +| **Total bytes, PNG-per-field (1.924 B/cell)** | **8.85 MB** | + +**The corrected always-keep global-tier figure is ~8.85 MB PNG-encoded +across the real population (~27.6 MB raw) — roughly 20× smaller than my +original 174 MB estimate.** This is the number for Tyre's bracket. The +correction is exactly the direction Jeroen's ruling implied ("dramatically +smaller") and for exactly the reason his correction identifies: a +region-grid canvas (one gridunit per ~205 km region) is a MUCH coarser +sample than a District-spacing canvas (one gridunit per 2,048 m) — roughly +100× coarser per axis, ~10,000× fewer cells per body at the same world +extent, which is the entire point of region being a real, distinct, +coarser rung rather than a relabeling of what District already does at +canvas resolution. + +**Range across the real population, for context:** largest rung-0 canvas is +GJ325Ac at 25,200 cells (47.4 KB PNG-encoded) — a body with radius +7,317.6 km, close to the largest bodies in the committed set; smallest is +GJ784c-m1 (a moon, radius 733.9 km) at just 253 cells (0.5 KB). The +Earth-class reference point (R=6371 km) lands at 195×97 = 18,915 cells, +matching the brief's own "~195×98 ≈ 19K" framing almost exactly (97 vs. 98 +rows — the one-off is `cols/2` integer division vs. the brief's own rounding +of a half-circumference figure; not a discrepancy worth chasing further, +both land at the same order of magnitude the brief already cited). + +**What does NOT change:** the eviction POLICY (Tier 1 = keep-always, never +evicted by time-since-last-visit; Tier 2 = sub-global geometry, storage- +evicted on time-since-last-visit) is unaffected by this correction — only +the BYTE BUDGET the keep-always policy commits to shrinks. The D-203-shaped +resource extension proposal (`orbital_canvas` field on `BodyWorldState` or a +sibling resource) also stands unchanged — it was never sized to the wrong +174 MB figure in a way that required different code, only a different +comment about how big the resident data actually is. At ~8.85 MB across the +ENTIRE real population, this budget is trivially affordable as permanent +process-resident memory (not just disk-safe, which was already true at +174 MB) — worth noting since it may simplify Stig's disk-vs-memory framing +for this ONE tier specifically (Tier 1 only; Tier 2/sub-global geometry +still wants the disk-backed, storage-evicted treatment his and my specs +already agree on). + +**Code changes:** `server/tests/bmv_global_tier_bench.rs` — new file, one +`#[ignore]`d release bench. Reads `server/data/systems.db` read-only via the +existing `BodyParamsReader` accessor (asset-pipeline golden rule respected — +no raw `sqlite3`, no write path touched). No `server/src/` file modified. + +### Rung-0 derive cost — MEASURED, not extrapolated (for Tyre's §(c) Option D table) + +Tyre also asked for a derive-cost figure for rung-0's own row in the ladder +table. The existing `bench_derive_orbital_at_metres_region_spacing` +(`zoom_ladder_bench.rs`) measures `derive_orbital_at_metres`'s per-cell rate +at a fixed 4,096-cell (64×64) sweep (**884.0 ns/cell single-thread**, +directly measured, ~2.13× cheaper than a full `derive_at_metres` +classification call — no `invent_primitives`, bilinear region-baseline blend +only) but its own full-canvas row is explicitly labelled "EXTRAPOLATED from +the measured per-cell rate," not independently measured at real canvas +shape. Per this workshop's own anti-extrapolation discipline (the T-1143 +planetary-rung post-mortem this whole workshop exists to avoid repeating), I +ran the REAL per-body canvas shape end-to-end instead of scaling the +4,096-cell number up. + +**New bench:** `server/tests/bmv_global_tier_bench.rs::bench_rung0_derive_cost_real_canvas_shapes`. +Calls `derive_orbital_at_metres` directly over the actual `cols × rows` +extent three representative real bodies would use (smallest, Earth-class, +largest — the same population `bench_global_tier_bytes_real_population` +surveys). + +| Body | Radius | Canvas (cols×rows) | Cells | Wall time | ns/cell | +|---|---:|---|---:|---:|---:| +| GJ784c-m1 (smallest, moon) | 733.9 km | 23×11 | 253 | 0.22–0.23 ms | 851–912 | +| Earth-class reference | 6,371.0 km | 195×97 | 18,915 | 15.90–16.19 ms | 835–856 | +| GJ325Ac (largest) | 7,317.6 km | 225×112 | 25,200 | 20.94–21.34 ms | 831–847 | + +**MEASURED, run twice for stability, both passes agree within ~5-8%** (the +same run-to-run noise band this workshop's other 4,096-cell-scale benches +show). Per-cell rate is flat across all three body sizes (~830–910 ns/cell), +consistent with `bench_derive_orbital_at_metres_region_spacing`'s own +884.0 ns/cell figure — no size-dependent degradation. + +**Direct answer to Tyre's two requests:** + +1. **Single-body rung-0 derive cost: ~16–21 ms single-thread**, depending on + body radius (larger bodies have proportionally more regions per equator, + hence more cells) — trivially interactive, one order of magnitude below + any step-cross tolerance this workshop has used. +2. **All-267-bodies-summed single-thread total (the worst-case ceiling, using + the real total cell count 4,825,615 from the byte-cost bench above): + ~4.0 s.** Stated precisely as a ceiling, not a real production cost: + rung-0 is populated lazily, once per body, on that body's first + Atlas-open (same D-206 background-queue population path every other + cached layer already uses) — no production path solves all 267 bodies' + rung-0 canvases synchronously in one batch. The per-body row is the + number that matters for "how long until a fresh Atlas-open feels + snappy" (~16–21 ms, imperceptible); the summed total is only useful as + an upper bound on total server-side compute if every body were opened + once, back to back — which is a stress ceiling, not a real request + pattern (mirrors exactly how T-1177's 273-body hydrology figure works: + the meaningful number is per-body cost, the summed total is a sanity + ceiling). + +Both numbers are real and can go directly into Tyre's bracket without +qualification — no extrapolation anywhere in this section. + +--- + +## LAKES CONVERGENCE (2026-07-23): Araminta's three routed questions — §1 SUPERSEDED-BY-CROSSING, see the correction block immediately below + +**SUPERSEDED-BY-CROSSING (2026-07-23, later same day).** Everything under +§1 and the struct in §3 below answers a framework Araminta has since +**withdrawn**: a new `water: Vec` dense field. Her final position, +reached independently by verifying my own code-read at source, **adopts +the morphology-fold I proposed in the POST-RATIFICATION ADDENDUM above** +(`MorphologyZone::Lake` sourced from `HydrologyResult`, zero new fields) — +not the `water` field this section was built against. Both of us landed +on the morphology-fold independently, which is worth stating plainly: it +isn't one of us conceding to the other, it's convergent verification. **§2 +(the pipeline/projection ruling — filled-surface continuous sampling) +stands and is ADOPTED UNCHANGED** — that reasoning is agnostic to which +field carries the result (morphology-fold or a dedicated `water` field +both need the same continuous-sampling mechanism to avoid the blocky-edge +problem), so nothing about the correction touches it. **§3's struct is +WRONG and superseded** — the real struct is 6 static + 2 sim-state dense +(8 total) + 2 sparse (`courses`, `cliffs`), Araminta's final shape, +corrected further down this section. §1's crossover data (dense-vs-sparse +for CARRYING a bit) is **repurposed, not discarded** — see the endorheic-cue +ruling below, which uses this data for a different question than the one +§1 originally answered. + +*(§1/§3 text below is left in place, unedited, as the historical record of +what was superseded — do not cite it as current. The corrected content +starts at "### Endorheic cue under morphology-fold" after §3.)* + +--- + +Araminta's `araminta-round2.md` §(e) specs `water: Vec` (`{None=0, +Lake=1, Sea=2}`, static plane, new dense field) — I agree with her +reasoning outright (frozen `morphology` vocabulary stays frozen, bit-packing +already loses on T-1179's own DEFLATE finding, a dedicated field costs +almost nothing given lake regions are contiguous). Her three questions, +answered below. + +### 1. Endorheic carrier sizing — sparse `lakes: Vec` vs. `water`'s 4th value + +**Ruling: `water` gains a 4th value (`LakeEndorheic=3`). Do NOT add a +sparse `lakes: Vec` list.** This is a clean, decisive result, +not a close call — I built the actual crossover analysis rather than +reasoning from the single 412,700-cell synthetic data point T-1177's +original appendix carried (that number is from a SYNTHETIC ridged-terrain +grid at 8.3M cells, a canvas size no production path derives hydrology at +synchronously — not a real-population statistic, and not what the +crossover call should be argued from). + +**New bench:** `server/tests/hydrology_equilibrium_bench.rs::bench_per_basin_size_distribution_real_population`. +Solves every real committed body at the real 512×256 production working +grid (same 267-body population, same solver, same grid size as the +existing T-1177 population survey) and records every INDIVIDUAL basin's +cell count — not just the aggregate total the original survey reported. + +**Results — MEASURED, run twice for stability, byte-identical both times +(deterministic: real terrain, no randomness), cross-checked against the +population survey's own total (2,694,012 lake cells — exact match):** + +| | Value | +|---|---:| +| Total basins | 22,270 | +| Min basin size | 1 cell | +| Max basin size | 18,782 cells | +| Mean | 121.0 cells | +| Median (p50) | 18 cells | +| p90 / p95 / p99 / p99.9 | 246 / 486 / 1,703 / 7,128 cells | + +**Histogram:** + +| Bucket | Basins | % of basins | Cells | % of lake cells | +|---|---:|---:|---:|---:| +| 1–10 | 8,894 | 39.94% | 33,399 | 1.24% | +| 11–50 | 6,299 | 28.28% | 160,553 | 5.96% | +| 51–200 | 4,402 | 19.77% | 451,537 | 16.76% | +| 201–1,000 | 2,236 | 10.04% | 951,614 | 35.32% | +| 1,001–5,000 | 401 | 1.80% | 783,355 | 29.08% | +| 5,001–20,000 | 38 | 0.17% | 313,554 | 11.64% | +| 20,001+ | 0 | 0.00% | 0 | 0.00% | + +**The crossover analysis, priced at both carriers' real byte rates +(sparse `Vec<(u16,u16)>` = 4 raw bytes/cell, a pessimistic/conservative +sparse estimate since it doesn't apply MessagePack's own compact framing +discount; dense = T-1179's measured PNG-per-field rate, 1.924 bytes/cell):** + +**Dense wins for 100% of basins, at every size measured, including the +largest (18,782 cells).** Zero basins in the real population cross over to +where sparse would be cheaper. This isn't close: even a single-cell basin +costs 4 bytes sparse vs. ~1.9 bytes dense-if-isolated, and the moment a +basin has ANY neighboring cells (which every real basin does, by +construction — `label_lake_basins`' BFS only produces contiguous regions), +DEFLATE's run-length win on the dense field only widens the gap. **The +naive per-cell byte-rate comparison alone (4 vs. 1.924) already means +dense wins point-for-point before any compression benefit is even +counted** — sparse's `Vec<(u16,u16)>` shape has no structural advantage +over dense here the way it does for genuinely sparse, spatially-isolated +features (settlements, cliff edges) precisely because lake cells are +NEVER isolated — a basin's very definition is a contiguous flood-filled +region, which is the exact shape a raster field compresses best and a +per-cell coordinate list compresses worst (a coordinate list gets zero +benefit from spatial contiguity; a raster field gets maximum benefit from +it). + +**Why this differs from the cliff/`CliffSegment` sparse-list decision +(round 1/2, still correct, not being revisited):** cliffs are RARE (zero +carved cells across the entire real population, T-1177's population +survey addendum) — a sparse list of a feature that's empty on 267/267 +real bodies costs nothing because it's usually zero-length. Lakes are the +opposite: EVERY body in the population has multiple lake basins (22,270 +basins across 267 bodies, ~83 basins/body average), and every basin is a +contiguous blob, not a rare point event. The two features look superficially +similar (both are "extra water-related hydrology output") but sit on +opposite sides of the sparse-vs-dense argument for structural reasons, not +because one measurement contradicts the other. + +**Concrete consequence for Araminta's schema:** drop `lakes: +Vec` from `EncodedStepCanvas` entirely — it was always marked +as her fallback-pending-my-numbers, and the numbers say the fallback is +the answer. `water: Vec` becomes 4-state: `{None=0, Lake=1, Sea=2, +LakeEndorheic=3}`. This also SIMPLIFIES the schema relative to her +provisional draft (one dense field instead of one dense field plus a +sparse list), which is a strictly better outcome than either of us +expected going in — the crossover question had a clean, one-sided answer +rather than needing a size-dependent hybrid rule. + +**One consequence worth flagging for Stig's map-art rule (her own +constraint, restated against the concrete field values):** the +endorheic/overflow visual distinction she specified ("secondary visual +cue... never competing with sea-vs-lake") maps directly onto `Lake` vs. +`LakeEndorheic` as two `water` discriminants that share styling family — +the client reads `water == 1 || water == 3` for "this is a lake, color it +lake-blue," and additionally checks `water == 3` for the secondary +endorheic cue (the outline/texture/saturation treatment she described). +This is a natural fit for the discriminant-value shape and needs no +special-casing beyond what a 4-state enum already gives a colorizer. + +### 2. Pipeline slot — where `water` gets populated, and the projection answer + +Confirming and sharpening what I already worked out for the coordinator's +earlier lake-schema question (same underlying mechanism, now answered +against Araminta's specific field shape): + +**`HydrologyResult` solves once per body at 512×256** (T-1177's own scope, +unchanged), held in the Tier 1 keep-always cache alongside rung-0's own +data. A finer-rung canvas derive needs, for each gridunit's world +position, the `water` classification at that position. + +**Direct answer to the specific question posed: does `water` derive +per-gridunit from the FILLED SURFACE (elevation < filled level = water), +or does it project BASIN-CELL MEMBERSHIP?** + +**Rule: from the filled surface, sampled continuously — NOT basin-cell +membership projected as a discrete lookup. This is the right answer under +D-227's invention discipline, and here's the precise reasoning, not just +the conclusion:** + +- **Basin-cell membership is a WORKING-GRID-RESOLUTION fact** — `Basin.cells` + is a `Vec` of row-major indices into the 512×256 grid. Projecting + membership directly (nearest-cell or bilinear-bool lookup against a + 512×256 boolean mask, exactly how `ocean_mask`/`ocean_fraction_q` work + today) would make a lake's EDGE only as precise as the 512×256 working + grid's own cell size — at District spacing (2,048 m) that's coarser than + the 512×256 grid itself in the wrong direction is fine (the grid is + finer), but at Chunk spacing (64 m) a bilinear-bool sample of a + 512×256-resolution mask produces a BLOCKY lake edge that doesn't refine + as the rung gets finer — exactly the "magnified interpolation of a + coarser composite" D-166's corollary forbids, the same error class this + entire workshop exists to eliminate. +- **The filled surface (`filled_scaled: Vec`, also 512×256-resolution) + has the SAME resolution problem if sampled the same crude way** — but it + doesn't have to be, because `filled_scaled` is a CONTINUOUS quantity + (an elevation-like field, water-surface height), not a boolean mask. + Comparing a bilinearly-INTERPOLATED filled-surface sample against a + bilinearly-interpolated ORIGINAL elevation sample at the gridunit's exact + world position (both sampled the same way `elev_q` already is — this is + literally the same sampling mechanism `ocean_fraction_q` uses today for + sea, just against `filled_scaled` instead of the raw heightmap) produces + a lake edge that's a smooth function of position, refining naturally as + the rung gets finer — exactly the same way coastlines already refine + today via `coast_invention`'s domain-warp on the SAME kind of continuous + primitive. +- **Concretely: `water_at_position = if bilinear(filled_scaled, pos) > + bilinear(original_elevation, pos) { Lake-or-Endorheic } else if + bilinear(original_elevation, pos) < sea_level { Sea } else { None }`** — + three continuous comparisons at the gridunit's own world position, the + same shape as the sea-level check `derive_morphology_zone` already + performs via `ocean_fraction_q`, just reading `HydrologyResult`'s + `filled_scaled` field instead of (or alongside) the raw elevation. This + is mechanism-B-shaped from the seed-chaining reconciliation (round 2 §(b) + and my earlier lake-schema note): a coarser rung's CONTINUOUS PRIMITIVE + (the filled-surface field, computed once at 512×256) is sampled fresh at + every finer rung's own position, never re-solved, never read as a + discrete cached lookup. +- **`LakeEndorheic` vs. `Lake`:** since `Basin.outcome` is a per-BASIN + property (constant across all of a basin's cells, not itself a + continuous field), it needs one more piece: which basin (if any) a + position falls inside, to know which `outcome` applies. This IS a + discrete lookup (basin id per working-grid cell, nearest-sample not + bilinear — you can't blend "half endorheic, half overflow"), but it only + matters at the water/land BOUNDARY resolution question is already + resolved by the continuous `filled_scaled` comparison above — the + endorheic/overflow classification is uniform across the interior of a + water region, it only needs a coarse per-basin lookup (populate a + 512×256 basin-id grid once when hydrology solves, nearest-sample it), + never a fine-grained boundary computation of its own. + +**No new bench needed to confirm this is affordable** — sampling one more +bilinear field (`filled_scaled`) plus one nearest-sample lookup (basin id, +only evaluated where the continuous check already says "this is water") is +the same shape of work `ocean_fraction_q`'s existing sample already +performs every cell, every rung. My own T-1178/T-1154 per-cell rate numbers +(~190–220 ns/cell parallel, flat across every rung) already include +equivalent-cost sampling operations in that per-cell budget — this is a +derivation-pipeline WIRING change, not a new cost category, exactly as I +told the coordinator for the original lake-schema question. + +### 3. Struct field count — reconciled + +Araminta's `EncodedStepCanvas` in her round-2 draft lists 10 dense fields +(counting `flooded` and the now-superseded `water`+`lakes` pair) plus 2 +sparse lists. Per question 1's ruling above (`lakes` dropped, `water` +becomes 4-state), the reconciled field count against my `StepCanvasResponse`/ +`EncodedStepCanvas` design (round 2 §(a)) is: + +```rust +pub struct EncodedStepCanvas { + pub width: u32, + pub height: u32, + // STATIC GEOMETRY PLANE (7 dense fields) + pub morphology: EncodedField, + pub elev_q: EncodedField, + pub temp_dc: EncodedField, + pub moisture_q: EncodedField, + pub vegetation: EncodedField, + pub settlement_id: EncodedField, + pub water: EncodedField, // {None,Lake,Sea,LakeEndorheic} — 4-state, NEW + // SIM-STATE PLANE (2 dense fields) + pub glaciation: EncodedField, + pub flooded: EncodedField, + // SPARSE LISTS (2 — courses existing, cliffs ratified; NO lakes list) + pub courses: Vec, + pub cliffs: Vec, +} +``` + +**9 dense fields total (7 static + 2 sim-state), 2 sparse lists — one +fewer field than Araminta's provisional 10-dense-plus-lakes count, because +`water`/`lakes` collapse into ONE field (`water`, 4-state) rather than +staying two separate schema elements.** This is the single authoritative +field list per her own (d) note ("keeps the struct's field list as one +authoritative list rather than two documents each claiming to be +current") — confirming here so both documents converge on the same struct +rather than each claiming a slightly different one. + +**Envelope-framing consequence: none.** Exactly as both of us already +argued for `flooded` and the original `water` addition — one more/fewer +field derived in the same row-chunked pass (or, for `water`, in a pass +keyed on the same `HydrologyResult` `cliffs` already draws from) doesn't +reopen the flat-envelope ruling. This is field-set growth on the existing +flat `StepCanvasResponse`, not a new response type or a dense/sparse +split — my round-2 §(a) reasoning against splitting the envelope applies +unchanged to a 9-field struct exactly as it did to a 6-field one. + +*(§3 above is superseded by the crossing — the real struct has no `water` +field at all. Corrected below.)* + +--- + +## CORRECTED (2026-07-23, post-crossing): struct, final; endorheic cue, ruled + +### Struct, Araminta's final shape — confirmed, no dissent + +`MorphologyZone::Lake` (discriminant 1, already in the frozen 17-zone +vocabulary) is sourced from `HydrologyResult` per the POST-RATIFICATION +ADDENDUM's morphology-fold above — zero new fields for the lake/sea +distinction itself. The corrected, authoritative struct: + +```rust +pub struct EncodedStepCanvas { + pub width: u32, + pub height: u32, + // STATIC GEOMETRY PLANE (6 dense fields) + pub morphology: EncodedField, // Lake/OpenOcean now sourced from HydrologyResult + pub elev_q: EncodedField, + pub temp_dc: EncodedField, + pub moisture_q: EncodedField, + pub vegetation: EncodedField, + pub settlement_id: EncodedField, + // SIM-STATE PLANE (2 dense fields) + pub glaciation: EncodedField, + pub flooded: EncodedField, + // SPARSE LISTS (2) + pub courses: Vec, + pub cliffs: Vec, +} +``` + +**8 dense fields (6 static + 2 sim-state), 2 sparse lists.** No `water` +field anywhere. This is Araminta's final shape; I have no dissent — it's +strictly simpler than either of our provisional drafts (mine at 9 fields, +hers at 10), because the morphology-fold makes the lake/sea distinction +free to carry rather than requiring its own slot. + +### Endorheic cue under morphology-fold — the actual open question + +The coordinator is right that my §1 crossover analysis doesn't settle +this: that analysis compared dense-field-bit vs. sparse-coordinate-list +for carrying an endorheic signal, and **both of those options are now +foreclosed** — a dense bit under morphology-fold means either widening +the frozen 17-zone vocabulary to 18 (option ii below) or resurrecting a +new field (which contradicts the entire point of the fold), and a sparse +per-basin list was never Araminta's proposal for the cue specifically, it +was for the abandoned `lakes: Vec` carrier. The real question +— no wire signal at all vs. a vocabulary change vs. a deferred mechanism +— is a different decision than the one my crossover data answered, and I +want to be precise that I'm not stretching that data to cover a question +it wasn't built for. + +**My pick: (i), Araminta's outflow-course-presence inference. No wire bit +at all for endorheic-vs-overflow; the cue arrives as a side effect of +wiring basin outlets into the courses/river-network machinery, which is +work T-1170 Ruling 7b already reserved room for and which the map needs +done anyway.** + +**Rationale, grounded in the actual basin-outcome data:** + +- **Proportionality — 1,030 of 22,270 basins (4.63%) are endorheic.** This + is the number that should drive the vocabulary-change call, and it + argues against option (ii) specifically. An 18th `MorphologyZone` entry + is a permanent widening of a D-239 frozen vocabulary — every consumer + of that enum (client colorizer, any future classification logic, the + one-colorizer-family guarantee Araminta's own round-1 §3 argues for) + inherits the new arm forever, to carry a distinction that's true for + fewer than 1 in 20 lake basins. A frozen vocabulary earns its frozen + status by being conservative about additions; 4.63% is not the kind of + frequency that should be the bar-clearing case for reopening it, + especially when a zero-cost alternative exists that reaches the same + end state. +- **Option (i) has zero marginal wire cost and zero marginal vocabulary + cost, and the work is required regardless of this decision.** An + overflow lake's exit river is real hydrology output — `outlet_path` is + already a non-empty, guaranteed field on every `BasinOutcome::Overflow` + (confirmed by direct read: `hydrology_equilibrium.rs`'s own + `overflowing_basin_has_nonempty_outlet_path` test asserts exactly this). + Wiring that path into the river-course/`RiverNetwork` machinery so an + overflow lake visibly shows its exit river on the map is not a + cue-specific feature — it's the map correctly showing hydrology that + already exists, which the workshop needs for overflow lakes to read as + hydrologically complete regardless of whether anyone cares about the + endorheic distinction. The cue is a **free side effect** of doing that + work, not an additional feature built to carry it. This is exactly the + shape T-1177's own `RIVER_DOWNSTREAM_TERMINAL` sentinel reservation + anticipated: I flagged in the original T-1177 measurement doc that + `river_course::build_edges` already treats that sentinel as a safe + no-op, confirming the endorheic case was already designed to be + additive when this moment came — it has come, and it's additive. +- **The inference is structurally sound, not a fragile heuristic.** Every + `Overflow` basin has a real, non-empty `outlet_path`; every `Endorheic` + basin has none, by definition (that's what "endorheic" means — no + outflow). There's no edge case where the presence-of-exit-river signal + could misfire or need a tie-break rule the way, say, a quantized + threshold might. The client reads "does this lake have a river flowing + out of it" and that question has an unambiguous, always-correct answer + under the solver's own definitions — no cue-specific correctness burden + at all. +- **Option (iii) (defer with no mechanism recorded) is worse than (i) for + a reason specific to this workshop's own discipline, not a general + preference for shipping features.** The outlet-wiring work is going to + happen regardless (overflow lakes need to show SOME exit-river + behavior to be honest about their own hydrology — the current schema + can't even represent "this basin has settled but I'm choosing not to + draw where its water goes"). Recording "no mechanism, unscoped" would + either silently under-scope that ticket later or force a second design + pass to rediscover exactly what (i) already worked out. Since the + mechanism costs nothing extra to name now, naming it is strictly better + than deferring the naming. + +**Verdict: (i).** Endorheic-vs-overflow ships with zero wire bits, zero +vocabulary change, as a client-side inference from whether a lake's +`morphology`-classified region has a river course exiting it — which is +answerable directly from the existing `courses: Vec` sparse +list once basin outlets are wired into it (a ticket-level follow-up, +pre-cleared by T-1170 Ruling 7b, not a wire-schema change). If Jeroen +wants the distinction visually stronger than "no exit river present" can +convey on its own (a genuinely different aesthetic call, not a data +question), that's a Stig-side styling decision layered on the same +zero-bit signal, not a reason to revisit the carrier. diff --git a/docs/workshops/body-map-viewer/dudley-round1.md b/docs/workshops/body-map-viewer/dudley-round1.md new file mode 100644 index 000000000..c8dc13910 --- /dev/null +++ b/docs/workshops/body-map-viewer/dudley-round1.md @@ -0,0 +1,351 @@ +--- +title: "Body Map Viewer — Dudley Round 1" +description: "Server derivation position: hydrology algorithm + cliff representation, step-canvas generation + canonical-vs-viewport, compute-chunk partitioning, cache tiers" +workshop: body-map-viewer +round: 1 +owner: Dudley +status: complete +decision_refs: [D-166, D-226, D-227, D-243, D-203] +--- + +# Dudley — Round 1 Position + +All four numbers I was gated on are in the appendix and I helped produce three +of them (①②③, plus half of ④'s framing). I'm not going to re-derive them here — +I'll cite and build on them. Nothing below is extrapolated past what +`t1177-hydrology.md`, `t1178-t1154-derive-bench.md`, and `t1179-wire-table.md` +measured. Where I lean on Stig's ⑤ for the render-side implication of a +server decision, I'll say so explicitly. + +## Question 1 — Hydrology: algorithm, cost, cliff representation + +**Algorithm: priority-flood (Barnes/Planchon-Darboux class) + Dijkstra overflow +routing, exactly as prototyped in T-1177.** Min-heap flood fill from +below-sea-level + grid-edge seeds finds each basin's true spill point; ascending- +spill-level basin processing lets a resolved lower basin be a valid target for a +higher basin's overflow search; carving applies only where peak flow accumulation +on the carved path clears `RIVER_THRESHOLD = 200`. This is a **pure function** +(`solve(elevation, width, height, sea_level, climate) -> HydrologyResult`) — no +tick loop, no RNG, no wall-clock — which is the only shape D-010 and D-227 permit +for anything that later feeds a save-relevant derivation. I'm recommending we +adopt it as-is; the prototype module (`server/src/atlas/hydrology_equilibrium.rs`) +is production-shaped, not throwaway. + +**Cost, both regimes measured:** + +- **Single-body solve** (the real production shape — hydrology settles once per + body, like `drainage::analyze` does today): 512×256 (real GJ1c working grid) ≈ + 24 ms single-threaded. This is cheap enough to not need parallelizing internally + at all. +- **All 273 bodies, Rayon `par_iter` across bodies** (not within one solve — see + below for why): **0.7–0.8 s total**, both initial and stability-re-run passes + agree within 1-3%. This is the number that answers "can we settle hydrology for + the whole Reach at world-open without it being felt": yes, comfortably, and it + parallelizes on the *correct* axis. +- **8.3M-cell single grid** (the stress ceiling, not a real request shape): ~5.7 s + single-threaded. I want this number on record because it's the honest "computer + catches fire" case Tyre's implications doc asked for — but no production path + needs to pay it. Hydrology is a **per-body Layer-1 property**, computed once at + 512×256 (or whatever the working-grid resolution ends up being — see the + step-canvas discussion below for why 512×256 stays the right size even in the + stepped model), not re-solved per step-canvas or per zoom step. It is exactly + the kind of thing D-203's `BodyWorldState` LRU cache already exists to hold. + +**Why `solve()` isn't internally parallelized, and why that's fine.** The +priority-flood fill and the Dijkstra overflow search are both globally sequential +by construction — one shared min-heap with strict pop order, architecturally +identical to `road_graph.rs`'s own unparallelized `astar`. A basin's outcome can +depend on a lower basin having already resolved (the chaining case in the +`DownstreamTarget` model), so cells are not independent pure functions the way +`layer_proxy.rs`'s row-chunked derive is (D-227's per-cell independence, which is +*exactly* what lets that path parallelize so cleanly — see Q3). Hydrology doesn't +have that property internally. It doesn't need to: the real workload is "273 +independent bodies," and that parallelizes trivially and is the number that +matters. + +**Cliff/multi-height representation (red flag 4).** Proposal, grounded directly +in what the solver already computes, not chosen independently of the +measurement: **dominant height (`elevation`, unchanged) + `channel_depth: u16` +(quantized) + `cliff_edge: bool`.** + +- `elevation` keeps carrying the gridunit's rim/dominant/walkable height — zero + change to the existing single-height contract for the non-gorge case (which is + nearly every case — see below). +- `channel_depth` is a direct, non-lossy carry of `channel_depth_scaled` — the + solver already produces this number, no re-derivation downstream. +- `cliff_edge` marks a rim/discontinuity cell so the client map-art function + renders a cliff-face transition instead of a smooth gradient at that boundary. + +I considered and rejected a min/max height pair: it loses the *shape* of the +transition (point-drop-at-one-edge vs. spans-the-whole-gridunit) that +depth+edge-flag preserves implicitly, and it's a worse fit to what the solver +actually outputs — it would require synthesizing two heights from one measured +depth value for no informational gain. + +**The honest finding that should reframe red flag 4's urgency: gorge carving is +structurally rare.** All three production-scale benches (512×256, 768×432, +3.84M... 8.3M) carved **zero cells**. This isn't a bug — I traced it to a real +structural property: priority-flood finds the *true* global minimum rim of a +basin, so the first cell any overflow search reaches outside a sealed lake is, by +construction, never higher than that basin's own spill level. A single sealed +basin, however jagged its rim, always carves zero. Genuine carving needs a +narrower geometry — two independently-sealed basins connected by a corridor +exactly one cell wide in both dimensions and higher than both basins' own rims — +which is real terrain but rare at continental working-grid resolution. I +constructed four fixture attempts trying to force it and each one correctly +diagnosed as "basins merged" or "spill level absorbed the connector," not a code +defect (`server/src/atlas/hydrology_equilibrium.rs`, "Gorge carving" test +section, four named tests covering the mechanism directly since an end-to-end +production-scale trigger proved impractical to construct honestly). + +**What this means for the wire-cost side of red flag 4:** the field is real and +needed for correctness, but it will be populated `false`/`0` for the overwhelming +majority of gridunits at every canvas size measured. The wire-cost argument +against including it is weaker than the red flag assumed — a rarely-nonzero +`bool` + quantized `u16` is cheap under any of the encodings in ④'s table (PNG's +DEFLATE in particular loves a field that's constant almost everywhere). I'd +rather we carry it at Phase-4 Atlas scope than defer cliffs to Phase-5 purely on +a cost argument that the measurement doesn't actually support — Tyre and +Araminta should weigh in on whether "rare but real, cheap to carry" changes the +Phase-4-vs-Phase-5 call red flag 4 posed; I'm only settling the representation +and the frequency, not the scope ruling. + +## Question 2 — Step-canvas generation and canonical-vs-viewport (red flag 3) + +**Recommendation: viewport-sized canvases at every step, no canonical fixed +canvas anywhere on the ladder. This isn't just the cheaper cache shape — at the +deep steps it's the only one that stays legal under D-226(d).** + +The reasoning, stated plainly because red flag 3 called this "the sharpest thing +in the whole design" and I don't think that's overstated: + +- A **canonical** fixed-size canvas at a given step's spacing would have to cover + enough of the body to serve every possible viewport at that step — at shallow + steps (region/district spacing) that's affordable (it's what the orbital tile + mosaic already does today, per-tile ImageTextures at capped density). At the + **deepest** step (10 px/tile, 1 m spacing), a canonical canvas covering enough + ground to be reusable across viewports is a near-whole-body metre-resolution + derivation — which is precisely what D-226(d)'s surviving whole-body-planetary- + layer prohibition forbids, independent of whether the cost is affordable. +- A **viewport-sized** canvas at 1 m spacing is exactly the 216×384 m (82,944 + cell) shape I measured in T-1154: **17 ms parallel, comfortably interactive**, + and by construction never a whole-body layer — it's bounded by what's on + screen, the same "windowed viewport" carve-out D-226(d) already opened for the + district/quarter rungs (T-1124 amendment). This is the *only* canvas policy + that keeps the deep end of the ladder inside the boundary that survives this + workshop's governance delta. + +**Practical shape at each step: viewport-sized, sized to a fixed pixel budget +(the same "land on a 4K window" instinct from Jeroen's outline), not to the +literal current window size.** Concretely: server renders each step's data +canvas at a fixed canvas-pixel budget (e.g. 3840×2160, or whatever the +round-2 wire-contract decision lands on) regardless of the client's actual +monitor resolution, the same way the current windowed-family already treats +`WIRE_CAP_CELLS` as a budget, not a literal viewport echo. This gives headroom +for larger monitors without re-deriving per-monitor, and it's the shape all +four of my measured cost numbers (330K/2.07M/8.3M) already assume — I didn't +measure a "canonical whole-body" shape at any step because I don't think one +should exist past the region/orbital rungs. + +**Cost at each rung, all MEASURED not extrapolated (own numbers, T-1178/T-1154):** + +| Step spacing | 330K canvas (parallel) | 2.07M canvas (parallel) | 8.3M canvas (parallel) | Realistic viewport canvas | +|---|---:|---:|---:|---| +| District (2,048 m) | 63.7 ms | 394.6 ms | 1,827 ms | — | +| Block (128 m) | same band, ~190-220 ns/cell | same band | same band | — | +| Tile (1 m) | same band | same band | same band | **82,944 cells (216×384 m) → 17 ms** | + +The per-cell parallel rate (~190–220 ns/cell) is flat from 330K to 8.3M cells at +every spacing I tested (District through Tile) — this is the T-1143-killing gap +closed: there is no cliff where chunking overhead or cache pressure eats the +win at scale. **Cost does not gate any step in the ladder.** What gates the deep +steps is the wire carrier (④: even the best encoding is 21×–563× the existing +30 KB windowed-payload cap) and the canonical-vs-viewport governance boundary +above — neither is a cost question my measurements can resolve, both of which +I've now priced and named explicitly for Araminta/Tyre's round-2 synthesis. + +**One caveat I want on record precisely because it's easy to gloss over:** the +8.3M-cell numbers above are for a *canvas that size*, not a *viewport that +size*. A 3840×2160 canvas at 1 m spacing covers only 3.84 km × 2.16 km of +ground — tiny. If "8.3M cells" is ever read as "a viewport-sized Tile-rung +canvas," that's wrong; the viewport-sized Tile canvas is the 83K-cell number +(216×384 m), not the 8.3M one. The 8.3M row exists to answer "does the derive +path degrade at that many cells" (no), not "is an 8.3M-cell canvas a realistic +Tile-step request" (it categorically is not, by the viewport-sizing policy +above). + +## Question 3 — Compute-chunk partitioning across the Rayon queue + +**The existing row-chunked `into_par_iter()` loop in `build_district_window_layer` +(`layer_proxy.rs:1564-1586`) is the mechanism, and it holds at every size and +every spacing on the ladder — this is the single most load-bearing "measured, +not extrapolated" result across all four gates.** Same per-cell rate (190–220 +ns/cell parallel) confirmed at 330K, 2.07M, and 8.3M cells, at District, Block, +and Tile spacing, across three independently-run fixtures (my own synthetic +gradient body, the real GJ1c body, and Araminta's independent GJ338Bd run for +④) — three cross-validating measurements landing in the same band is a robust +finding, not a single-run artifact. + +**Step boundaries as compute-chunk boundaries (premise 8) is compatible with +this mechanism as-is, with one caveat.** The row-chunking happens *within* one +step's derive call — it's how a single step canvas's cells get distributed +across the 16 Rayon workers, not how work is split *between* steps. Premise 8's +"step boundaries = compute-chunk boundaries" reads correctly as: each step is +one `build_district_window_layer`-shaped call (one derive request, internally +row-chunked), and steps are never partially computed or streamed mid-derive — +you get a whole step canvas or you're still waiting. That matches the +hold-fetch-swap model Stig's ⑤ measured render costs for. + +**The caveat: courses are excluded from every chunked-cost number I measured +at 8.3M and at the deep-step 83K shape.** The row-chunked *replica* loop used +for every rectangular (16:9) canvas measurement is courses-empty by +construction (H2 in `t1178-t1154-derive-bench.md`) — it has no `RiverNetwork` +wiring at all. The real production path (square windows, real river network) +measured courses-inclusive at real density: **+0.09–0.21 ms against a ~5 ms +District-cap baseline (under 5%)**, cross-checked at 18 real courses in a +331,776-cell GJ1c window (195.0 ns/cell, within 2% of the courses-sparse +synthetic number). So the chunking mechanism's cost story holds either way — +courses add a small, bounded tax, not a different order of magnitude — but +anyone implementing step-canvas serving should wire courses into the row-chunk +loop from day one rather than treating them as a bolt-on, since the deep-step +83K number I reported (17 ms) technically excludes them and I don't want that +caveat to get lost between this document and an implementation ticket. + +**One thing I did NOT measure and want flagged for round 2/implementation:** +whether chunking should change shape when a step canvas also needs to serve as +seed input for the next tier down (Jeroen's outline: "this at the same time +serves as seed information for the deeper cascade"). Today's row-chunked loop +produces one flat output array — using that array as *input* to a finer step's +derive is a data-flow question (does the finer step re-derive from the same +`(seed, position)` pure function, or does it consume the coarser step's output +values directly?), not a partitioning-cost question. My measurements assume +each step re-derives from `(seed, position)` independently per D-227's +derive-don't-store discipline (the coarser canvas is not an input the finer +canvas depends on for correctness, only for display continuity/pre-fetch +ordering) — if round 2 wants the finer tier to actually consume the coarser +tier's values as a literal input, that's a different architecture than what I +benchmarked and needs its own cost pass. + +## Question 4 — Cache tiers server-side + +**The global-tier cost, computed from measured numbers, not a vibe (red flag +2):** Jeroen's "always keep the global level" instinct is affordable **if +"global level" means the orbital/region-spacing rung, not a per-step cache of +every rung at every zoom the player has visited.** Using T-1179's measured +6.00 bytes/cell raw rate (or ~0.32× that with PNG-per-field, ~1.9 bytes/cell +effective): + +- At district-spacing global canvas (~330K cells is the deep end of what + "orbital" would ever need — the real orbital rung is coarser, region-spaced, + which is fewer cells, not more): raw ≈ 1.99 MB/body, PNG-encoded ≈ 638 KB/body. +- **Across all ~273 bodies, raw: ~543 MB. PNG-encoded: ~174 MB.** Consistent + with red flag 2's own back-of-envelope (~440 MB at 5×5, ~13 GB at 1×1) — my + numbers land lower because I'm using the *measured* PNG-encoded rate rather + than the brief's pre-measurement estimate, and because "global" for the + orbital rung should be region-spaced (far fewer cells than the district- + spacing canvas I priced above as a conservative upper bound). + +**Recommendation: "always keep the global level" = a resident, disk-backed +per-body cache at the orbital/region rung only, never a permanent allocation +for finer rungs.** ~174 MB (PNG-encoded, all 273 bodies) is a reasonable +resident/disk budget for "atlas navigation snappy after first calc" — this is +squarely a caching decision, not a live-memory one, and D-227's "transient +evictable cache" framing applies even to a tier that's practically +never-evicted in normal play: it must still be *derivable*, not +authoritative, so a cold-start or corrupted cache is a recompute, never data +loss. This is the same shape D-203's existing `BodyWorldState` LRU already +established server-side (50-body cap, ~5 MB budget, oldest-`last_accessed` +eviction, pinned current-location/neighbors) — I'd extend that same +resource/pattern to cover the orbital-rung step canvas rather than invent a +parallel cache mechanism. Concretely: `BodyWorldState` (or a sibling resource +following its exact shape) gains an `orbital_canvas: Option` +field, populated lazily on first Atlas-open for that body by the existing +Rayon background-queue population path (D-206), evicted only on the existing +LRU discipline (which for 273 bodies at ~638 KB PNG-encoded each barely +pressures a 50-entry cap sized for a much smaller ~100 KB/entry Layer-1 +budget today — this may argue for either a higher cap or a split resource; +I'd defer that split to whoever implements it, since it's a tuning question +once the byte budget above is accepted, not an architecture one). + +**Finer rungs (district/block/tile step canvases): TTL(detail, time, +distance), never "always keep."** This is D-227's concrete eviction policy +made specific: + +- **detail** (rung depth) is the dominant term — deeper rungs get shorter TTL, + monotonically, because they're both cheaper to re-derive (my own measured + ~190–220 ns/cell parallel holds flat across every rung, so re-derivation + cost does NOT argue for longer retention at deeper rungs the way it might in + a system where finer detail was expensive) and cover less ground (so a + given player session visits far more distinct deep-rung windows than + shallow ones, meaning the *hit rate* on a long TTL at a deep rung would be + low anyway — cache entries at Tile spacing are viewport-sized and + effectively single-use across a normal pan). +- **time** and **distance** (player/viewport focus, both character location + AND atlas-viewport-as-player-focus per premise 1) modulate the base TTL the + same way the existing FIFO-256 window cache + client 24-entry LRU already + behave, generalized: entries far from current focus (either kind) decay + faster; entries at the current focus are effectively pinned the way D-203 + pins current-location/neighbor bodies. +- **Concretely, I'd propose the TTL formula as a multiplicative discount**: + `ttl = BASE_TTL[rung] × time_decay(age) × distance_decay(distance_from_focus)`, + with `BASE_TTL` the only per-rung tunable and the two decay functions shared + across rungs — this keeps the policy legible (one knob per rung, two shared + curves) rather than a per-rung bespoke formula, which matters for the same + reason the classification pipeline avoids per-rung decision-boundary tables + (T-1150 §6 precedent Araminta's ② question cites). I'm not committing to + exact `BASE_TTL` numbers here — that's a tuning pass once the wire contract + and step count are settled in round 2, not a round-1 architecture call. + +**What I'm explicitly NOT proposing:** a disk-backed persistent tier for +anything finer than the orbital/global rung. Every finer-rung canvas is cheap +enough to re-derive (my own flat ~200 ns/cell parallel number holds at every +size and spacing I tested) that a disk tier for them would be optimizing a +cost that isn't the bottleneck — the bottleneck at finer rungs is wire size +(④) and step-cross latency (Stig's ⑤: sub-5ms upload, not a texture-upload +problem either), not derive cost. A disk tier for deep rungs would add +complexity (self-cleaning sweep, disk I/O latency on a cache miss) to solve a +problem the measurements say doesn't exist. This is my answer to Stig's +question 4's first candidate shape ((i) server-side SQLite cache DB) as it +applies to the *global* tier specifically: I think a simple in-process +resource (the D-203-shaped extension above) is sufficient for the orbital +rung's ~174 MB budget without reaching for SQLite at all — SQLite would only +earn its keep if the budget were large enough to need database-grade +eviction bookkeeping, and 273 rows at one field each doesn't need that. If +Stig's cross-boundary analysis (server-primary vs. client-primary reads) finds +a reason SQLite specifically is preferable for other reasons (client-facing +query shape, cross-session persistence semantics), I'd defer to that — my +claim is narrower: the *cost* doesn't force SQLite, a plain resource would +work fine at this size. + +## Summary for round 2 / Jeroen's interview + +1. **Hydrology is settled-viable, cheap, and mostly non-carving.** Ship the + T-1177 prototype's algorithm as-is; solve once per body (like + `drainage::analyze` today), hold in the D-203-shaped cache, never re-solve + per step or per request. +2. **Cliff representation is `channel_depth` + `cliff_edge` alongside + unchanged `elevation`** — cheap to carry given how rare carving measures + as being; Phase-4-vs-Phase-5 scope call is Tyre/Araminta's, not mine to + settle alone. +3. **Viewport-sized canvases at every step, always — no canonical fixed + canvas past the orbital/region rung.** This is the only policy that keeps + the deep ladder inside D-226(d); cost was never the constraint here, + governance is. +4. **The row-chunked par_iter mechanism already in `layer_proxy.rs` is the + answer to compute-chunk partitioning** — it holds flat from 330K to 8.3M + cells at every spacing, at three independent cross-validations. Wire + courses into it from the start; don't let the courses-empty replica-loop + measurement become the shipped assumption. +5. **Cache tiers: one resident/disk global tier at the orbital rung + (~174 MB PNG-encoded across all bodies, D-203-shaped resource extension), + TTL(detail, time, distance) for everything finer, no disk tier below + orbital.** The wire carrier (④) and the canonical-vs-viewport boundary, + not derive cost, are what gate the deep steps — cost is a solved problem + at every rung I measured. + +None of the four things I was gated on turned into a "computer catches fire" +result. The genuinely open items for round 2 are Araminta's wire contract +(the tagged-envelope call is now unavoidable per ④, I agree with that +reading), Tyre's gridunit↔D-243 snap-or-float ruling, and the Phase-4-vs- +Phase-5 cliff scope call — none of which are cost questions my measurements +can resolve alone. diff --git a/docs/workshops/body-map-viewer/dudley-round2.md b/docs/workshops/body-map-viewer/dudley-round2.md new file mode 100644 index 000000000..79f3a6adc --- /dev/null +++ b/docs/workshops/body-map-viewer/dudley-round2.md @@ -0,0 +1,768 @@ +--- +title: "Body Map Viewer — Dudley Round 2" +description: "Envelope mechanics (wire/serving design), seed-chaining ruling, step-ladder tables, and final cache-tier spec incorporating Jeroen's storage-eviction amendment" +workshop: body-map-viewer +round: 2 +owner: Dudley +status: complete +decision_refs: [D-166, D-225, D-226, D-227, D-243, D-203] +--- + +# Dudley — Round 2 Position + +Read `lead-interview-1.md` and all three other round-1 documents first. Four +items in the coordinator's split: envelope mechanics (wire/serving half — +Tyre owns the D-record text), the seed-chaining ruling (closing round-1 open +item 8), step-ladder tables (with Tyre — producing the tables Jeroen rules +from at interview 2), and the final cache-tier spec incorporating Jeroen's +storage-eviction amendment. + +**Coordination note:** I first attempted to reach Tyre directly (SendMessage) +to split envelope-mechanics drafting live and he wasn't addressable yet, so I +designed the wire/serving half unilaterally, grounded directly in the actual +`decode_inbound`/`Inbound` code (`server/src/bridge/mod.rs`). Tyre reached me +mid-draft with his own three concrete questions (message shape, legacy +coexistence, step-0 carrier) plus his snap-rule constraints and three +candidate step-ladder skeletons (Options A/B/C, all D-243-disciplined, +correcting an error in my own first pass — see §(c)). This document is +revised to answer his questions directly and replaces my original two tables +with cost fills against his skeletons. Araminta also relayed her dense/sparse +envelope-framing question (via the coordinator) — answered at the end of +§(a). All three of us are converging live; this is the reconciled version. + +--- + +## (a) Envelope mechanics — wire/serving design + +### The constraint this has to satisfy, stated precisely + +I read the actual code before designing this, not just the doc summaries. +`server/src/bridge/mod.rs`'s `decode_inbound` demuxes five inbound map/array +shapes today (`Inputs`, `AtlasRequest`, `StarMapRequest`, `CityNamesRequest`, +`BrowseRequest`), and the module's own doc comment (lines 67-75) already +names this exact moment: + +> "The next genuinely NEW inbound shape (a sixth) must migrate the channel to +> the tagged-envelope framing D-225 deferred — do not add a sixth probe." + +A step-canvas request is unambiguously a sixth new shape. So this isn't a +judgment call about *whether* to migrate — the ceiling was pre-declared and +this is the shape that trips it. What I'm designing is the concrete *how*. + +### What "tagged envelope" already means in this codebase — don't invent a new mechanism + +Reading `Inbound`'s existing three newer variants (`StarMapRequest`, +`CityNamesRequest`, `BrowseRequest`), the "tag" D-225's 2026-06-12 amendment +asked for is **already partially implemented**, just not generalized: each of +those three structs carries a mandatory boolean discriminator field the +others don't have at all (`star_map: bool`, `city_names: bool`, `browse: +bool`), and `decode_inbound`'s `ShapeProbe` defensively checks that at most +one discriminator is present in any given frame, rejecting ambiguous unions +outright (PR #176 review H1). This is a **tagged envelope in substance** — a +required marker field distinguishing shapes — it just hasn't needed a shared +name or a unified probe struct because five shapes fit in one hand-rolled +`ShapeProbe`. + +**My recommendation: extend this exact pattern for the sixth shape, don't +invent a parallel envelope format.** + +```rust +/// A step-canvas data-canvas request (body-map-viewer workshop, T-1176 +/// follow-on). Discriminator field `step_canvas: true` — same tagged-marker +/// pattern as `star_map`/`city_names`/`browse`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StepCanvasRequest { + pub step_canvas: bool, // mandatory discriminator, always true when present + pub body_id: String, + pub step_index: u32, // the discrete step (§(c) below), never a raw spacing float + pub center: (i64, i64), // world-metres, snapped to the step's D-243 rung grid (Tyre's snap ruling) + pub extent: (u32, u32), // canvas px budget, e.g. 3840x2160 — fixed budget, not literal viewport echo + pub min_wl_m: u32, // octave cutoff, quantized band (existing window_min_wl_m precedent) +} +``` + +Add `StepCanvasRequest(StepCanvasRequest)` as a sixth `Inbound` variant, +extend `ShapeProbe` with a `step_canvas: Option` field, and add it +to the mutual-exclusivity sum and the try-order chain in `decode_inbound`. +This is a small, mechanical, additive change — the demux *mechanism* doesn't +need a redesign, because the five-shape ceiling was never about the +mechanism breaking, it was a **discipline marker** ("stop adding shapes this +way past five, go do the real thing"). The "real thing" it points at, once I +traced the actual code, turns out to be exactly the pattern already in place +— which is good news, not a corner cut: it means the migration is +low-risk, not a rewrite. + +**Response side is the actual new shape, and it does need its own message — +not a field.** `AtlasLayerResponse`'s own doc (`layer_proxy.rs:605-631`) is +explicit that a windowed-family second field is "a dedicated response +message by rule, not a second `Option`" (D-226 T-1124 §2). A step-canvas +response is emphatically that case — Tyre's amendment text already rules +this (§1d of his round-1 doc). Concretely: + +```rust +/// Response to a StepCanvasRequest. Deliberately NOT a field on +/// AtlasLayerResponse (D-226 T-1124 §2's windowed-family ceiling names this +/// exact case) — a wholly separate response type outside that family. +#[derive(Debug, Clone, Serialize, Deserialize)] +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) — mirrors the existing three-way status enums + pub canvas: Option, +} + +/// One dense field, PNG-per-field encoded (T-1179's measured winner). +pub struct EncodedField { + pub png_bytes: Vec, +} + +pub struct EncodedStepCanvas { + pub width: u32, + pub height: u32, + pub morphology: EncodedField, + pub elev_q: EncodedField, + pub temp_dc: EncodedField, + pub moisture_q: EncodedField, + pub vegetation: EncodedField, + pub glaciation: EncodedField, + pub settlement_id: EncodedField, // Araminta's new dense field + pub courses: Vec, // sparse, MessagePack-native (unchanged shape) + pub cliffs: Vec, // sparse, MessagePack-native — see §(a) note on the Dudley/Araminta tension below +} +``` + +`SimBridge` gains `fn send_step_canvas_response(&self, resp: +&StepCanvasResponse) -> Result<(), BridgeError>`, parallel to the existing +five `send_*` methods — same pattern `TcpBridge` already implements for +`send_atlas_response`/`send_star_map_response`/etc. This is the entire +"envelope migration" in mechanical terms: **one more tagged inbound variant, +one more dedicated outbound method, both following patterns already proven +five times over in this exact file.** I want this stated plainly because +Tyre's round-1 text (correctly) frames the migration as "expected scope, +challenging but doable" at the governance level — at the *code* level, once +you actually read `bridge/mod.rs`, it's closer to "doable" than +"challenging." The challenge is elsewhere (the client-side rebuild Stig owns, +the cache-tier and step-ladder work below), not in extending this demux. + +**Note on the cliff-field dense-vs-sparse tension (round-1 conflict #1, +`round-1-notes.md` §3):** I'm adopting Araminta's sparse `Vec` +shape in the struct above, not my round-1 text's implicit dense-array +framing. Reading her argument again against my own hydrology numbers, she's +right and I was underspecified, not actually disagreeing: I only ever +proposed the *field set* (`channel_depth` + `cliff_edge`), never committed to +dense-array-shape explicitly, and given the measured rarity (zero carved +cells at every production-scale bench I ran), a mostly-empty `Vec` costs +strictly less than a mostly-zero dense array under every encoding in T-1179's +table — sparse wins outright here, no tradeoff to weigh. Tyre's "new arrays" +language in his synthesis was, I think, imprecise rather than a third +position — resolving as: **`cliffs: Vec` parallel to +`courses`, both sparse, both MessagePack-native, neither PNG-encoded.** + +### Legacy `district_window` / `AtlasLayerRequest` coexistence during migration + +**No retirement message, no deprecation flag, no dual-write.** The legacy +windowed carrier keeps working completely unchanged — same struct, same +`serve_district_window`, same cache key, same wire bytes — for as long as the +*current* Atlas viewer (`AtlasWindowViewer`, the thing Stig's round-1 doc +describes retiring client-side) keeps requesting it. The server has no +concept of "the old viewer" — it just answers whichever request shape +arrives. Once Stig's stepped viewer ships and the old `AtlasWindowViewer` +code is deleted client-side, the server simply stops receiving +`AtlasLayerRequest.window_center`-populated requests, and `district_window` +quietly goes cold — no server-side cleanup ticket needed, per D-005/D-192's +co-ship guarantee (client and server ship together, so there's never a +window where an old client talks to a new server or vice versa). This is the +same "eviction → recompute, always valid" property D-227 already gives every +cache entry; an unrequested carrier isn't a liability, it's just unused code +someone can delete in a follow-up cleanup ticket once the cutover is +confirmed complete, not a coexistence *problem* requiring design work now. + +**One thing worth flagging for the ticket plan (not a design gap, a +sequencing note):** the migration should ship the new `StepCanvasRequest` +path fully working, verified via the live Gauntlet pipeline (project +convention), *before* the client cuts over — i.e. the server-side sixth-shape +addition and the client-side viewer rewrite don't have to land in the same +PR, since the old carrier keeps serving the old client throughout. This +lowers implementation risk (the migration doesn't need a flag-day cutover) +and is worth Tyre/the lead naming explicitly in the ticket plan. + +### Direct answers to Tyre's three questions + +**1. Message shape: discriminator field on the existing stream, not a new +wrapper/framing layer.** Concretely, `step_canvas: bool` on a new +`StepCanvasRequest` struct, extending the exact `ShapeProbe` pattern that +already governs `star_map`/`city_names`/`browse` (`server/src/bridge/mod.rs`, +read directly — see above). This *is* "a required marker field," which is +literally the alternative-satisfying language D-225's 2026-06-12 amendment +used ("a tagged envelope (or a required marker field)") — I'd steer away from +inventing a wrapper/framing layer on top of the existing stream, because the +stream itself doesn't need restructuring, only one more disambiguated shape. +Same answer for the response side: a new `StepCanvasResponse` type + +`SimBridge::send_step_canvas_response`, not a wrapped/multiplexed response +envelope. My reasoning for this over a more general `envelope_version`/ +`msg_type` integer tag: zero new demux machinery, proven five times already, +and I don't have a seventh shape waiting that would justify generalizing the +mechanism now — if you want to formally name a reusable envelope *type* in +the record text for whatever comes after this one, that's a framing choice +the record can state independent of what actually ships in code (the code +only needs one more discriminated variant either way). + +**2. Legacy coexistence: `district_window` survives unchanged, and I'd let +the envelope subsume only the NEW step-canvas traffic, not migrate existing +windowed traffic onto it.** Reasoning: `district_window`'s current consumer +(the existing `AtlasWindowViewer`) isn't being redesigned by this workshop — +it's being *replaced* by Stig's new stepped viewer. There's no reason to +migrate the old carrier's wire shape when the old carrier's only client is +also being deleted; migrating it would be extra engineering (touch the +legacy struct, the legacy cache key, the legacy client decode path) for a +code path with a defined end-of-life, not an extended future. Your lean +matches mine exactly — keep `district_window` alive, byte-for-byte, for +whatever old-viewer traffic exists until the client cutover, then it goes +cold and gets deleted in a follow-up cleanup ticket. One carrier "cleaner in +the abstract" isn't worth the extra migration surface for a carrier that's +already scheduled for deletion. + +**3. Step-0/Region rides the NEW envelope, not the legacy whole-body +path.** Now that you and Araminta have confirmed Region = step-0, it's +generated by the *same* mechanism every other step uses (a `StepCanvasRequest` +at `step_index: 0`, D-243 rung = Region, viewport-sized per the capped-tile +mosaic shape T-1143 §4 already established) — not a special case riding +`district_grid`/`region_grid`'s existing whole-body `Option` fields on +`AtlasLayerResponse`. Putting step-0 on the legacy whole-body family would +mean the new stepped client has to speak two different response protocols +depending on which step it's on (the new envelope for steps 1-5, the old +family for step 0) for no architectural gain — one request shape, one +response shape, across the whole ladder is simpler for both sides and is +what "step boundaries = compute-chunk boundaries" (premise 8) already implies +uniformly. The existing `region_grid`/whole-body layers keep serving whatever +non-stepped consumers still want them (if any survive the cutover) — they're +just not what step-0 of the new ladder uses. + +### Araminta's dense/sparse framing question — answered + +Relayed via the coordinator: does the tagged envelope frame the step-canvas +response as (a) one flat tagged message with N typed fields (her default, +today's `DistrictWindowLayer` shape scaled up), or (b) dense/sparse split +into separately-tagged sub-messages enabling partial/progressive delivery? + +**Answer: (a), one flat message — I don't think (b) is forced by anything in +my design, and I'd argue against it for this pass specifically.** My +`StepCanvasResponse` struct above is exactly her default assumption: one +`Ready | Pending | Error` response carrying all eight fields (six dense + +`courses` + `cliffs`) together, matching the whole-payload-together +precedent D-225 already established for `Layer1Output` ("the layers are +produced together in one drainage pass... per-layer requests save no compute +and only add round-trips") and the countervailing case (b) would trade +against isn't free: + +- **Cost argument against (b):** every field in the response comes off the + *same* row-chunked derive pass (`build_district_window_layer`'s loop + computes all six dense fields per cell in one traversal, per my own + measured numbers) — splitting delivery into sub-messages doesn't save any + server-side compute, it only adds round-trips and partial-state handling + on both ends for a payload that's already fully computed by the time the + *first* sub-message could go out. This is the identical argument D-225 used + against per-layer whole-body requests, and it applies with the same force + here. +- **The progressive-paint UX case is real but belongs one layer up, not + in the wire protocol.** If "classification planes first, courses/cliffs + after" is worth doing for perceived latency, it's cheap to get without + splitting the *server response* — the client can paint the terrain RTT + layer as soon as the (whole) response decodes and defer building the + annotation layer's draw calls by a frame or two, since courses/cliffs are + a tiny fraction of total decode time (sparse lists, a few KB, against a + multi-hundred-KB-to-tens-of-MB dense payload per Table B below). That's a + client-side rendering-order choice Stig can make freely without any + server-side protocol complexity — no reason to pay demux/partial-delivery + cost on the wire for a UX win available for free on the receiving end. +- **Where (b) would earn its keep and doesn't yet:** if a future + measurement showed the dense-field derive pass and the sparse-list + derive pass (courses/cliffs) were on meaningfully different cost/latency + timelines — e.g. if course invention turned out to be slow enough to want + to ship the terrain first and stream courses in later — that would be a + real argument for (b). Nothing measured this workshop shows that: courses + add "+0.09–0.21 ms against a ~5 ms baseline" per my round-1 citation of + `bench_course_cost_on_vs_off` — negligible, not staggered. I'd revisit + this if a future profiling pass finds a real timeline split, but I'm not + designing speculative complexity against a cost gap that isn't there. + +**Net: her default holds. One flat tagged `StepCanvasResponse`, all fields +together, matching D-225's whole-payload precedent — she can write the +client-facing field spec against shape (a) without qualification from my +side.** + +--- + +## (b) Seed-chaining ruling — independent re-derivation + +**Ruling: each step re-derives independently from `(seed, position)`. A +step's output is never consumed as literal input by a finer step's +derivation.** This closes round-1 open item 8, and it's not a new +architectural call — it's D-227 applied to a question I should have answered +more explicitly in round 1 instead of flagging as unmeasured. + +### The argument + +**D-227 purity is the whole case.** `subtile(x,y,z) = derive(seed, atlas, +position)` — every derived value is a pure function of the fixed inputs, not +of some other derived value's cached output. If a finer step consumed a +coarser step's *computed* array as an input (rather than re-deriving from the +same `(seed, position)` the coarser step also started from), the finer step's +correctness would depend on the coarser step having been computed first, with +a specific set of parameters, and cached — which means: + +1. **A cache-dependency chain**, exactly what D-227's "evictable, cache is a + bonus, never truth" discipline forbids. If the coarser step's cache entry + is evicted (my own TTL policy in §(d) below deliberately evicts sub-global + geometry), a finer step that depends on it as literal input either fails + to derive or must first regenerate the coarser step — silently + reintroducing exactly the "step boundaries as compute-chunk boundaries" + complexity premise 8 was trying to keep simple, plus a correctness risk + if the regenerated coarser step doesn't byte-match what was evicted (it + would, since both are pure functions of the same seed — but now *proving* + that becomes load-bearing instead of automatic). +2. **A determinism ordering hazard.** Two clients requesting the same finer + step from a cold cache, in different orders relative to any coarser-step + requests, would need to agree on exactly which coarser-step values fed + the derivation — this is solvable, but it's solving a problem that + doesn't exist if every step independently calls `derive_at_metres(seed, + position, this_step's_own_min_wl_m)`. +3. **It contradicts my own measured cost model.** All of T-1178/T-1154's + numbers (the flat ~190–220 ns/cell parallel rate at every rung) are for + independent per-cell derivation calls — `derive_window_cell` never reads a + neighboring cell's *derived* value, let alone a different rung's. If a + finer step needed the coarser step's actual array as input, that's a + different, unmeasured computational shape (a dependency graph between + rasters, not a row-chunked independent-cell map) — my GO verdicts at + every rung do not cover that shape. + +**What Jeroen's outline phrase actually means, read against the code that +exists today.** "This at the same time serves as seed information for the +deeper cascade" — I read this as describing the existing, already-shipped +pattern in `district_profile.rs`/`detail_scatter.rs`: a district's +`invent_primitives` call reads the *region baseline* (a coarser, independent +derivation) as one of several deterministic inputs alongside the fixed seed +and position, the same way `voxel_relief` already contributes to +`elev_q`/`slope_q` at Block spacing today (confirmed in my own T-1154 +measurement doc). That's not "consume the coarser step's computed array +element-for-element as this step's starting point" — it's "the coarser +rung's own *independently-derivable* baseline value at this position is one +of several `(seed, position, ...)`-keyed inputs to the finer rung's own +independent derivation call." Both are pure functions; the finer one just +happens to call the coarser one's derivation function internally as a +sub-computation, not read its *cached* output. This distinction is exactly +what keeps D-227 intact: `derive_district(seed, pos)` calling +`region_baseline_at_district(seed, pos)` internally is still one pure +function of `(seed, pos)` — no cache dependency, no ordering hazard, because +the "coarser step" being referenced is re-executed as code, not looked up as +data. + +**Concretely, the ruling is: "consuming the coarser output" already happens, +and it happens the *only* way D-227 permits — as a nested pure-function +call, evaluated fresh every time, never as a read from the coarser step's +cache entry.** This is not new work; the region-baseline-feeds-district +pattern is the precedent, and step-canvas generation should follow it +exactly: a Tile-step's derive call may internally call the same +`region_baseline_at_metres`/`derive_at_metres`-family functions a Region-step +call would, at the same `(seed, position)`, but it never reads +`StepCanvasResponse` bytes from a prior request as an input. + +### What this settles and what it doesn't + +- **Settles:** my own cost numbers (measurement ②/③) are validated as the + correct model for step-canvas generation — no re-measurement needed, + because "independent re-derivation, coarser rung called as a nested + function" is exactly the shape those benchmarks already exercise + (`derive_at_metres` calling into the same octave-primitive functions at + every rung). +- **Doesn't settle:** whether every finer step's derivation *should* call + every coarser rung's function as a sub-computation (a "does Tile-rung + derivation call Block-rung, District-rung, AND Region-rung functions in + sequence, or just the immediately-coarser one?" question) — that's an + algorithm-composition detail for whoever implements `derive_at_metres`'s + Tile/Block extension, not an architecture question this ruling needs to + answer. The architecture-level answer is just: however many coarser + functions get called, they're called fresh, never read from a response + cache. + +--- + +## (c) Step-ladder tables — with Tyre + +**Correction to my own first pass, before the tables:** my original draft of +this section used a "Tile-adjacent (4 m)" spacing that is **not a D-243 +rung** — I conflated a T-1154 benchmark data point (which measured 4 m only +as a probe value, not a named ladder level) with a real step. Tyre's message +caught this by construction — his three skeletons are built strictly from +D-243's actual six rungs (voxel 1 m, chunk 64 m, block 128 m, quarter 512 m, +district 2,048 m, region 204.8 km) and nothing else, which is the right +discipline and the one I should have applied the first time. The tables +below use only real D-243 rungs, per his skeletons, with my cost fills. + +**Measurement ⑥ status:** not received — checked +`docs/workshops/body-map-viewer/measurements/`, still only ①–⑤ present as of +this document. Cost fills below are ①–④ only; ⑥ affects Stig's shader-vs-CPU +call, not these tables. + +**Canvas-extent convention for every row below:** the fixed 3840×2160 px +budget you proposed, viewport-sized (never canonical past step 0) — using +your suggested convention directly, not a different one. + +### Cost fills against your three skeletons + +**Option A — one-rung-per-step (6 steps): Region → District → Quarter → +Block → chunk (64 m) → voxel (1 m).** + +| Step | D-243 rung | Spacing | Step factor (from prev) | Viewport extent @ 3840×2160 canvas | Cells | Derive cost (parallel) | Wire cost (PNG-per-field) | +|---|---|---:|---:|---|---:|---:|---:| +| 0 | Region | 204.8 km | — | whole body (capped-tile mosaic, T-1143 §4) | 8,294,400 | 1,827 ms (measured) | 16.88 MB (measured) | +| 1 | District | 2,048 m | 100× | 7,864 × 4,424 km | 8,294,400 | 1,827 ms (measured) | 16.88 MB (measured) | +| 2 | Quarter | 512 m | 4× | 1,966 × 1,106 km | 8,294,400 | ~1,827 ms (same band, T-1154 confirms Quarter costs the same per-cell rate as District) | ~16.88 MB | +| 3 | Block | 128 m | 4× | 491 × 276 km | 8,294,400 | ~1,827 ms (measured directly at Block spacing, T-1154: same ~190–220 ns/cell parallel band) | ~16.88 MB | +| 4 | chunk | 64 m | 2× | 246 × 138 km | 8,294,400 | **UNMEASURED — flagging honestly, not filling with a guess.** T-1154 tested Block (128 m), Tile-adjacent (4 m — an ad hoc probe, not this rung), and Tile (1 m). 64 m spacing was never benched directly. The cutoff-mechanism finding (T-1154: `VOXEL_OCTAVE_WAVELENGTHS_M` bottoms at 128 m, so nothing truncates below Block) strongly implies chunk costs the *same* flat ~1.8 µs/cell single-thread / ~200 ns/cell parallel rate every other rung in this band does — but "strongly implies" is not "measured," and I'm not reporting a number I didn't run. | Same caveat — implied ~16.88 MB by the flat-rate pattern, not measured. | +| 5 (deepest) | voxel | 1 m | 64× | **8.3M cells at 1 m spacing = 3.84 × 2.16 km — this is the wrong deep-step shape**, see the general note below | 8,294,400 | same caveat as row 4 pattern, ~1,827 ms if it held | ~16.88 MB | + +**Option A's real problem isn't the missing chunk measurement — it's that a +fixed 3840×2160 canvas at voxel (1 m) spacing is oversized for what a real +viewport needs at 10 px/tile** (3840 px ÷ 10 px/tile = 384 m — my own T-1154 +"realistic deep-step canvas" used exactly this reasoning to arrive at +216×384 m, 82,944 cells, not an 8.3M-cell canvas). Applying your fixed-canvas +convention literally at every step breaks down at the deepest step +specifically because the fixed-px-budget convention and the display-ratio +target (10 px/tile) can't both hold at 1 m spacing simultaneously — this +isn't a flaw in your skeleton, it's a flaw in "same canvas budget at every +step" as a blanket rule, which is a genuinely useful thing for these tables +to expose. I'd apply the fixed-3840×2160 convention at every step **except** +the deepest, where the canvas must instead be sized to the display-ratio +contract (my round-1 T-1154 number: 216×384 m, 17 ms, ~486 KB) — noted +consistently in all three tables below, not just this one, since it's a +convention-level correction, not a skeleton-specific one. + +**Option B — skip-chunk (5 steps): Region → District → Quarter → Block → +voxel (1 m).** + +| Step | D-243 rung | Spacing | Step factor | Viewport extent @ 3840×2160 (except deepest) | Cells | Derive cost (parallel) | Wire cost | +|---|---|---:|---:|---|---:|---:|---:| +| 0 | Region | 204.8 km | — | whole body | 8,294,400 | 1,827 ms (measured) | 16.88 MB (measured) | +| 1 | District | 2,048 m | 100× | 7,864 × 4,424 km | 8,294,400 | 1,827 ms (measured) | 16.88 MB (measured) | +| 2 | Quarter | 512 m | 4× | 1,966 × 1,106 km | 8,294,400 | ~1,827 ms (T-1154 same-band confirmation) | ~16.88 MB | +| 3 | Block | 128 m | 4× | 491 × 276 km | 8,294,400 | ~1,827 ms (measured directly, T-1154) | ~16.88 MB | +| 4 (deepest) | voxel | 1 m | **128×** | 216 × 384 m (display-ratio-sized, per the correction above, NOT the fixed-canvas convention) | 82,944 | **17 ms (measured directly, T-1154)** | **~486 KB (measured rate, T-1179)** | + +**This is the skeleton I'd recommend, and the missing-chunk-measurement +problem is exactly why: every single row in Option B is either directly +measured or in the same measured cost band as a directly-measured rung — +there is no row I have to caveat as unmeasured.** That's not a coincidence +of which skeleton I like; it's a direct consequence of chunk (64 m) never +having been benched, and Option B is the one skeleton among your three that +doesn't need it. If chunk's cost does turn out to match the flat-rate pattern +(likely, per the cutoff-mechanism reasoning above), Option A costs the same +as Option B row-for-row at every *other* rung anyway — the only real +difference is Option A has six fetches per full zoom-out-to-in traversal +where Option B has five, and pays one unmeasured/probably-redundant rung to +get there, since chunk sits between Block and voxel with a huge factor +either way (128× Block→voxel in Option A's own 64m→1m final leg, vs 128× +Block→voxel directly in Option B — the chunk step doesn't actually *reduce* +the biggest jump in the ladder, D-243 names chunk as "stream/derive unit," +not a natural *display* rung, which is exactly Tyre's own hesitation about +it in his message). + +**Option C — coarse-doubled (4 steps): Region → Quarter → Block → voxel, +skipping District.** + +| Step | D-243 rung | Spacing | Step factor | Viewport extent | Cells | Derive cost | Wire cost | +|---|---|---:|---:|---|---:|---:|---:| +| 0 | Region | 204.8 km | — | whole body | 8,294,400 | 1,827 ms (measured) | 16.88 MB (measured) | +| 1 | Quarter | 512 m | **400×** | 1,966 × 1,106 km | 8,294,400 | ~1,827 ms (same band) | ~16.88 MB | +| 2 | Block | 128 m | 4× | 491 × 276 km | 8,294,400 | ~1,827 ms (measured) | ~16.88 MB | +| 3 (deepest) | voxel | 1 m | 128× | 216 × 384 m (display-ratio-sized) | 82,944 | 17 ms (measured) | ~486 KB (measured) | + +**Cost-wise, Option C is not cheaper than B — the per-step cost numbers are +identical to B's (same rungs at the same measured rate), it just skips +District.** So the choice between B and C is **not a cost question at +all** — every row I can cost is the same regardless of which skeleton wins. +It's purely the UX-pacing question your message already correctly separated +out (my answer to your question 2 below). + +### Answering your three direct questions + +**1. Which skeleton do the costs favor?** None of them are uncomfortable — +every derive cost across all three options is comfortably interactive (17 ms +to 1.8 s, the same band my round-1 doc already established as safe against +any step-cross tolerance). **The costs don't pick a winner; they only +disqualify chunk (64 m) from being *load-bearing* for the decision**, because +it's the one rung nobody has actually measured. That makes Option B (which +never needs chunk) the *cleanest* recommendation on evidentiary grounds, not +because A or C are unaffordable — I'd frame this to Jeroen as "B is the +option with zero asterisks," not "A and C are too expensive." + +**2. Does the D-243 factor unevenness (100×, 4×, 4×, 128×) matter for +serving/precache, or is it purely UX pacing?** Both, but asymmetrically — +**it matters for serving in exactly one place (the Region→District 100× jump +at step 0→1), and is purely UX pacing everywhere else.** Reasoning: my cache +tiers (§(d) below) already treat Region as a categorically different tier +(keep-always, global) from every step below it (storage-evictable) — the +100× factor at the top of the ladder lines up with a real architectural +seam that already exists in the cache design, not just a display jump. The +4×/4×/128× factors *within* the sub-global tier don't correspond to any +serving-side seam — District, Quarter, and Block are all "sub-global +geometry," evicted by the same storage-TTL mechanism, served by the same +row-chunked derive path, at the same flat per-cell cost. So: the top jump is +architecturally real (it's the global/sub-global cache boundary already +established for other reasons); every jump below it is exactly what you +said — a UX-pacing question about how gradual the zoom feels, with no +serving-side consequence I can find. This is useful to tell Jeroen directly: +he can pick B vs C on feel alone for the lower rungs without worrying he's +picking a cost or caching regression. + +**3. Viewport metre-extent assumption per step?** Confirmed: your fixed +3840×2160 canvas budget, with the one correction above (the deepest step +must use the display-ratio-sized canvas — 216×384 m at 10 px/tile — not the +fixed-px-budget convention, because the two conventions are mutually +incompatible exactly at voxel spacing, per Option A's row 5 above). I'd +apply "fixed 3840×2160 px budget" as the rule for every step except the +last, and "sized to the display-ratio contract" as the rule for the last +step specifically — both tables B and C already reflect this split. + +### Confirmation from Stig's measurement ⑥ (relayed, landed after the tables above were drafted) + +Stig's `Image.set_pixel` colorize cost measurement (77.5 ns/cell flat across +330K–8.3M cells, 25.7 ms at 330K, beating `PackedByteArray`-direct ~2×) closes +his round-1 open item and independently confirms c1 as CPU-first — not +something my tables need to change for, but his own px-band recommendation +(1×1 at Block/Tile "full fidelity," 1×1 preferred at Quarter/District as long +as the realistic canvas stays under ~2M cells, ~5×5 fallback reserved for +shallow/orbital steps where canvas *extent* is what grows, not density) is +worth stating explicitly against my tables because **it's the same policy my +cost fills already assume, from a different, client-side cost driver.** + +Every row in Options A/B/C above already uses 1 gridunit-per-pixel (cell +count = canvas px count) at every step from Region through Block — I never +invoked the ~5×5 fallback anywhere in my server-side cost fills, because +nothing in my derive-cost numbers forced it (the flat ~190–220 ns/cell +parallel rate holds at every canvas size I measured, so there was no +server-side reason to sample coarser-than-1:1 at any step). Stig's finding +adds the client-side half of that same argument: colorize cost is +**cell-count-driven, not display-density-driven**, so downsampling to 5×5 at +a step that doesn't need it wouldn't even save client-side coloring time +proportionally — the cost is paid per gridunit regardless of how many screen +pixels each one covers. **Two independent cost models (server derive, client +colorize) land on the same policy: 1×1 wherever the resulting cell count is +affordable (every step through Block, per both his ~2M-cell comfort line and +my own flat-rate measurements), reserving the coarser ratio only for the one +place canvas *extent* — not density — is actually the pressure (the +whole-body Region/orbital step, T-1143 §4's capped-tile mosaic).** This +confirms, not revises, every table above; I'm noting it because two +independently-measured cost models agreeing on a policy neither one was +designed to argue for is exactly the kind of convergence worth flagging to +Jeroen rather than leaving as a coincidence buried in two separate documents. + +--- + +## (d) Cache tiers — final spec incorporating Jeroen's storage-eviction amendment + +Jeroen's amendment (verbatim, `lead-interview-1.md` ruling 2): *"we still may +also want to evict non global level geometry based on time to save storage +for planets the player visits but never goes back to."* Captured there as: +**staleness-eviction and storage-eviction are distinct axes** — geometry +never goes stale (re-derivable, byte-identical forever), but sub-global +geometry still gets evicted on time-since-last-visit as a storage-budget +policy, independent of whether it's "correct." The global tier alone is +keep-always. + +This sharpens, rather than replaces, my round-1 TTL proposal — I had +conflated "TTL" as a single staleness-driven mechanism; Jeroen's ruling +correctly separates it into two policies with different triggers and +different consequences on a miss. + +### Tier 1 — Global (Region/orbital rung): keep-always, my round-1 number stands + +**~174 MB PNG-encoded across all ~273 bodies** (recomputed check against +Table B's step-0 numbers above: step 0's capped-tile mosaic shape, per +T-1143 §4, is smaller per-body than a full district-spacing canvas — my +round-1 174 MB used district-spacing as a conservative upper bound, so 174 MB +remains a safe ceiling, not an underestimate, for whatever the actual +step-0/orbital canvas shape lands on). + +- **D-203-shaped resource extension** — `BodyWorldState` (or a sibling + resource matching its exact pattern) gains an `orbital_canvas: + Option` field. +- **Never evicted by time-since-last-visit.** This is the one tier Jeroen's + amendment explicitly excludes ("the global tier alone is keep-always") — + "always keep the global level" from the original outline stands unmodified + by the amendment; the amendment only sharpens what happens *below* this + tier. +- **Still an evictable cache in D-227's sense, not stored truth** — a + corrupted or manually-cleared global-tier entry is a recompute (~24 ms + hydrology + the derive cost from Table B row 0), never data loss. "Keep- + always" is a *policy choice about when eviction runs*, not an exemption + from D-227's "cache, never source of truth" discipline. + +### Tier 2 — Sub-global geometry (District/Quarter/Block/Tile step canvases): dual-axis eviction + +This is where Jeroen's amendment lands. Two independent axes, evaluated +separately, either one sufficient to evict an entry: + +**Axis 1 — staleness: NEVER for geometry.** Morphology, elevation, moisture, +vegetation, glaciation, settlement presence, river/cliff geometry — every +field D-227 covers as `derive(seed, position)` — is byte-identical on every +recompute. There is no staleness concept for these fields at all; an entry +that exists is always correct, forever, regardless of age. This part of my +round-1 proposal (the `time_decay`/`distance_decay` multiplicative TTL +formula) was **wrong to frame as staleness** — re-reading Jeroen's ruling, I +was solving "when does this become wrong" for data that is never wrong. What +I actually needed was axis 2. + +**Axis 2 — storage-budget eviction: time-since-last-visit, a distinct +mechanism.** A per-entry `last_accessed` timestamp (the exact field D-203's +`BodyWorldState` already carries — I'm reusing the pattern, not inventing a +new one), swept periodically (not per-tick — a coarse background sweep, +matching the "not blocking for user output" premise). An entry whose +`last_accessed` exceeds a per-rung threshold is evicted **for storage +reasons, not correctness reasons** — the distinction matters operationally: +a storage-eviction miss is silently identical in cost to a cold-start miss +(re-derive at Table B's measured cost, 17 ms–1.8 s depending on rung), it is +never treated as an error or a "the data might be stale, re-verify" +condition, because there is nothing to verify — it's a pure function, it +recomputes to the same bytes every time. + +**Concrete formula, revised from round 1:** + +``` +evict_if: time_since_last_visit(entry) > STORAGE_TTL[rung] +``` + +One term, not the three-term multiplicative formula I proposed in round 1 — +Jeroen's amendment removes the need for `distance_decay` and `detail`-scaling +as *separate* factors once staleness is off the table: what's left is purely +"how long has it been since anyone looked at this," which is +time-since-last-visit alone. `STORAGE_TTL[rung]` is still the one per-rung +tunable (deeper rungs plausibly get a shorter floor, since they're both +cheaper to regenerate and cover less ground per entry — same reasoning as my +round-1 doc, just now attached to a storage-thrift rationale rather than a +staleness one), but the mechanism collapses to a single, legible sweep rather +than a compound scoring formula. This is a simpler design than what I +proposed in round 1, which I'll flag plainly: **Jeroen's amendment made my +original proposal simpler, not more complex** — separating the two axes +removed a term instead of adding one, because "distance from current focus" +turns out to be redundant with "time since last visited" for a +storage-thrift purpose (a location far from current focus that the player +*just* visited doesn't need evicting for storage reasons yet; one they +haven't touched in a long session, regardless of current distance, does) — +distance matters for *prioritizing what to precache next*, which is a +different mechanism (smart precache, premise 1) than *what to evict for +storage*, and conflating them in one formula was my round-1 mistake. + +**What "player visits a planet, never goes back" concretely evicts under +this policy:** every sub-global step-canvas entry for that body — District +through Tile, every step index, every center the player's viewport ever +requested — ages out independently on its own `last_accessed`. The global +(Region/orbital) entry for that same body does NOT evict (Tier 1's +keep-always rule), so "atlas navigation snappy after first calc" survives +even for an abandoned body — the player can still open the Atlas and see the +planet at global zoom instantly; only the finer-grained "I was standing at +this exact district window" entries age out, which is exactly the storage +being reclaimed (per-step canvases are the bulk of the byte cost — District +alone is 16.88 MB per cached window per Table B, vastly larger than the +174 MB *total* global tier across all 273 bodies). + +**Sim-state fields (frozen/flooded) — the map-time-axis TTL, separate from +both axes above, unaffected by this amendment.** Jeroen's lead-interview +ruling on the map time axis (a different ruling from the storage-eviction +amendment, but adjacent) already established: static geometry cached +indefinitely-fresh, frozen/flooded carried as separately-cached short-TTL +planes re-requested as sim time advances. This is a genuine staleness TTL +(the sim state actually can become wrong as game time passes), and it's +orthogonal to the storage-eviction axis above — a frozen/flooded plane has +both a staleness TTL (it goes wrong after some sim-time interval) AND is +subject to the same storage-eviction sweep as its parent geometry entry (if +nobody's visited the body in a long time, evict the whole cached window, +sim-state plane included, for storage reasons — regenerating both on the +next visit is cheap either way). + +### Tier 3 — client-side (Stig's, composing not competing, per lead-interview ratification) + +Restating only to confirm compatibility, not re-designing Stig's tier: +Stig's `FileAccess` disk cache dir + in-memory LRU (round-1 §4) already +implements the identical two-axis split independently — his "geometry: +LRU-evict-only, no TTL" is my Axis 1 (never stale) plus his own LRU acting as +*his* storage-thrift mechanism (client disk budget, not server RAM budget); +his "sim-state: explicit TTL sweep" is the same map-time-axis TTL described +above. **The two tiers were designed independently and landed on the same +two-axis shape** — worth naming as convergent validation of Jeroen's +amendment being the right cut, not just a server-side patch. + +### Summary table + +| Tier | Scope | Staleness eviction | Storage eviction | Size | +|---|---|---|---|---| +| 1 — Global | Region/orbital canvas, per body | Never (D-227 pure) | **Never** (Jeroen: keep-always) | ~174 MB, all bodies | +| 2 — Sub-global geometry | District→Tile step canvases | Never (D-227 pure) | **`time_since_last_visit > STORAGE_TTL[rung]`** | unbounded resident, bounded by sweep | +| 2b — Sim-state planes | Frozen/flooded overlays | **Yes** — real staleness TTL (map-time-axis ruling) | Same sweep as parent geometry entry | small, per-window | +| 3 — Client disk/memory | Stig's `FileAccess` + in-memory LRU | Mirrors tier 2/2b split exactly | Client-local LRU + disk budget | ~440 MB ceiling at 5×5, disk-safe | + +**Note on the "5×5" figure in the row above:** that's Stig's round-1 citation +of red flag 2's original conservative estimate for the *global* tier +specifically (all 273 bodies' Region/orbital canvases, disk-resident) — it +predates his own measurement ⑥ px-band refinement above and isn't affected by +it, since ⑥'s finding is about *sub-global* steps (Block/Quarter/District) +staying 1×1, not about the global tier's own sizing. My own Tier 1 number +(~174 MB, PNG-encoded) is the tighter, measured figure for the same tier — +both numbers describe the same "keep-always" budget from two different +starting estimates (his conservative pre-④ ceiling vs. my post-④ measured +rate); not a live disagreement, just two vintages of the same calculation +worth reconciling to one number (mine) before this goes to filing. + +--- + +## Summary for interview 2 + +1. **Envelope mechanics are lower-risk than the governance framing might + suggest** — the tagged-marker pattern the sixth shape needs is already + proven five times in `bridge/mod.rs`; this is one more `Inbound` variant + and one more `SimBridge` method, not a demux redesign. Legacy + `district_window` needs zero coexistence engineering — it just keeps + working until nothing requests it. Step-0/Region rides the same new + envelope as every other step, not the legacy whole-body family. One flat + tagged response (Araminta's default shape (a)), not a dense/sparse split + — the progressive-paint UX case is real but cheaper to get client-side + than by adding wire-protocol complexity for a cost split the measurements + don't show. +2. **Seed-chaining: independent re-derivation, ruled and argued from D-227 + purity** — a finer step may call a coarser rung's derivation *function* + internally (the existing region-baseline-feeds-district precedent), but + never reads a coarser step's cached *response* as literal input. My + round-1 cost numbers are validated as the correct model for this shape. +3. **Three step-ladder skeletons costed against Tyre's D-243-disciplined + options; Option B (skip-chunk, 5 steps: Region→District→Quarter→Block→ + voxel) recommended** — the only skeleton where every row is directly + measured or in a directly-measured cost band, because chunk (64 m) was + never benched and Option B is the one option that doesn't need it. Costs + don't disqualify Option A or C (nothing is unaffordable), they just can't + fully justify chunk as its own step. The D-243 factor unevenness matters + for serving at exactly one seam (Region→District, which lines up with the + global/sub-global cache boundary) and is pure UX pacing everywhere below + it — Jeroen can pick B vs C on feel alone for the lower rungs. My own + first draft of this section used an invented 4 m "Tile-adjacent" spacing + that isn't a real D-243 rung — corrected once Tyre's message caught it; + the deepest step also needs a display-ratio-sized canvas (216×384 m, not + the fixed-3840×2160 convention that works at every shallower step). +4. **Cache tiers finalized**: Jeroen's amendment simplified my round-1 + three-term TTL formula into a cleaner two-axis model — geometry never + goes stale (removed from the eviction question entirely), sub-global + geometry evicts purely on time-since-last-visit for storage thrift, the + global tier alone is keep-always, sim-state planes keep their own + separate real staleness TTL. Stig's independently-designed client tier + landed on the identical two-axis shape, which I read as confirmation the + amendment cut the problem correctly. + +Nothing in this round changed a round-1 cost number. What changed is +precision: the cliff wire-shape tension resolves to Araminta's sparse +framing, the seed-chaining question resolves to independent re-derivation +with a named precedent, the cache-tier formula gets simpler (not more +complex) once staleness and storage-thrift stop being conflated, and the +step-ladder section itself got one honest correction (the invented 4 m +spacing) caught by live coordination with Tyre rather than by me catching it +alone — worth naming, since the correction is exactly what round 2's +synthesis format is supposed to produce. diff --git a/docs/workshops/body-map-viewer/lead-interview-1.md b/docs/workshops/body-map-viewer/lead-interview-1.md new file mode 100644 index 000000000..5a97d8f48 --- /dev/null +++ b/docs/workshops/body-map-viewer/lead-interview-1.md @@ -0,0 +1,58 @@ +--- +title: "Body Map Viewer — Lead Interview 1 (post round 1)" +description: "Jeroen's rulings on the round-1 open decisions" +type: workshop +status: active +workshop: body-map-viewer +created: 2026-07-25 +--- + +# Lead Interview 1 — Jeroen's rulings (post round 1) + +Held after [round-1-notes.md](round-1-notes.md). Presented: the three independent +convergences (tagged envelope triggered; viewport-sized canvases below +orbital/region; T-1177 cliff representation), the composing cache proposals, the +two named tensions, and the 10-item OPEN-FOR-SYNTHESIS list. + +## Ratified by silence (presented as settled-unless-objected; no objection) + +- **Tagged-envelope migration: triggered** (three seats, measured numbers). Round 2 + designs the mechanics. +- **Viewport-sized canvases at every step below orbital/region; canonical survives + only as the global tier.** (This also resolves the brief's canonical-vs-viewport + interview question.) +- **Cliff representation** `elevation` + `channel_depth` + `cliff_edge` (unanimous + among adopters). +- **Cache composition**: Dudley's server-side global tier (~174 MB PNG-encoded, + D-203-shaped resource, not SQLite) + Stig's client-side `FileAccess` cache dir — + compose, don't compete. Both rejected SQLite in every shape independently. + +## Rulings + +1. **Step ladder — "Round 2 proposes, I rule at interview 2."** The synthesis round + produces 2–3 concrete ladder tables (step counts, factors, per-step derivation + rung + measured cost); Jeroen picks from real tables, not principles. +2. **Map time axis — current state via TTL-split**, his prior hint made concrete: + static geometry cached indefinitely-fresh (determinism), frozen/flooded carried + as separately-cached short-TTL planes re-requested as sim time advances. + **Amendment (verbatim):** *"we still may also want to evict non global level + geometry based on time to save storage for planets the player visits but never + goes back to."* → Captured as a design rule: **staleness-eviction and + storage-eviction are distinct axes.** Geometry never goes stale (re-derivable, + byte-identical), but sub-global geometry entries still get evicted on + time-since-last-visit as a storage-budget policy. The global tier alone is + keep-always. +3. **Cliffs — sparse `CliffSegment` feature list, Phase-4 scope.** Aligns the + rarity finding, Araminta's encoding logic, and Tyre's scope ruling. Troblum + still stress-tests the rarity basis (one real body + synthetics) in round 2. +4. **Round 2: GO, with measurement ⑥ run now** — Stig runs the missing + `Image.set_pixel` cost measurement (330K/2.07M/8.3M cells, client-side) in + parallel with synthesis so the shader-vs-CPU (c1) call lands with a number. + Troblum joins with the adversarial mandate. + +## Carried to round 2 (synthesis work, no ruling needed) + +Envelope mechanics (message shape, demux, legacy-carrier coexistence); +seed-chaining data flow (independent re-derivation vs consuming the coarser step's +output — Dudley's costs assume the former); region-as-step-0 confirmation; +label placement de-dup at step seams (Tyre's gray case → Araminta). diff --git a/docs/workshops/body-map-viewer/measurements/t-setpixel-c1.md b/docs/workshops/body-map-viewer/measurements/t-setpixel-c1.md new file mode 100644 index 000000000..c77daa173 --- /dev/null +++ b/docs/workshops/body-map-viewer/measurements/t-setpixel-c1.md @@ -0,0 +1,182 @@ +--- +title: "Measurement ⑥: GDScript CPU-colorize cost — Image.set_pixel vs PackedByteArray-direct (330K–8.3M cells)" +ticket: T-1176 (body-map-viewer workshop, round 2 synthesis) +owner: Stig +workshop: body-map-viewer +status: complete +--- + +# Measurement ⑥ — CPU-colorize cost (the c1 input) + +## What this answers + +Round 1 (`stig-round1.md` §3) recommended CPU-side coloring for the terrain +RTT layer as the default, explicitly flagged as **pending** this exact +number, and named it the missing sixth measurement. Jeroen ruled at lead +interview 1: proceed, run ⑥ now, so the c1 shader-vs-CPU call lands with a +number instead of a "known-good fallback" hedge. + +This measures the classify-byte → palette-lookup → write-into-`Image` loop — +the CPU-side half of the terrain-layer build, at the same three canvas sizes +(330K / 2.07M / 8.3M gridunits) everything else in the appendix was measured +at — so it can be compared directly against ④'s encode/decode numbers and +①'s derivation numbers on the same size axis. + +## Method + +**Headless is correct here, unlike ⑤.** ⑤ (ImageTexture upload) required a +real GPU-backed display because Godot's headless dummy renderer fakes GPU +texture uploads — timing that path headless would have measured nothing. +This measurement has **no GPU/rendering-driver dependency at all**: +`Image.set_pixel()` and `PackedByteArray` element writes are ordinary +CPU-side data-structure operations, and `Image.create()`/ +`Image.create_from_data()` allocate a plain in-memory buffer — none of it +touches the RenderingServer or a swapchain. Confirmed by inspection of the +actual production code path this mirrors +(`atlas_window_overlay.gd::_rebuild_texture_if_needed`, which itself never +does anything GPU-side until the *separate* `ImageTexture.create_from_image` +call — the exact boundary ⑤ already priced). Run via +`godot --headless --path client --script res://tmp_drive_t_setpixel_c1.gd` +from the main checkout, no `DISPLAY` needed. + +- Driver: a temporary `SceneTree` script (`client/tmp_drive_t_setpixel_c1.gd`, + deleted after this measurement — not committed, same convention as ⑤'s + `tmp_drive_t1180.gd`). +- Two candidate implementations, since the delta between them **is** the c1 + decision input (per the task instruction): + - **(A) `Image.set_pixel(x, y, Color)` per cell** — the exact shape + `atlas_window_overlay.gd`'s `_rebuild_texture_if_needed()` / + `_build_tile_texture()` use today: read a classification byte from a + per-cell array, look up a `Color` in a palette table, call + `img.set_pixel()`. + - **(B) Direct `PackedByteArray` buffer write + `Image.create_from_data`** + — skip the per-pixel method call and `Color` object construction; look + up a 4-byte RGBA quad and write it directly into a flat buffer at + `(row*width+col)*4`, then hand the whole buffer to + `Image.create_from_data()` once. Same output format + (`Image.FORMAT_RGBA8`) as path A and as today's production code. +- **Realistic colorize loop, not a synthetic fill.** The per-cell + classification array is a 17-zone D-239 §6 morphology-vocabulary spread + (`RandomNumberGenerator`, fixed seed 1234, `randi_range(0, 16)` per cell) + — every class actually gets looked up across the canvas, exercising real + branchy palette-array access rather than a constant/degenerate case that + would flatter either path. +- 12 iterations per case, 2 discarded as warmup, 10 kept. Median, p95, min, + max computed over the 10 kept samples. Timed with `Time.get_ticks_usec()` + immediately around each colorize call (palette-array construction and the + synthetic classification array are built once, outside the timed loop). +- Run twice back-to-back for stability confirmation (below). + +## Environment + +Same machine as ①–⑤ (16-core, Rayon 14, no GPU involvement in this +particular measurement since it's headless). No background load running +during this measurement. + +## Results — run A + +| size | path | median_ms | p95_ms | min_ms | max_ms | +|---|---|---:|---:|---:|---:| +| 330K | A_set_pixel | 25.695 | 27.852 | 24.644 | 27.852 | +| 330K | B_byte_buffer | 51.033 | 51.510 | 49.246 | 51.510 | +| 2.07M | A_set_pixel | 165.170 | 174.554 | 158.683 | 174.554 | +| 2.07M | B_byte_buffer | 319.174 | 333.268 | 308.201 | 333.268 | +| 8.3M | A_set_pixel | 643.514 | 655.528 | 634.316 | 655.528 | +| 8.3M | B_byte_buffer | 1261.385 | 1284.790 | 1245.232 | 1284.790 | + +## Results — run B (stability re-run) + +| size | path | median_ms | p95_ms | min_ms | max_ms | +|---|---|---:|---:|---:|---:| +| 330K | A_set_pixel | 26.186 | 26.892 | 25.238 | 26.892 | +| 330K | B_byte_buffer | 52.566 | 55.523 | 49.407 | 55.523 | +| 2.07M | A_set_pixel | 161.351 | 164.131 | 155.692 | 164.131 | +| 2.07M | B_byte_buffer | 316.721 | 329.357 | 310.894 | 329.357 | +| 8.3M | A_set_pixel | 641.453 | 742.036 | 637.308 | 742.036 | +| 8.3M | B_byte_buffer | 1266.614 | 1291.422 | 1252.194 | 1291.422 | + +Run A and run B agree within ~2–3% on every case (the one outlier, 8.3M +`A_set_pixel` p95 at 742 ms vs 656 ms in run A, is a single high sample in a +10-kept-sample set — the median, the number this doc leads with, moves by +under 0.3ms between runs). Same stability profile as ⑤'s own two-run +confirmation. + +## Headline numbers (using run A, run B confirms stability) + +| size | path | median | ns/cell | +|---|---|---:|---:| +| 330K (331,776 cells) | `set_pixel` | 25.7 ms | **77.5 ns/cell** | +| 330K | byte-buffer | 51.0 ms | 153.8 ns/cell | +| 2.07M (2,073,600 cells) | `set_pixel` | 165.2 ms | **79.7 ns/cell** | +| 2.07M | byte-buffer | 319.2 ms | 154.0 ns/cell | +| 8.3M (8,294,400 cells) | `set_pixel` | 643.5 ms | **77.6 ns/cell** | +| 8.3M | byte-buffer | 1,261.4 ms | 152.1 ns/cell | + +**Per-cell rate is flat across all three sizes for both paths** (~78 ns/cell +for `set_pixel`, ~153 ns/cell for byte-buffer, both within ~3% across the +25× cell-count range from 330K to 8.3M) — no cliff, matching the "flat rate +holds at scale" pattern every other measurement in this appendix (①②③④⑤) also +found. This is a clean, linearly-scaling GDScript-interpreter cost, not an +algorithmic blowup. + +**`Image.set_pixel` is ~2× FASTER than the direct `PackedByteArray` write — +the opposite of the naive assumption.** I expected the raw-buffer path to +win by skipping `Color` object construction and the `set_pixel` method-call +overhead; measured, it loses by roughly 2×, consistently at every size. Read +on this: GDScript's own per-element `PackedByteArray` indexed write +(`buf[off] = ...`, four separate indexed writes per cell in path B) carries +enough per-access interpreter overhead of its own that it outweighs whatever +`set_pixel`'s internal `Color`-to-RGBA8-conversion cost is; `set_pixel` is +presumably a single, more-optimized engine-side call per pixel rather than +four separate GDScript-level array-index operations. This is a genuinely +useful finding for implementation: **prefer `Image.set_pixel` over hand- +rolled buffer writes in GDScript** — the "avoid the method-call" instinct +that's often correct in compiled languages does not hold here. + +## What this means for the frame/interaction budget + +**643 ms at the largest canvas size (8.3M cells) is not free — this is the +first CPU number in the whole appendix that DOES land near a budget that +matters, and it changes the shape of the c1 recommendation.** + +Context against the rest of the appendix, same 8.3M-cell canvas: +- Server derivation (②): ~1.7–1.8 s parallel — the client colorize cost + (0.64 s) is roughly a third of the server's own derive time, not a rounding + error against it. +- Wire decode, PNG-per-field (④): ~91 ms — colorize is **~7× the decode + cost** at this size. +- ImageTexture upload (⑤): ~3–5 ms — colorize is **~130–200× the upload + cost** at this size. + +At the 330K size (the workshop's own "acceptable fallback" resolution, +5×5 px/gridunit), `set_pixel` colorize costs **25.7 ms** — comfortably +interactive on its own (single-digit frame budgets, well under any +reasonable "player is waiting for this step" tolerance), and this is the +size that matters most for the deep/mid steps of the ladder per Dudley's +round-1 recommendation (viewport-sized canvases, not the 8.3M "stress +ceiling" shape — his own round-1 doc is explicit that 8.3M is a canvas-size +stress test, not a realistic per-step request shape). **At the sizes the +ladder will actually request in steady-state play, CPU colorize is cheap.** +It only becomes a real cost at the largest canvas sizes this appendix +measured as an upper-bound stress case, which per Dudley's own viewport- +sizing recommendation should rarely if ever be requested as a literal +step-canvas payload. + +## Implication for the hold-fetch-swap sequence (§2 of my round-1 doc) + +Colorize happens once per arrived step canvas, in the hold-fetch-swap +sequence's "on arrival: decode, colorize, upload, swap" chain. At 330K cells +(the realistic per-step size), colorize (25.7 ms) is now the **second most +expensive step** in that chain after server derivation itself, ahead of both +decode (④: ~3.6 ms at 330K) and upload (⑤: ~0.2 ms at 330K, +create_from_image RGBA8) — worth naming explicitly since round 1 didn't have +this number and could have under-priced the arrival-side cost. + +## Repro + +```bash +# Driver script was client/tmp_drive_t_setpixel_c1.gd (temporary, deleted +# after this measurement — not in the committed tree). Launch command used: +cd /var/mnt/data/projects/settled-reach +godot --headless --path client --script res://tmp_drive_t_setpixel_c1.gd +``` diff --git a/docs/workshops/body-map-viewer/measurements/t1177-hydrology.md b/docs/workshops/body-map-viewer/measurements/t1177-hydrology.md index f96971933..9ce12c451 100644 --- a/docs/workshops/body-map-viewer/measurements/t1177-hydrology.md +++ b/docs/workshops/body-map-viewer/measurements/t1177-hydrology.md @@ -417,3 +417,204 @@ time — or only ever paid once per body, offline/precomputed, the same way architecture question for the workshop's cache-tier synthesis (Dudley's question 4 / red flag 2), not a solver-cost question this measurement can settle alone. + +--- + +## POPULATION SURVEY (post-adversarial addendum, 2026-07-23) + +**Gap found by Troblum's adversarial pass, round 2:** the "273 bodies in +0.7–0.8 s" number above (`bench_parallel_273_bodies_at_512x256`) is real and +still correctly answers the question it was built for — but that question is +narrower than it reads at a glance. The bench calls `gj1c_512x256()` **once** +and reuses the same `elev`/`sea_level` inside the `par_iter` closure for all +273 iterations (`server/tests/hydrology_equilibrium_bench.rs`, confirmed by +direct read: `let (elev, sea_level) = gj1c_512x256();` sits outside the +`(0..body_count).into_par_iter().map(...)` block, and the 273×68-basin sum +in the original run's printed output is the tell — 68 is GJ1c's own basin +count, times 273). **It is GJ1c solved 273 times, not 273 distinct real +bodies.** It proves per-body-open wall-clock affordability (still true, still +a valid, useful measurement — see "What survives unchanged" below) but it +proves nothing about how basin counts, endorheic classification, or — +load-bearing for the cliff Phase-4 ruling — **carved-cliff-edge frequency** +vary across the real body population, since every one of the 273 "bodies" in +that bench is byte-identical terrain. + +This addendum closes that gap directly: every real committed heightmap PNG +in the repo, solved independently, once each, at the real production +512×256 working grid, with each body's own PNG-embedded `sea_level` (not a +shared default). + +### What was run + +New bench: `server/tests/hydrology_equilibrium_bench.rs::bench_population_survey_all_committed_bodies` +(added alongside the existing benches, same file, same `#[ignore]`d +release-only convention). Discovers every `wiki/star-systems/*/bodies/*/heightmap.png` +on disk (**267 files**, confirmed by direct `find` — matches the coordinator's +cited figure), decodes and downsamples each to 512×256 (identical path +`gj1c_512x256()` already used, generalized to every body), and solves each +one independently via Rayon `par_iter` — 267 distinct `solve()` calls on 267 +distinct elevation grids, each body's own real `sea_level` read from its +PNG's `sea_level` tEXt chunk (falling back to 0.3 only if a chunk is absent, +same convention `load_heightmap_png` already uses elsewhere). + +```bash +cd server +cargo test --release --test hydrology_equilibrium_bench bench_population_survey_all_committed_bodies -- --ignored --nocapture +``` + +**Known scope limit, stated plainly:** every body uses the same +`ClimateInputs { moisture_q: 55 }` — real per-body moisture would need +`BodyWorldState.districts`/`regions` climate context wired in, which the +original T-1177 prototype already documented as out of scope ("No per-basin +moisture/climate lookup"). This affects the **endorheic** classification +count (a real per-body climate pass could shift how many basins classify +endorheic vs. overflow) but does **not** affect the carving finding below — +carving is gated on ELEVATION geometry (the narrow two-independently-sealed- +basins-plus-single-cell-corridor condition the original module doc +describes), never on `moisture_q`; climate only decides the endorheic/ +overflow split of a basin whose carving behavior is already fixed by terrain +shape alone. + +### Results — MEASURED, run twice for stability + +| Run | Total wall time | ms/body avg | Total basins | Overflow | Endorheic | Lake cells | **Carved-outlet basins** | **cliff_edge cells** | +|---|---:|---:|---:|---:|---:|---:|---:|---:| +| Initial | 858.35 ms | 3.215 | 22,270 | 21,240 | 1,030 | 2,694,012 | **0** | **0** | +| Stability re-run | 857.30 ms | 3.211 | 22,270 | 21,240 | 1,030 | 2,694,012 | **0** | **0** | + +Both runs agree to within 0.1% on wall time and are **exactly identical** on +every basin/lake/carve count — which is exactly what determinism predicts +(same 267 PNG files in, same pure-function solver, same output every time) +and is itself a useful confirmation that the population survey's own results +aren't measurement noise. A determinism spot-check on the would-be top +outlier is built into the bench (re-solves the #1-by-cliff-edge-count body a +second time and asserts byte-identical `cliff_edge`/`channel_depth_scaled` +output) — it did not fire in either run because no body had any cliff_edge +cells to check, but the assertion path exists and is exercised the moment +any future body does carve. + +**Total wall time across the entire real 267-body population: under 0.9 +seconds, single Rayon pass, 16 cores.** This is a slightly smaller number +than the original 273×GJ1c bench's 0.7–0.8 s, consistent with the two benches +measuring genuinely comparable per-body costs (real bodies average +3.2 ms/body here vs. the original bench's ~2.6–3.0 ms/body on repeated +GJ1c — same order of magnitude, real terrain is not meaningfully more +expensive than GJ1c's own terrain on average). **The original "under a +second for the whole population" affordability verdict is CONFIRMED, not +just assumed, by this survey** — it was previously proven only for one +body's cost profile repeated; it now holds directly, measured on the real +population. + +### The headline finding: carving is not merely rare — it was not observed at all, population-wide + +**Zero carved cells. Zero carved-outlet basins. Zero bodies with any +`cliff_edge` cell. 267 out of 267 real committed bodies (100.00%) produced +no cliff/gorge output whatsoever**, at the real 512×256 production working +grid, with each body's own real elevation data and real sea level. + +This is a **stronger** result than the original T-1177 measurement's +"structurally rare" finding, not merely a confirmation of it. The original +measurement found zero carved cells across three benches — but two of those +three were synthetic gradient/ridge grids at unrelated resolutions (768×432, +3840×2160), and the one real body tested (GJ1c) is a single data point. It +was an honest, well-argued structural finding (the priority-flood +true-minimum-rim property genuinely does make single-basin carving +mathematically impossible, and that reasoning holds regardless of which body +you test), but "structurally rare, verified on one real body" and +"structurally rare, verified on the entire real population" are different +strengths of evidence, and Troblum's finding was right to demand the latter +before the cliff Phase-4 ruling leans on the rarity argument for its wire +cost case. + +**Top 15 bodies by cliff_edge cell count — every single one is 0.** The +"outlier" list below is therefore a non-event by construction (there is no +outlier to report — the highest cliff_edge count found anywhere in the +population is 0, tied 267 ways), but it's included for completeness and +because it shows the population has real geometric diversity (basin counts +range from GJ103c's 40 basins with zero endorheic classification to +GJ1e-m1's 187 basins with 12 endorheic) even while uniformly failing to +trigger carving — the "no carving" result isn't from a population of +near-identical flat worlds, it's from real, varied terrain that genuinely +never happens to produce the narrow two-basin-saddle geometry carving +requires: + +| Body | cliff_edge cells | carved-outlet basins | Total basins | Overflow | Endorheic | Lake cells | +|---|---:|---:|---:|---:|---:|---:| +| GJ1c | 0 | 0 | 68 | 66 | 2 | 4,623 | +| GJ1e-m1 | 0 | 0 | 187 | 175 | 12 | 24,041 | +| GJ1002b | 0 | 0 | 61 | 60 | 1 | 3,543 | +| GJ1005Ac | 0 | 0 | 54 | 53 | 1 | 3,395 | +| GJ103c | 0 | 0 | 40 | 40 | 0 | 1,463 | +| GJ105Ac | 0 | 0 | 57 | 54 | 3 | 8,224 | +| GJ1073c | 0 | 0 | 43 | 43 | 0 | 2,084 | +| GJ1075c | 0 | 0 | 31 | 30 | 1 | 2,813 | +| GJ107Ae | 0 | 0 | 56 | 55 | 1 | 3,662 | +| GJ111d | 0 | 0 | 70 | 67 | 3 | 6,047 | +| GJ1111b | 0 | 0 | 145 | 136 | 9 | 23,664 | +| GJ1116Ac | 0 | 0 | 141 | 135 | 6 | 15,873 | +| GJ1116Bc | 0 | 0 | 45 | 43 | 2 | 3,404 | +| GJ1125c | 0 | 0 | 62 | 60 | 2 | 5,154 | +| GJ1156d | 0 | 0 | 42 | 39 | 3 | 4,810 | + +(Sorted by cliff_edge count descending, so this is the FULL ranking's top — +every one of the other 252 bodies not shown also reports exactly 0.) + +### Verdict on "carving is structurally rare" — upgraded, not merely confirmed + +**Holds population-wide, and more strongly than originally stated.** The +correct framing going forward is not "rare" but **"not observed in the real +committed body population at the real production working-grid resolution, +for a structurally-argued reason that predicts the same result on +unobserved bodies too."** The structural argument (priority-flood's +true-minimum-rim property makes single-sealed-basin carving mathematically +impossible; genuine carving needs a rare two-independently-sealed-basins- +plus-single-cell-corridor geometry) was always the load-bearing part of the +original finding, not the one-body sample — this survey confirms the +structural argument's prediction holds at 267× the sample size, with zero +counterexamples. + +**What this changes for the cliff Phase-4 wire-cost argument:** strengthens +it. The original ruling's wire-cost case ("a rarely-nonzero field costs +almost nothing under PNG/sparse encoding") is now backed by "zero-populated +across the entire real population," not "zero-populated in one spot-check +plus a structural argument." The `cliffs: Vec` sparse field +(Araminta's round-1 shape, adopted in round 2) will ship empty for every +currently-committed body — which is exactly the cheap case the wire-cost +argument always assumed, now measured rather than inferred. + +**What this does NOT change:** the field still needs to exist and be +correctly wired, because "never observed yet" is not "cannot occur" — the +mechanism's own unit tests (hand-constructed fixtures, +`cheapest_overflow_path_finds_the_true_minimum_crossing` etc., unchanged by +this survey) already prove the carving arithmetic is correct in isolation, +and a future body (procedurally generated, not yet authored, or an existing +body re-baked with different parameters) could still produce the narrow +geometry that triggers it. The Phase-4-not-Phase-5 scope ruling (Tyre's +round-1 call) is, if anything, easier to defend now: carrying a field that +costs nothing on 267/267 real test cases and is verified-correct by +construction is a strictly better position than carrying a field justified +mostly by argument. + +**One honest limitation of this survey, named for the same reason the +original measurement named its own:** this is still the CURRENT real body +population (267 authored bodies) at the CURRENT heightmap generation +parameters, not a claim about every possible future body a procedural +generator might produce. If world generation later produces bodies with +qualitatively different terrain character (the original measurement's own +example: "heavily tectonic, high-relief, many small nested basins"), this +survey doesn't cover that population — it covers the one that exists today, +completely (267/267, not a sample), which is a categorically stronger claim +than the original one-body spot-check but is still bounded by "bodies that +exist in this repo as of 2026-07-23." + +### Repro + +```bash +cd server +cargo test --release --test hydrology_equilibrium_bench bench_population_survey_all_committed_bodies -- --ignored --nocapture +``` + +Bench code: `server/tests/hydrology_equilibrium_bench.rs` +(`bench_population_survey_all_committed_bodies`, added alongside the +existing T-1177 benches in the same file). No source file under `server/src/` +was modified — same scope discipline as the original measurement pass. diff --git a/docs/workshops/body-map-viewer/measurements/t1178-t1154-derive-bench.md b/docs/workshops/body-map-viewer/measurements/t1178-t1154-derive-bench.md index fca66bea6..17a9f72c9 100644 --- a/docs/workshops/body-map-viewer/measurements/t1178-t1154-derive-bench.md +++ b/docs/workshops/body-map-viewer/measurements/t1178-t1154-derive-bench.md @@ -504,6 +504,132 @@ planning for these rungs should size off the flat ~1.8 µs/cell single-thread --- +## INTERVIEW-2 ADDENDUM (2026-07-23): chunk (64 m) — the new deepest rung + +Jeroen's interview-2 ruling replaced the tile/voxel deepest rung with chunk +(64 m, D-243's "stream/derive unit") — *"the actual tile level rung seems +unusable. maybe replace with 64?"*, judging 10 px/tile-scale content +(~192×108 m full-screen) as in-world viewport territory (Phase 5), not Atlas +map content. Chunk had never been benched (the original T-1154 pass tested +Block 128 m and Tile-adjacent 1 m/4 m; chunk sits between them and was +skipped). New benches, same file (`server/tests/bmv_gridunit_bench.rs`), +same discipline as the rest of this document. + +### Chunk per-cell rate, 4,096-cell sweep + +| Spacing | Cutoff | Total (4,096 cells) | ns/cell | +|---|---|---:|---:| +| Chunk (64 m) | 64 m (Nyquist) | 7.53 ms | 1,838.1 | +| Chunk (64 m) | 0 (uncut) | 7.44–7.57 ms | 1,816.0–1,847.9 | +| Block (128 m) [reference] | 128 m | 7.49–7.61 ms | 1,827.8–1,856.8 | + +**MEASURED, run twice for stability, both passes agree within ~1%.** +Confirms directly (not inferred from Block) the same "no truncation left" +finding this document already established: cutoff=64m vs. uncut is within +noise, because `VOXEL_OCTAVE_WAVELENGTHS_M`'s finest entry (128 m) is +already coarser than 64 m, so nothing is skipped at either cutoff value. +**Chunk pays the identical flat ~1.8 µs/cell single-thread rate the whole +Block-through-chunk band shares** — same conclusion as this document's +existing "cross-cutting note," now confirmed at the actual new deepest rung +rather than assumed to transfer from Block. + +### Chunk realistic deep-step canvas (replaces the retired 216×384 m tile bench) + +Full 3840×2160 canvas (8,294,400 cells) at 1 gridunit-per-screen-px — the new +bottom-out rule (see the interview-2 response doc for the full derivation): +chunk does NOT need the old tile rung's 10×-magnification-margin display +convention, so this uses the SAME fixed-canvas-budget convention every +other rung in this document uses, no special case. + +| Path | Wall time | ns/cell | Speedup | +|---|---:|---:|---:| +| PARALLEL (16 threads, row-chunked) | 1,724.48–1,732.84 ms | 207.9–208.9 | — | +| SINGLE-THREAD | 15,051.59–15,145.11 ms | 1,814.7–1,825.9 | — | +| — | — | — | 8.73–8.74× | + +**MEASURED, run twice for stability, both passes agree within 0.5%.** This +is NOT the 17 ms the old tile-rung deep-step bench reported for its +216×384 m window — chunk's canvas covers 245.8 km × 138.2 km (two orders of +magnitude more ground) at the same 8.3M cell count, so the derive cost is +correspondingly larger in absolute terms. **1.72–1.73 s parallel is still +comfortably affordable** — the same band this document's District/Quarter/ +Block full-canvas numbers already occupy (1,731–1,827 ms at 8.3M cells). +Cost does not gate chunk as the deepest rung. + +### Go/no-go: Chunk (64 m spacing) — GO, replacing the retired Tile verdict + +**GO for interactive per-step serving, cost-wise, with no viewport-window +qualifier needed.** Unlike the old Tile verdict (which required a +viewport-sized-canvas carve-out to stay inside D-226(d), since a canonical +whole-body 1 m canvas would have been a governance violation), chunk's +full-canvas cost (1.7 s at 8.3M cells, same fixed-canvas-budget convention +as every shallower rung) requires no special case at all — it is priced and +served exactly like Region/District/Quarter/Block. This is a genuine +simplification the interview-2 ruling produced, not a workaround: chunk +doesn't have the same-order-of-magnitude legibility problem a 1 m ground +tile had at screen resolution, so it never needed the 10× margin (or the +canvas-sizing exception that margin forced) in the first place. + +--- + +## S2 ADDENDUM (2026-07-23): deep-step × high-river-density courses cost + +Ruled before filing: the last zero-data-point cell on the courses-cost +axis. Every prior courses-inclusive measurement in this document (Cross-check +1, 18 courses/331,776 cells) is at District spacing; nothing measured +courses-on cost at Chunk or Block. New bench: +`bench_s2_courses_density_at_chunk_and_block`, using the real GJ1c river +network (densest real confluence region — the river cell with the most +other river cells within an 8 px search radius), built via the actual PUBLIC +invention pipeline (`river_course::build_edges` + `river_course::invent_course`, +both `pub`; `layer_proxy::invent_courses_near_window` itself is private to +that module, so this bench replicates its per-edge invention loop using the +same public primitives — the same replica-loop discipline this document's +`rect_window_replica` already established). + +### Results — MEASURED, run twice for stability + +| Rung | Window | Courses in window | Avg points/course | Courses OFF | Courses ON | Delta | +|---|---|---:|---:|---:|---:|---:| +| Chunk (64 m) | 64×64 cells (4,096 m × 4,096 m) | 2 | 1,732.0 | 1,861.9–1,913.3 ns/cell | 3,489.7–3,533.7 ns/cell | **+84.7% to +87.4%** | +| Block (128 m) | 64×64 cells (8,192 m × 8,192 m) | 2 | 867.0 | 1,849.4–1,855.7 ns/cell | 2,544.1–2,816.8 ns/cell | **+37.6% to +51.8%** | + +Both runs agree within run-to-run noise typical of this document's other +4,096-cell sweeps; the direction and rough magnitude are stable across both +passes. + +**This is a real, structural finding — meaningfully larger than District's +<5% courses-cost figure (H2/H3 above), not the same order of magnitude.** +Traced to a concrete mechanism, not left as an unexplained number: +`near_perennial_water`'s cost is `O(courses × points-per-course)`, and +`invent_course` resamples each course's control polyline at +`station_spacing_m` — the SAME spacing value as the rung's own cutoff. A +course spanning a fixed chord length gets proportionally MORE points the +finer the rung's station spacing is: this bench's own instrumentation +confirms 1,732 points/course at Chunk (64 m stations) vs. 867 at Block +(128 m stations) — almost exactly the 2× ratio matching the 2× spacing +ratio. District's courses (2,048 m stations) have roughly 32× fewer points +per course for the same chord length than Chunk's, which is exactly why +District's courses-cost figure was small and nobody had measured this +effect before — no courses-inclusive bench existed below District until +this pass. + +**Still affordable in absolute terms.** Even the worst case (+87% on a +~1.9 µs/cell baseline) lands at ~3.5 µs/cell — a full 8.3M-cell chunk- +spacing canvas at that rate would be ~3.3 s parallel (at the same ~8.7× +speedup this document's other benches measure), larger than the courses-off +1.7 s figure but not a "computer catches fire" case. **The real implication +is implementation-side:** `near_perennial_water` resampling courses at the +SAME spacing as the rung it serves is a real cost driver that scales +inversely with rung spacing — a station-spacing cap independent of rung +spacing (courses don't need MORE points just because the rung asking for +them is finer, if the goal is "is this cell near a river" rather than +"render the river at full rung resolution") is a plausible optimization +worth flagging for the implementation ticket. This is a finding, not a +ruling — a river-rendering design call, not a cost-measurement one. + +--- + ## Notes on scope and what this document does not claim - This document does not decide the wire carrier, the windowed-family diff --git a/docs/workshops/body-map-viewer/round-1-notes.md b/docs/workshops/body-map-viewer/round-1-notes.md new file mode 100644 index 000000000..7b27f4536 --- /dev/null +++ b/docs/workshops/body-map-viewer/round-1-notes.md @@ -0,0 +1,314 @@ +--- +title: "Round 1 Notes — Body Map Viewer Workshop" +description: "Compiled record of Round 1 positions: per-question summary, cross-agent agreements, conflicts/tensions, the open-for-synthesis decision list, and scope-drift check against settled premises." +type: workshop +status: active +workshop: body-map-viewer +agent: qatux +round: 1 +created: 2026-07-25 +--- + +# Body Map Viewer — Round 1 Notes + +**Compiled by:** Qatux +**Round:** 1 — Positions against the measured appendix +**Participants:** Dudley, Araminta, Stig, Tyre (Troblum is round 2 only) +**Sources:** [dudley-round1.md](dudley-round1.md), [araminta-round1.md](araminta-round1.md), +[stig-round1.md](stig-round1.md), [tyre-round1.md](tyre-round1.md) + +All four positions are argued from the measured appendix (①–⑤, +[the brief's appendix](body-map-viewer-workshop-brief.md#pre-workshop-measurement-appendix)) +with no extrapolation beyond what was measured — each agent says so explicitly and +the citations check out against the four measurement docs. + +--- + +## 1. Per-question position summary + +### Dudley — server derivation (hydrology, step-canvas generation, chunking, cache tiers) + +1. **Hydrology algorithm:** adopt T-1177's priority-flood + Dijkstra prototype as-is + (production-shaped, not throwaway). Cost: ~24 ms/body single-threaded at 512×256; + all 273 bodies in parallel ≈ 0.7–0.8 s; solved once per body (like `drainage::analyze` + today), held in a D-203-shaped cache, never re-solved per step/request. +2. **Cliff representation:** `elevation` (unchanged) + `channel_depth: u16` + + `cliff_edge: bool` — direct, non-lossy carry of solver output, no min/max synthesis. + Gorge carving measured **structurally rare** (zero carved cells across all three + production-scale benches) — weakens, doesn't eliminate, the wire-cost argument. + Explicitly defers the Phase-4-vs-Phase-5 scope call to Tyre/Araminta. +3. **Canonical-vs-viewport (red flag 3):** **viewport-sized canvases at every step, + no canonical fixed canvas past the orbital/region rung** — argued as a governance + necessity (D-226(d)), not merely a cache-cost preference. Fixed pixel budget per + canvas (e.g. 3840×2160), not literal client viewport echo. +4. **Compute-chunk partitioning:** the existing row-chunked `into_par_iter()` loop in + `build_district_window_layer` holds flat (190–220 ns/cell) at every measured size + and spacing — "the single most load-bearing measured result across all four gates." + Flags that courses are excluded from the replica-loop numbers (small, bounded tax, + not a different order of magnitude) and that chunking-as-seed-input-for-next-tier + is unmeasured, an open data-flow question for round 2/implementation. +5. **Cache tiers:** global/orbital tier only, "always keep" ≈ ~174 MB PNG-encoded + across all 273 bodies, as a D-203-shaped resource extension (not SQLite — cost + doesn't force it). Everything finer: `TTL = BASE_TTL[rung] × time_decay × + distance_decay`, no disk tier below orbital. + +### Araminta — named-feature encoding, per-gridunit payload, encoding continuity + +1. **Named-feature encoding is three decisions, not one**, per the existing carrier + three-way rule: settlements/POIs → ids-with-lookup against the already-built + `atlas_city_names` pool (D-223) — presence + id per gridunit, never inline name. + Rivers → geometry already settled (T-1170 Ruling 1c `courses`); adds a new + whole-body `river_names: BTreeMap` lookup, many-edges-to-one-name. + Roads: out of scope but the pattern generalizes for free when they land. +2. **Payload schema:** the existing six dense fields stay as-is (no new classification + vocabulary); one new dense field `settlement_id: Vec`; the cliff fields are + **sparse, not dense** — a `cliffs: Vec` list parallel to `courses`, + not two more mostly-zero dense arrays (this differs from Dudley's dense-field framing + — see Conflicts below). States a hard client rule: color/style/tween presentation + only, never invent geometry/flooding/cliffs — missing data is a schema gap, not a + license to interpolate. +3. **Encoding continuity:** one colorizer family confirmed across every step (same + field set, same enum vocabularies, only sampling density changes). Average-back is + safe for continuous fields (`elev_q`/`temp_dc`/`moisture_q`) but **meaningless for + categorical fields** (`morphology`/`vegetation`/`glaciation`) — coarser steps must + re-derive via dominant-mode/plurality pick, never arithmetic mean. "Clearings/ponds + at low zoom" is scoped as sub-zone octave detail within an existing classification, + not new vocabulary — no new wire field needed. +4. **Wire-contract summary for synthesis:** confirms PNG-per-field for all dense arrays, + MessagePack-native for sparse lists; confirms the payload is categorically over the + 30 KB cap at every size regardless of her additions — doesn't change the + tagged-envelope verdict. + +### Stig — client component, step-cross UX, overlay compositing, cache store + +1. **Client structure:** kill `_canvas.scale` entirely — two sibling layers replace it: + RTT terrain/classification layer (texel-exact `ImageTexture`, generalizing the + existing `_build_tile_texture`/`_rebuild_texture_if_needed` mosaic path from + per-tile to universal) + an unscaled screen-space annotation layer (world→screen + transform per frame, literal px sizes, deletes `_zs()`/`_zs_stroke()`/ + `_zs_ring_radius()` by construction). T-1158 (viewer decomposition) is **subsumed** + — recommends cancellation with a supersession note, not separate scheduling. +2. **Step-cross UX:** hold-fetch-swap baseline (cursor-anchored), cache-hit swaps + immediately, cache-miss holds the current texture magnified during fetch (bounded + by measurement ①/④: uncached 330K canvas ≈ 75 ms derive + 5 ms encode). Morph/tween + is real but **strictly cosmetic and sequenced after** the baseline ships, as an + optional toggle. Upload cost (⑤) confirmed non-gating; prefers `texture.update()` + reuse over fresh allocation (avoids Texture object churn, despite being marginally + slower in raw ms) and L8 wherever a plane is single-channel. +3. **Overlay compositing (c1):** **ship CPU `set_pixel` coloring first**, not shaders + — today's code already does this and works; nothing in ①–⑤ prices CPU coloring cost + at step-canvas scale (330K–8.3M cells), which Stig names explicitly as **the missing + sixth measurement** (`Image.set_pixel` cost at those sizes). Shader migration is the + natural landing spot for T-1175's tapering work later, not a precondition for the + ladder itself. +4. **Client cache store:** **(iii) plain `FileAccess` cache dir + index** — rejects both + (i) server-side SQLite (relocates a client decision onto the server's process + boundary, dilutes the "server owns the DB" mental model) and (ii) godot-sqlite (a + relational addon for a point-lookup-with-expiry access pattern that doesn't need + one). Two-tier eviction: geometry entries get LRU + a retention floor at step-0 + (the concrete "always keep global" mechanism, disk-budget-safe at ~440 MB where RAM + would not be); sim-state-tagged entries get an explicit TTL sweep (the literal + self-cleaning behavior Jeroen asked for). In-memory `atlas_window_tile_set.gd`-style + LRU stays on top as the hot tier. + +### Tyre — governance ratification, gridunit↔D-243, determinism boundary, cliff ruling + +1. **Governance delta ratified as concrete amendment texts** (five, drafted and ready + to file): D-166 corollary repoint (per-step derivation floor + display ratio, + between-step magnification named as a bounded exception not a silent violation); + T-1143 ruling 3 superseded for zoom transport only (cursor-centering/edge-scroll/ + full-reset survive); `select_rung`/coverage-walk replaced by the step index; + **tagged-envelope migration ruled triggered, not merely likely** (21×–563× over + the ceiling, "off by two to three orders of magnitude before encoding is even + considered"); T-1170 carrier rule survives with a terminology gloss only + ("windowed payload" → "tagged step-canvas envelope"). +2. **Gridunit↔D-243 — the load-bearing call: gridunit SNAPS to D-243 rungs, does not + float.** Argued from determinism (`derive_at_metres` on viewport-relative spacing + would contaminate the cache key with a presentation parameter, breaking premise 9's + "canvas for a fixed seed never changes") and from cost (measurement ③ shows no + derivation-cost reason to invent a seventh spacing value — the six D-243 rungs + already cover every depth cheaply). What floats instead: the **display ratio** + (px-per-gridunit, 1×1 to 5×5), a pure client-side presentation parameter, decoupled + from spacing. Files the D-243 gridunit vocabulary entry as "a role, not a new rung." +3. **Determinism boundary:** generalizes the T-1170 course-invention ruling into a + three-bucket test — legal client interpolation is interpolation over a **closed, + server-supplied input set** (two arrived textures, wire-carried control points); + illegal is inventing any sample outside that set or smoothing across a step/ + truncation/cliff boundary. Three concrete legal cases named (step-cross morph, + river/road curve-fitting through wire-carried stations, texture-to-viewport + scaling), two illegal (upsampling terrain detail, blending across a step boundary + as if continuous), one gray case flagged for Araminta (label de-dup at coarse-step + seams — legal only if identity, not proximity, drives it). +4. **Cliff ruling:** adopts Dudley's dominant-height + `channel_depth` + `cliff_edge` + representation outright (settled by what the solver emits, zero re-derivation). + **Rules Phase-4 Atlas scope, not deferred to Phase-5** — argues deferral would + silently contradict "settled hydrology" itself (a map showing a smooth shoreline + where the solver computed a carved channel misrepresents the settled state), and + that the rarity finding makes inclusion nearly free rather than making exclusion + safe. Flags his own caveat: rarity is measured on one real body (GJ1c) + two + synthetics, not surveyed across ~273 bodies — names this explicitly as something + for Troblum's round-2 adversarial pass to stress. + +--- + +## 2. Cross-agent agreements (settled going into round 2 — do not re-litigate) + +- **Dudley, Araminta, Tyre** all treat the **tagged-envelope migration as settled by + the numbers**, not a round-2 open question. Each cites T-1179 independently and + lands on the same order-of-magnitude reading (21×–563× over the 30 KB cap). Tyre's + position sharpens the brief's "likely triggers" framing to "triggered, unambiguous." + Round 2 should treat *whether* as closed and spend synthesis time only on the + *shape* of the tagged envelope (which is genuinely open — see §4). +- **Dudley and Tyre** independently land on **viewport-sized canvases, no canonical + fixed canvas past the orbital/region rung** — Dudley from cost-shape-plus-D-226(d) + necessity, Tyre from the same D-226(d) argument plus the determinism/cache-key + argument in his gridunit↔D-243 ruling. These are two independently-argued paths to + the same conclusion, not one agent citing the other — worth naming as convergent, + not coincidental. +- **Dudley, Araminta, and Tyre all adopt the T-1177 cliff representation + (`elevation` + `channel_depth` + `cliff_edge`) without dispute.** The only open + edge is dense-vs-sparse wire shape (§3) and Phase scope (§3), not the field set + itself. +- **Araminta and Tyre agree explicitly** that the client may never invent + geometry/flooding/cliff state — Araminta states it as a payload-schema hard rule, + Tyre generalizes it into the three-bucket determinism-boundary test. Same + conclusion, complementary framings (schema discipline vs. interpolation discipline). +- **Stig and Dudley/Araminta agree** that morph/tween between steps is real, + cosmetic-only, and non-blocking for the baseline — Stig sequences it as a + follow-up after hold-fetch-swap ships; Tyre's bucket-A ruling gives it the + determinism green light; nobody argues for shipping it in the first cut. +- **All four agents report their four gated measurements (①–④) as clean GO/VIABLE + results with no extrapolation** — no agent treats any of the pre-workshop numbers + as contested or requiring a re-run. + +--- + +## 3. Conflicts and tensions (named, not yet resolved) + +- **Dudley vs. Araminta — dense vs. sparse wire shape for the cliff fields.** Dudley's + round-1 text frames `channel_depth`/`cliff_edge` as "the wire field" without + specifying dense-array vs. sparse-list shape, and his cost argument ("cheap under + any of the encodings in ④'s table... PNG's DEFLATE in particular loves a field + that's constant almost everywhere") reads as implicitly assuming dense per-gridunit + arrays alongside the other six. Araminta explicitly rejects that shape: "as dense + per-gridunit arrays would be wasteful given near-zero occupancy" and proposes a + sparse `cliffs: Vec` list parallel to `courses` instead. Tyre's + synthesis leans toward Araminta's read ("I'd steer toward 'new arrays' over 'steal + a bit'... a sparse-friendly encoding... helps this field cost less than its raw + byte width suggests" — but this sentence is ambiguous on dense-vs-sparse, using + "new arrays" language while endorsing a sparse-friendly encoding argument). + **This needs an explicit round-2 resolution, not an inferred one** — the three + positions are close but not identical, and "new arrays" (Tyre) vs. "sparse feature + list, not arrays" (Araminta) is a real implementation-shape fork even if the wire + bytes converge. +- **Stig vs. the brief's implicit assumption — shader-vs-CPU (c1) is not close to + resolved, and Stig's position is a deferral, not an answer.** The brief posed c1 as + "genuinely open for the workshop"; Stig's position doesn't rule for CPU on the + merits so much as rule for CPU *by default* pending a measurement that doesn't + exist yet. This isn't a disagreement with another agent (no one else took a + position on c1), but it is a gap between what the brief expected round 1 to + produce (a position) and what it got (a sequencing recommendation plus a flagged + missing measurement) — surfaced here so the lead interview doesn't mistake "ship + CPU first" for "shader question closed." +- **Scope-boundary tension, not a disagreement: Dudley defers the Phase-4-vs-Phase-5 + cliff-scope call to "Tyre/Araminta," and Tyre rules on it unilaterally** ("Phase-4 + Atlas scope, not deferred to Phase-5") in the same round without an explicit + Araminta sign-off in her own document (her round-1 text adopts the field shape but + does not independently address the Phase-4-vs-Phase-5 question at all). Tyre's + ruling may be correct and well-argued, but strictly by the documents, it is a + two-of-three-invited-parties call, not a three-way confirmed one — worth a direct + Araminta confirm-or-object in round 2 rather than treating it as fully closed. + +--- + +## 4. OPEN-FOR-SYNTHESIS decision list (for the lead interview) + +Carried into Jeroen's Round 1 lead interview and/or Round 2 synthesis, in the order +the brief's Expected Outputs raises them: + +1. **Step count and step factor** (largest → visible tile) — every agent treats this + as still-open and downstream of their own numbers; Dudley's table gives per-rung + costs but explicitly does not propose a step count. This is the brief's own + Jeroen-interview question 2 and remains unanswered by round 1. +2. **Map time axis** (climatology vs. current sim state for frozen/flooded) — not + addressed head-on by any of the four round-1 documents beyond Araminta's schema + table flagging it as "where the outline's sea/flooded distinction needs a decision + the workshop, not me, should rule on." Still fully open; this is Jeroen-interview + question 1. +3. **Tagged-envelope migration shape** (not the yes/no, which is agreed — see §2 — + but the concrete framing: new IPC message type, demux mechanics, how the legacy + `district_window` carrier coexists) — Tyre's amendment text names the destination + but defers the mechanical design; this is round-2 synthesis work. +4. **Cliff field wire shape: dense arrays vs. sparse feature list** — the Dudley/ + Araminta/Tyre tension in §3, needs an explicit round-2 call, not an inferred one. +5. **Cliff field Phase-4 vs. Phase-5 scope** — Tyre ruled Phase-4; Araminta has not + independently confirmed; the §3 two-of-three tension needs closing explicitly. +6. **Shader-vs-CPU styling (c1)** — Stig's position is "CPU first, sequence shader + later," not a resolution of the open question the brief posed. **Depends on the + missing sixth measurement (below) before it can be treated as settled even + provisionally.** +7. **NEW: the missing sixth measurement — `Image.set_pixel` cost at 330K/2.07M/8.3M + cells (GDScript, client-side).** Stig names this explicitly as a gap: nothing in + measurements ①–⑤ prices CPU per-cell coloring cost at step-canvas scale, and the + c1 shader-vs-CPU call cannot be made "with the same rigor as everything else in + this brief" without it. This should be added to the measurement appendix (as ⑥) + or explicitly scoped as a round-2/pre-implementation follow-up — flagging per the + coordinator's instruction that this lands in the decision list, not just Stig's + own document. +8. **Chunking-as-seed-input-for-next-tier data flow** — Dudley flags this as + unmeasured and architecturally undecided (does a finer step re-derive independently + from `(seed, position)`, or literally consume the coarser step's output values?). + His own cost measurements assume independent re-derivation; round 2 needs to either + confirm that assumption or scope a new cost pass if the alternative is preferred. +9. **Region-as-step-0 confirmation** — Tyre's §1e amendment note flags this needs + confirming with Araminta in round 2 synthesis ("Region likely IS the global/step-0 + rung, worth confirming"). +10. **The gray determinism case — label placement de-duplication at step seams** — + Tyre flags this explicitly as unresolved and hands it to Araminta's named-feature + call; not yet addressed in her document. + +--- + +## 5. Scope-drift check against settled premises + +No round-1 document relitigates a settled premise (1–9) or a "settled by round A" +clarification-round item. Specific checks: + +- **Stepped zoom, vocabulary (tile vs. gridunit), data-resolution tunable, hydrology + as deterministic equilibrium** — all four agents use the settled vocabulary and + model correctly throughout; no drift back to continuous zoom or "tile" misuse + detected in any of the four documents. +- **Server-determines-content / client-draws-art (premise 2)** — held firmly by all + four; Araminta's explicit client-rule statement and Tyre's determinism-boundary + bucket test both *reinforce* this premise rather than erode it. No agent proposes + client-side derivation of any invented-detail category. +- **CPU/Rust-server-side-only derivation, GPU presentation-only (premise 2, the c2 + pre-empt)** — untouched; Stig's shader discussion (c1) is explicitly about + *styling/coloring already-derived data*, not derivation, and he states this + distinction himself. No drift toward GPU-side derivation anywhere in round 1. +- **One caution, not a violation:** Stig's structural recommendation to cancel + T-1158 outright (not just note supersession) is a **ticket-plan action**, one + category beyond a "position" — it's appropriate content for round 1 (the brief's + Expected Output 2 explicitly wants conflicting tickets named), but it is a + concrete cancellation recommendation rather than a design position, worth the lead + noting as an actionable item distinct from the architecture questions proper. +- **No Phase-5/Phase-6 drag detected.** Stig explicitly declines to scope Phase-5 + reuse speculatively ("I don't want to speculatively design against now — that + would be exactly the kind of later-phase drag the cascade discipline warns + against") — this is the correct cascade-discipline call, named here because it's + a positive scope-discipline example, not a violation. + +--- + +## Summary for the lead interview + +Round 1 produced four internally consistent, cross-citing positions with **no cost +surprises** — every measured gate came back GO, and three of the four agents +converge on viewport-sized canvases and the tagged-envelope migration independently. +The genuinely open items are load-bearing but narrow: step count itself, the map +time axis, the cliff field's wire shape (dense vs. sparse) and phase scope, the +shader-vs-CPU call (blocked on a **new, named missing measurement** — +`Image.set_pixel` cost at step-canvas scale), and two smaller synthesis-confirmation +items (chunking/seed-input data flow, Region-as-step-0). Nothing here requires +relitigating a settled premise, and no scope drift toward later cascade phases was +observed. diff --git a/docs/workshops/body-map-viewer/round-2-notes.md b/docs/workshops/body-map-viewer/round-2-notes.md new file mode 100644 index 000000000..498f92f58 --- /dev/null +++ b/docs/workshops/body-map-viewer/round-2-notes.md @@ -0,0 +1,435 @@ +--- +title: "Round 2 Notes — Body Map Viewer Workshop" +description: "Compiled record of Round 2 synthesis positions: convergences, verified-absent tensions, and the exact interview-2 decision list against lead-interview-1's rulings." +type: workshop +status: active +workshop: body-map-viewer +agent: qatux +round: 2 +created: 2026-07-25 +--- + +# Body Map Viewer — Round 2 Notes + +**Compiled by:** Qatux +**Round:** 2 — Synthesis (envelope mechanics, seed-chaining, step-ladder tables, +final cache spec, TTL-split payload, adversarial pass) +**Participants:** Dudley, Araminta, Stig, Tyre, Troblum (adversarial) +**Sources:** [dudley-round2.md](dudley-round2.md), [araminta-round2.md](araminta-round2.md), +[stig-round2.md](stig-round2.md), [tyre-round2.md](tyre-round2.md), +[troblum-round2.md](troblum-round2.md) (920 lines, incl. ADDENDUM + FINAL UPDATE +scorecard), [lead-interview-1.md](lead-interview-1.md), +[measurements/t-setpixel-c1.md](measurements/t-setpixel-c1.md) (⑥), +[measurements/t1177-hydrology.md](measurements/t1177-hydrology.md) (POPULATION +SURVEY addendum, 2026-07-23) + +This round produced a filing-ready package: **1 new D-record + 11 amendment +texts + 8 ticket dispositions** (Tyre §a/§d), all four round-1 open items closed +(envelope mechanics, seed-chaining, step-ladder tables, cache tiers), a +population-scale re-verification of the cliff rarity claim, and a full +adversarial pass that resolved 5 of 7 original findings in-round. Troblum's own +FINAL UPDATE scorecard is the authoritative statement of what remains open — +this document verifies it against the four synthesis documents rather than +restating it blind. + +--- + +## 1. Per-agent positions, round 2 + +### Dudley — envelope wire/serving design, seed-chaining ruling, step-ladder cost fills, final cache spec + +- **(a) Envelope mechanics:** read `server/src/bridge/mod.rs` directly and found + the "tagged marker" pattern D-225 asked for **already implemented five times** + (`star_map`/`city_names`/`browse` each carry a mandatory boolean discriminator, + `ShapeProbe` enforces mutual exclusivity). Recommends extending the exact + pattern: `StepCanvasRequest { step_canvas: bool, body_id, step_index, center, + extent, min_wl_m }` as a sixth `Inbound` variant; response is a **dedicated** + `StepCanvasResponse` message (not a field on `AtlasLayerResponse` — D-226 + T-1124 §2's own rule names this exact case). Legacy `district_window` survives + byte-unchanged with no coexistence engineering — it just goes cold once its + only client (the retiring `AtlasWindowViewer`) is deleted. Step-0/Region rides + the *new* envelope, not the legacy whole-body family. Answers Araminta's + relayed dense/sparse framing question: **one flat tagged message** (her + default shape), not a split — no server-side compute is saved by splitting, + and the progressive-paint UX case is cheaper to solve client-side. +- **Cliff wire-shape self-correction:** explicitly adopts Araminta's sparse + `Vec` shape over his own round-1 implicit dense-array framing, + stating plainly he was "underspecified, not actually disagreeing" — resolves + round-1 conflict #1. +- **(b) Seed-chaining ruling:** **independent re-derivation** — each step calls + `derive_at_metres(seed, position, ...)` fresh; a finer step may call a coarser + rung's derivation *function* internally (the existing region-baseline-feeds- + district precedent) but never reads a coarser step's cached *response* as + literal input. Argued from D-227 purity (cache-dependency chains and + determinism-ordering hazards are exactly what independent re-derivation + avoids) and states this validates his own round-1 cost numbers as the correct + model. +- **(c) Step-ladder tables:** live-coordinated with Tyre mid-draft; corrected his + own first-pass error (an invented, non-D-243 "4 m Tile-adjacent" spacing). + Cost-filled three D-243-disciplined skeletons (Options A/B/C). **Recommends + Option B** (skip-chunk, 5 steps) as the only skeleton where every row is + directly measured or same-band-confirmed — chunk (64 m) was never benched, and + B is the one option that doesn't need it. States plainly that cost does not + distinguish B from C (identical per-row costs); the choice is pure UX pacing, + which he explicitly hands to Jeroen. +- **(d) Final cache spec:** Jeroen's storage-eviction amendment **simplifies** + his round-1 three-term TTL formula, not complicates it — drops + `distance_decay` entirely once staleness and storage-thrift stop being + conflated, landing on one axis: `evict_if: time_since_last_visit(entry) > + STORAGE_TTL[rung]`. Global tier (~174 MB PNG-encoded, D-203-shaped resource, + keep-always) unchanged from round 1. Notes his own design and Stig's + independently-designed client tier converged on the identical two-axis shape. + +### Araminta — final payload schema (TTL-split), region-as-step-0, label de-dup, final wire schema + +- **(a) TTL-split payload:** the field *set* doesn't change (ratified by + lead-interview-1); this section assigns each field to a **static-geometry + plane** (cached indefinitely-fresh) or a **sim-state plane** (short-TTL, + re-requested on clock-bucket rollover) as a **physical field separation**, not + a policy layered on afterward. States the reasoning per field, not inferred + from name: `glaciation`/new `flooded` → sim-state (their *live* value is the + map-relevant fact); `temp_dc` → static (the map consumes it as a + climate-baseline classification, not a live weather reading — same D-226 + clock mechanism, different consumer question). Rules against bit-packing the + two sim-state fields, citing T-1179's own finding that packing hurts DEFLATE + on low-entropy fields — separate L8 planes win on both compression and + independent-re-fetchability grounds. +- **(b) Region-as-step-0: CLOSED three-way** (Araminta + Tyre + Jeroen's + ruling) — states two clauses together per Tyre's wording nuance: Region = step + 0, **and** step 0 is the sole canonical/always-keep tier; nothing coarser than + Region is a rung (the planetary seam is elastic, not steppable). Three + independent arguments given (Tyre's snap ruling forces it structurally; + continuity with the existing orbital mosaic; elimination — no coarser D-243 + rung exists). +- **(c) Label de-dup:** a deterministic, identity-keyed anchor rule — mean-cell + position (point features) or cropped arc-length midpoint (line features), + computed independently per canvas/tile, explicitly **not** a cross-tile + coordination protocol. Adds a corollary no round-1 document stated explicitly: + during hold-fetch-swap, only the currently-authoritative canvas's annotation + layer draws — the held/stale canvas's terrain texture shows (magnified) but + its labels do not. +- **(d) Final wire schema: CLOSED**, envelope framing confirmed by Dudley's + design. Flags one sequencing note (not a disagreement): Dudley's struct as + literally drafted predates her `flooded` field and needs it added as a ninth + field — doesn't reopen the flat-envelope ruling. + +### Stig — measurement ⑥, final c1 call, cache-store spec, px-per-gridunit band + +- **(a) Measurement ⑥ headline:** `Image.set_pixel` flat ~78 ns/cell across + 330K–8.3M cells (25.7 ms / 165.2 ms / 643.5 ms); a hand-rolled + `PackedByteArray` direct write is **~2× slower**, counter to his own + going-in assumption — explicitly flags this as a "don't hand-roll the buffer + write" finding for implementers. +- **(b) C1 final call: CONFIRMED CPU-first**, not a flip to shaders. States the + number "changes how I'd frame the recommendation" — colorize is trivial at + the realistic 330K per-step size (25.7 ms) but meaningful at the 8.3M + stress-ceiling size (~1/3 of server derive time there) — reads this as a + second, independent validation of Dudley's viewport-sizing policy rather than + a new risk, since the policy structurally avoids ever requesting an 8.3M-cell + canvas as a real payload. Names the shader path's trigger condition precisely + (T-1175's tapering work landing, or a future step-count decision pushing the + realistic per-step size meaningfully above ~2M cells) rather than leaving it + a vague someday. +- **(c) Cache-store spec, final:** extends round 1's two-mechanism design + (retention-floor+LRU for geometry, TTL for sim-state) to **three independent + sweep mechanisms** kept structurally separate: Tier 1 (global/step-0) — + retention floor only, no sweep ever; Tier 2 (sub-global geometry) — two + independent triggers, (2a) time-since-last-visit and (2b) LRU-capacity + budget; Tier 3 (sim-state planes) — explicit staleness TTL. Gives a concrete + `IndexEntry` schema. Explicitly defers `STORAGE_TTL`/budget constants as "a + tuning pass once real play-pattern data exists," not an architecture call. +- **(d) Px-per-gridunit band:** step-dependent, not flat — 1×1 at the deep/ + ground steps (full fidelity where the player is nearest visible detail), + 1×1 preferred at mid steps while realistic canvas count stays under ~2M + cells, ~5×5 reserved for the shallow/orbital step only. Grounded in ⑤+⑥ + together: upload cost is flat regardless of display density, and colorize + cost is cell-count-bound, not density-bound — so there's no cost reason to + sacrifice fidelity at the deep end. + +### Tyre — final amendment texts, envelope governance disposition, step-ladder tables, deprecation sweep + +- **(a) One new D-record + 11 amendment texts**, each stated as record-ready + ("paste directly into the named record"): the new render-architecture record + (a.0); D-166 corollary repoint (a.1); T-1143 ruling 3 supersession (a.2); + `select_rung` replacement (a.3); D-243 gridunit vocabulary entry, including + Stig's ⑥-derived step-dependent display-ratio band (a.4); D-226 ceiling + **re-scope**, not retirement (a.5); the map-time TTL-split + staleness/storage + axis distinction, verbatim-grounded in Jeroen's amendment (a.6); the cliff + sparse-list terminology repoint, now **survey-confirmed** by the population + addendum (a.7); the D-226(d) per-request/per-derivation framing + client-cache + accumulation cap, closing Troblum's S3 (a.8); the persistent-cache + schema/version tag, closing Troblum's S5 (a.9). +- **(b) Envelope governance disposition:** the windowed-family ceiling's + **purpose survives untouched**; its **mechanism is re-scoped to the legacy + `district_window` carrier only** — the tagged envelope is a new carrier the + old rule does not apply to by construction. States explicitly: **"the ceiling + rule is RE-SCOPED, not retired"** and that the deprecation table's + conditional SUPERSEDE branch "does NOT fire." Folds in Dudley's three wire + rulings verbatim as confirmation his own drafted text required no changes. +- **(c) Step-ladder tables:** supplies the D-243-disciplined skeleton + constraints (six rungs only, step 0 = Region, deepest = voxel, display ratio + decoupled from spacing); Dudley fills costs. **Joint recommendation: Option + B** — states this is "an evidence argument, not a taste one... Dudley and I + both land here independently." Names the one real seam explicitly: the + Region→District ÷100 jump lines up with the global/sub-global cache-tier + boundary; every factor below it is pure UX pacing with no serving-side + consequence, which he states can be decided "on feel alone." +- **(d) Deprecation sweep:** 11 DQR dispositions (including two added directly + from Troblum's findings: D-226(d) AMEND for the accumulation cap, and a D-192 + cross-reference note) + 8 ticket dispositions (T-1176 close-as-delivered, + T-1158 cancel, T-1175 re-scope-and-unblock, T-1157 re-scope, T-1174 keep + unchanged, T-1153/T-1152 stay `done` with code-retirement flagged, their test + suites retired with their code) + an explicit `_canvas.scale` retirement path + for the implementation ticket. +- **Names the exact interview-2 items himself, at the end of his document** (see + §3 below — verified against this). + +### Troblum — adversarial pass (BLOCKING/SERIOUS/NOTE, then ADDENDUM, then FINAL UPDATE) + +Three-part document, read in full. Initial pass (before any round-2 files +existed) found 2 BLOCKING (B1: the "273 bodies" hydrology bench is one body +solved 273 times, not a population survey; B2: the seed-chaining fork was +unresolved and every cost number implicitly assumed one answer to it), 5 +SERIOUS (S1 eviction-interaction costing; S2 courses-density coverage gap; S3 +D-226(d) letter-vs-purpose on client-cache accumulation; S4 sim-state phase +cadence; S5 disk-cache schema/version tag), and 3 NOTE items (N1 units +convention; N2 Region-as-step-0 unconfirmed at time of writing; N3 clean bill +on the courses cost bound itself). + +**ADDENDUM** (after reading all four round-2 files): re-verified every finding +against source code, not agent prose. B2 **RESOLVED** — checked +`district_profile.rs:1705-1732` directly and confirmed the nested-function-call +pattern Dudley's ruling describes is *already what every measured ns/cell +number exercises*, not merely argued to be consistent with it. Attacked the +envelope design directly with three angles: a discriminator-collision attack +that **did not pan out** (cleared on inspection of `bridge/mod.rs:96-152`), the +version-skew angle that found Tyre's §(a.9) had **already closed** the gap +before Troblum could report it as new, and a genuinely new cache-format +cutover angle that traced to a **clean bill** (the legacy viewer never had a +disk cache to begin with, so there's no stale-key orphaning risk). Checked +Option B's "zero asterisks" claim row-by-row and found one precision note (the +Quarter row is same-band-inferred, not this-session-remeasured at 8.3M) — +explicitly states this doesn't change the recommendation. S1 downgraded to +NOTE (mechanism fully resolved by both Dudley's and Stig's independently +converging designs; only a tuning-pass number remains, correctly deferred). +S3 and S5 confirmed RESOLVED against Tyre's exact amendment texts. + +**FINAL UPDATE** (after independently verifying the new population-survey +bench against source): confirmed the new bench (`bench_population_survey_all_ +committed_bodies`) is genuinely distinct from the original defective bench — +loads each of 267 real heightmap PNGs independently inside the `par_iter` +closure, distinct `body_id`/`sea_level` per iteration, carries its own +determinism spot-check and a discovery-count guard. **B1 → RESOLVED.** States +the final scorecard explicitly: of seven original findings, five are resolved +(B1, B2, S3, S5, and S1's mechanism half — with S1's cost-number half correctly +deferred as tuning), and **exactly two remain open for interview 2**: S2 +(courses-inclusive derive cost has zero measured coverage at the deep-step × +high-river-density combination) and the S4 residual (sim-state TTL +phase-cadence is still an unnamed numeric variable, though the field-assignment +half Araminta resolved is fully closed). + +--- + +## 2. Convergences (many this round — verified against the source documents, not merely asserted) + +- **Flat envelope, one message, not a dense/sparse split.** Araminta's round-1 + default assumption; Dudley independently designs and confirms it in his own + `StepCanvasResponse` struct with an explicit cost argument (no server compute + saved by splitting, since every field comes off the same row-chunked pass); + Tyre folds Dudley's ruling into the governance text verbatim. Three-way + agreement, not two citing one. +- **D-226 §2 ceiling re-scope, not retirement — both seats that address it say + re-scope.** Tyre's governance framing ("its purpose survives... its mechanism + is re-scoped to the legacy carrier only") and Dudley's serving-level design + (legacy `district_window` "survives unchanged," the envelope "carries only + NEW step-canvas traffic") independently land on the identical disposition. + Tyre states explicitly in his own document that his §(d) deprecation table's + conditional SUPERSEDE branch "does NOT fire" — re-scope is final, not a live + option still being weighed. +- **Option B (skip-chunk, 5-step ladder) — joint, independently-reached + recommendation.** Tyre states this plainly: "Dudley and I both land here + independently... an evidence argument, not a taste one." The shared reasoning + (chunk/64m was never benched; B is the only skeleton with zero unmeasured + rungs) appears in both documents in the same form, cost-argued rather than + preference-argued. +- **Two-axis cache eviction (staleness vs. storage-budget) reached + independently twice.** Dudley's server-side spec (§(d), the collapsed + one-term `time_since_last_visit > STORAGE_TTL[rung]` formula, replacing his + own round-1 three-term formula) and Stig's client-side spec (§(c), Tier 2's + two independent sweep triggers with no distance term at all) were designed + separately and converged on the same shape. Troblum's ADDENDUM explicitly + names this convergence as evidence the simplification is correct, "not just + convenient." +- **Step-dependent px-per-gridunit ratio, not a flat display ratio.** Stig + proposes it (§(d), grounded in ⑤+⑥ together); Tyre files it directly into + the D-243 gridunit amendment text (§(a.4)) attributed to Stig's measurement + ⑥, and Dudley's ladder-table narrative independently confirms the same + policy from a *different* cost driver (server-side colorize is cell-count- + bound, not density-bound — so relaxing to 5×5 wouldn't even help at the + steps where it doesn't need to). Dudley states this as "two independently + measured cost models agreeing on a policy neither one was designed to argue + for" — worth flagging as convergence, not restating as one position echoing + another. +- **Cliff wire shape: sparse `Vec`, resolved unanimously.** + Dudley explicitly retracts his own round-1 implicit dense-array framing in + favor of Araminta's sparse-list shape; Tyre's filed amendment text adopts the + same shape and states his own round-1 "new arrays" language was about + rejecting bit-stealing, not endorsing dense arrays — the round-1 tension + (round-1-notes.md §3) is fully closed, three ways, with each agent's own + document stating why they landed where they did rather than silently + adopting a consensus. +- **SQLite rejected in every shape, independently, by both cache-tier owners.** + Restated and reconfirmed in round 2 (Dudley's D-203-shaped resource for the + server global tier; Stig's plain `FileAccess` dir for the client) — not a new + convergence this round but confirmed unchanged from the lead-interview-1 + ratification-by-silence. + +--- + +## 3. Tensions — verified against the documents: NONE survive into interview 2 + +The coordinator's belief that no tensions remain was checked directly, not +assumed. Two items that were live tensions or open questions at the close of +round 1 are confirmed closed by round 2, each with an explicit closing +statement in the record (not merely implied by silence): + +- **Round-1 conflict #1 (Dudley vs. Araminta, dense-vs-sparse cliff wire + shape):** closed. Dudley's round-2 document states outright: "Reading her + argument again against my own hydrology numbers, she's right and I was + underspecified, not actually disagreeing." Tyre's filed text adopts the same + shape and states his own prior "new arrays" phrasing was about the bit- + stealing question, not a competing dense-array position. Three-way + resolution with each party's reasoning on record — no residual disagreement + found. +- **Round-1 conflict #2 (Tyre ruling Phase-4-not-Phase-5 for cliffs without an + independent Araminta confirmation):** closed by the population-survey + addendum plus Araminta's own round-2 document, which adopts the ratified + Phase-4 sparse-list shape without objection and builds her final TTL-split + schema directly on it (`cliffs` listed in her static-geometry plane table + with no caveat). Lead-interview-1 ruling 3 already resolved this + ("aligns the rarity finding, Araminta's encoding logic, and Tyre's scope + ruling") before round 2 began; round 2's population survey (Troblum's B1, + now RESOLVED) strengthens rather than reopens it. + +**What is NOT a tension, despite surface appearance, and should not be treated +as one at interview 2:** Troblum's two remaining open items (S2, S4-residual) +are **not disagreements between agents** — no participant contests them, no +two documents give conflicting answers, and Troblum's own FINAL UPDATE frames +both as measurement/tuning gaps rather than architectural disputes. These +belong in the decision list below as **[decide]** items (does the gap get +closed before filing or deferred), not as **tensions** to relitigate. The +coordinator's framing (list them as decisions, not conflicts) matches what the +documents actually show. + +**One asymmetry worth naming, not a tension:** Stig's round-2 §(d) phrases the +step-dependent display-ratio policy from the deep end ("1×1 at the deep/ground +steps... relaxing toward 5×5 only at the shallower steps"), while Dudley's +narrative phrases the same policy from the cost-driver end ("1×1 wherever the +resulting cell count is affordable... reserving the coarser ratio only for the +one place canvas extent is the pressure"). These are the same policy stated +in two directions, not two different policies — Tyre's filed amendment text +(§(a.4)) reconciles both into one canonical statement. Flagging only so this +isn't mistaken for a wording drift at interview 2. + +--- + +## 4. EXACT interview-2 decision list + +Cross-checked against Tyre's own explicit "For interview 2, the calls left for +Jeroen" list (`tyre-round2.md`, final section) and Troblum's FINAL UPDATE +scorecard (`troblum-round2.md`). **The coordinator's proposed list is correct +as stated, with one addition Tyre's own document makes explicit that should be +folded in rather than treated as separate — see item 3's note below.** + +1. **[ratify] Step ladder: Option B vs. Option C.** Pure pacing choice — every + costed row is byte-identical between the two skeletons (Tyre: "it costs + identically to B row-for-row... District's absence is not a cost saving, + it's purely a pacing choice"). B is the joint recommendation (5 steps, + gradual ÷100-then-÷4-then-÷4-then-÷128 descent); C is 4 steps with a single + blunt ÷400 top jump. Option A (6 steps, including the unmeasured chunk rung) + is ranked last by both agents and is not presented as a live choice for + Jeroen — it is named only to show why it was excluded. + +2. **[ratify] D-226 §2 ceiling disposition: re-scope vs. retire.** Both seats + that address this — Tyre's governance framing and Dudley's serving-level + design — say **re-scope** (the rule keeps governing the legacy + `district_window` carrier verbatim; the tagged envelope is a new carrier + outside its scope, not an exemption from it). This is presented as a + one-line ratification, not a live fork — Tyre states his own deprecation + table's conditional retire-branch "does NOT fire." + +3. **[ratify] Seed-chaining = function-composition, confirmed as Jeroen's + outline intent.** Dudley's round-2 ruling (independent re-derivation; a + finer step may call a coarser rung's derivation *function*, never read a + coarser step's cached *response*) is **source-verified** by Troblum's + ADDENDUM — not merely argued, but checked directly against + `district_profile.rs:1705-1732` and confirmed to be the exact pattern + every measured cost number in the appendix already exercises. What + remains for interview 2 is narrower than "does the ruling hold" (it + does, on the code) — it is **authorial confirmation**: does "this at the + same time serves as seed information for the deeper cascade" in Jeroen's + original outline mean this function-composition reading, or did he intend + literal consumption of a coarser step's already-computed response bytes? + Troblum frames this precisely as a one-line confirmation item, "very + likely to confirm cleanly," not a re-open — but notes if Jeroen meant the + second reading, Dudley's cost model needs a re-bench before the ladder + table is authoritative. **This is exactly the coordinator's item 3, stated + with the precision both Tyre and Troblum give it — no correction needed.** + +4. **[decide] Troblum's two residuals — close before filing, or defer.** + Confirmed as the only two items remaining open on Troblum's own final + scorecard (of seven original findings, five are resolved; these two are + not): + - **S2 — deep-step × high-river-density courses bench.** The one + courses-inclusive-at-real-density number in the whole appendix (T-1178 + Cross-check 1, 195.0 ns/cell, 18 courses, District spacing, one body) has + zero coverage at the deepest step (Block/Tile spacing, where the deep-step + 83K-cell bench is courses-EMPTY by construction) crossed with high river + density (a BraidedDelta/MeanderReach-class body, not yet sampled). + Troblum's own framing: "not blocking... but it is the one combination in + the whole measured appendix with literally zero data point." His ask is + explicit: "run one more bench... before or shortly after filing; not a + blocker for filing itself" — meaning the decision Jeroen faces is + **run-now vs. defer-to-a-named-follow-up**, not whether the finding is + valid. + - **S4 residual — sim-state TTL phase-cadence.** Araminta's round-2 work + fully resolved *which* fields are sim-state and *why* (per-field + reasoning, not name-inference); what remains is a **numeric** gap — D-228's + "computed once per phase" water-height/glaciation model never states how + long a phase lasts, so the sim-state plane's TTL has no number to size it + by. Troblum's framing: fold into the same tuning-pass deferral Stig/Dudley + already used for their own unresolved constants (acceptable), **or** if + Jeroen wants a concrete number before filing, that's a named Dudley/ + Araminta follow-up question at interview 2. Same shape as S2: a + run-now-vs-defer choice, not a contested finding. + +**Nothing else requires a Jeroen ruling at interview 2.** Every other item any +agent flagged as open at the close of round 1 (envelope mechanics, cliff wire +shape + Phase-4 scope, region-as-step-0, label de-dup, the c1 shader-vs-CPU +call, TTL-split field assignment) is closed in round 2 with an explicit +closing statement traceable to a specific document and section, verified +above. + +--- + +## Summary for the lead interview + +Round 2 delivered a complete, cross-verified filing package — not a set of +independent positions still needing reconciliation. Every one of round 1's +open items closed, most of them multiple ways (region-as-step-0 three-way; +cliff wire shape three-way with each party's reasoning stated; the envelope +ceiling disposition two ways with an explicit "does not fire" statement on +the untaken branch). Five distinct convergences were independently reached by +different agents from different starting arguments — worth Jeroen hearing as +confidence signal, not restated agreement. Troblum's adversarial pass is the +strongest evidence the package is filing-ready: of seven original findings +(two BLOCKING), five are now fully resolved and **verified against primary +source**, not merely re-asserted by the agent being checked — including a +brand-new, population-scale (267/267 real bodies) re-run of the cliff-rarity +claim that came back stronger than the original one-body finding. Exactly +four items need Jeroen's voice at interview 2: two ratifications on facts +already agreed (step count B-vs-C is pure pacing; the ceiling re-scope is +unanimous), one authorial-intent confirmation that the code already supports +(seed-chaining), and two narrow run-now-vs-defer calls on measurement/tuning +gaps that nobody disputes are real but that nobody has treated as +architecture-blocking either. diff --git a/docs/workshops/body-map-viewer/stig-round1.md b/docs/workshops/body-map-viewer/stig-round1.md new file mode 100644 index 000000000..9d72c72de --- /dev/null +++ b/docs/workshops/body-map-viewer/stig-round1.md @@ -0,0 +1,333 @@ +--- +title: "Body Map Viewer — Stig's Round 1 Position" +description: "Client map component structure, step-cross UX, overlay compositing, and the client cache store decision" +type: workshop +status: active +workshop: body-map-viewer +created: 2026-07-25 +owner: Stig +--- + +# Stig — Round 1 Position + +Answering the four questions in my slice. Grounded in the measured appendix +(①–⑤) and the current `client/ui/implant/apps/atlas/` cluster as it exists on +disk today, not as remembered from the T-1143 doc. + +## 1. The client map-drawing component + +**Kill `_canvas.scale`. There is no more zoom-scaled canvas.** Today's model — +`_canvas.position`/`_canvas.scale` on a Node2D, `_apply_transform()` setting +`_canvas.scale = Vector2(_view_zoom, _view_zoom)`, every nature-overlay +`draw_*` call wrapped in `_zs()`/`_zs_stroke()`/`_zs_ring_radius()` compensation +— is the entire error class named in the brief (§2 of prep-grounding). The +premise "server determines content, client draws map-art" doesn't just permit +retiring that model, it removes the only reason it existed: the scale node was +there to let one held composite serve a continuous zoom range. Stepped zoom +with a server-resolved canvas per step means the client never needs to render +the *same* data at two different implied resolutions — each step gets its own +texture, drawn near enough to 1:1 that "zoom" stops being a client-side +transform of derived geometry at all. + +**Structure: two sibling layers under one step-canvas owner, replacing the +single `_canvas` Node2D.** + +- **Terrain/classification layer — RTT, texel-exact.** One `ImageTexture` per + held step canvas, built server-side-derived / client-colorized, drawn via + `draw_texture_rect` at the tunable px ratio (1:1 ideal, up to 5×5 px/gridunit + fallback per the resolution tunable). This is **not a new pattern** — it's + `_rebuild_texture_if_needed`/`_build_tile_texture` in + `atlas_window_overlay.gd` generalized from "per-tile mosaic composite" to + "the one and only terrain path." The tile-mosaic code + (`_draw_tile_mosaic`/`_draw_one_tile`/`_tile_texture_cache`) already proves + the RTT-per-cell-block shape works and composites cleanly at orbital rest + state — it's not being invented, it's being promoted to universal. +- **Screen-space annotation layer — unscaled sibling, literal px.** Everything + currently living in `atlas_window_geometry_nature.gd`'s `_zs`-wrapped + `draw_polyline`/`draw_circle`/`draw_arc` calls (rivers, settlement glyphs, + POI rings, mouth markers) moves to a sibling `Node2D` that is **never + scaled**. Positions are world→screen transformed per-frame (cheap — it's a + linear map, not a re-derivation); sizes are constants or class-driven + constants, never divided by a zoom factor. This deletes `_zs()`, + `_zs_stroke()`, `_zs_ring_radius()`, and the entire "did I remember to wrap + this call site" failure mode — by construction, not by discipline. + +**What retires:** +- `_canvas.scale`/`_canvas.position` transform model and `_apply_transform()` + as currently shaped (a replacement "step-canvas anchor" concept survives, + see step-cross below, but it's not a continuous scale). +- The `_zs`/`_zs_stroke`/`_zs_ring_radius` compensation family in + `atlas_window_geometry_nature.gd` — wholesale, once the sibling layer lands. +- `select_rung()`'s coverage-ceiling walk (Tyre's call to make formally, but + it's dead the moment the step index replaces it — my component doesn't need + a rung selector, it needs a step index). +- The `AtlasViewer`/orbital-mosaic-vs-window split as two *code paths* — under + the stepped model, global zoom is just step 0, not a structurally different + viewer. (T-1157's dead-goldens problem is a direct consequence of this split + existing today; the redesign should collapse it, not re-target goldens at + the same fork.) + +**What survives as-is:** +- `atlas_window_tile_set.gd`'s LRU-by-key cache *shape* (see §4 — it's the + right skeleton for the new client cache, wrong eviction policy alone). +- `_filter_for_granularity_v2()` — NEAREST vs LINEAR sampling filter choice + per rung is still a real question at RTT scale-ratio draw time, independent + of the transform model change. +- `atlas_window_water_clip.gd`, `atlas_marker_overlay.gd`, + `atlas_legend_panel.gd`, `atlas_overlay_bar.gd` — these are compositing/UI + chrome, not the transform mechanism; they consume whatever the new layer + pair exposes and shouldn't need structural rewrites, only call-site updates. + +T-1158 (viewer decomposition into input/orchestration/canonical-frame +clusters) is **subsumed, not scheduled separately** — the canonical-frame +state machine T-1158 wanted to extract is the exact thing that changes shape +under stepped zoom (no more continuous zoom floor, no more +`_canonical_fit_zoom()` fitting a float zoom — the "canonical frame" becomes +"step 0"). Extracting the old cluster now would be extracting code about to be +deleted. Recommend cancelling T-1158 with the supersession note and letting +the new component's structure be decided fresh as part of this implementation +(naturally three pieces again — input/pan, step-cross orchestration, and the +two draw layers — but that's a consequence of good decomposition, not a +retained ticket). + +## 2. Step-cross experience + +**Hold-fetch-swap, not blend-fetch-swap, as the baseline — a morph is a +separate, optional cosmetic layer on top.** + +Sequence on a scroll-step: +1. Player scrolls one notch. Compute the new step's data-canvas bounds + (cursor-anchored — the center the new canvas should be requested around is + the world point under the cursor, matching the surviving entry-seam + behavior). +2. If that canvas is already in the client cache (§4), swap immediately — + this is the common case for backtracking (zoom out then back in) and for + revisiting a spot, and it's why the cache matters for *feel*, not just + bandwidth. +3. If not cached, **hold the current step's texture displayed, unscaled, + while the fetch is in flight** — this is the "between-step magnification" + red flag's actual mechanism: showing the coarser canvas magnified to fill + the new step's viewport for the fetch duration. Measurement ① confirms + this window is short: District-class server derive is sub-millisecond + served, and even an uncached 330K-gridunit step canvas is ~75 ms + derive + ~5 ms PNG-encode server-side (measurement ④'s "derivation cost + for context" row) — the hold interval is double-digit milliseconds, not a + visible stall. +4. On arrival: decode, build/update the `ImageTexture` (measurement ⑤: + worst case 8.3M px update is ~4.3–4.6 ms median, comfortably under one + frame), swap the terrain layer, re-run the annotation layer's + world→screen transform against the new step's bounds. + +**ImageTexture upload cost is a non-gating input, confirmed by ⑤.** Every +canvas size the workshop cites uploads in single-digit ms with no observed +frame-budget break, including the frame-delta-spike case. This means the +step-count/canvas-size decision (Dudley's/Tyre's call) can be made on +derivation cost and wire size alone — upload is not a constraint that trades +off against them. One concrete implementation note from ⑤: **prefer +`texture.update()` reuse over fresh `create_from_image()` per step**, not for +raw speed (reuse is marginally *slower* in the raw numbers — 4.3–4.6 ms vs +3.2 ms median at 8.3M px) but because it avoids per-step Texture object churn +on the RenderingServer side that the microbenchmark doesn't capture. And: +**use L8 wherever a plane is genuinely single-channel** — 4–9× cheaper than +RGBA8 at every size in ⑤, a free win if any wire field (elevation, a +grayscale classification pass) can ship single-channel before the client +colorizes it. + +**The morph/tween is real but strictly cosmetic, and I'd sequence it after +the hold-fetch-swap baseline ships, not with it.** A cross-fade or scale-tween +between the held step-N texture and the arriving step-(N+1) texture, purely +in screen space, never touching derived data — this is squarely inside "GPU is +presentation only." It softens the perceptual jump D-166's amendment has to +own honestly (red flag 1) without pretending to be continuous zoom. I'd +implement it as an optional `CanvasItem` alpha/scale tween gated behind a +toggle, not a hard requirement — if it turns out ugly or distracting at real +step factors, dropping it costs nothing structurally because it never +participates in the data path. + +**Client-side cache is what makes repeated step-crossing (the actual common +case — players hunt around a region, not monotonically zoom in once) feel +instant.** Per premise 9: a canvas for a fixed seed never changes, so once +fetched it is valid forever for that exact (body, step, center) key — the +`atlas_window_tile_set.gd` LRU shape already assumes exactly this ("no +freshness check, no TTL, no invalidation path — the only reason an entry +leaves is capacity pressure"). The new cache needs the same discipline **plus** +the sim-state carve-out premise 9 calls out (frozen/flooded needs a shorter +TTL layered on top of the otherwise-permanent geometry entries) — see §4 for +the store shape. + +## 3. Overlay compositing under RTT — shader vs CPU (the c1 decision) + +**I'd ship CPU `set_pixel` coloring for the terrain layer first, with a +concrete, named follow-up to shader-side compositing once the toggle set +grows — not because shaders are wrong, but because the migration risk right +now is elsewhere and CPU coloring is what's already proven in-tree.** + +Reasoning: +- **Today's code already does this and it works.** `_build_tile_texture`/ + `_rebuild_texture_if_needed` build the `Image` cell-by-cell via `set_pixel` + from typed field values (`_cell_color`/`_base_cell_color`/`_temp_cell_color`/ + `_moisture_cell_color`/`_veg_cell_color`/`_apply_glaciation` — five toggle + overlays already implemented this way). This is the temperature/moisture/ + vegetation/glaciation toggle set the question asks about, already shipped + on the CPU path. Moving the *transform model* (RTT-as-universal, no more + scaled canvas) is already the workshop's biggest client-side change; I don't + want to also change the *coloring* mechanism in the same pass without a + measured reason to. +- **The measured numbers don't force the shader answer.** Nothing in ①–⑤ + prices CPU `set_pixel` coloring cost at step-canvas scale (330K–8.3M + cells) — that's a real gap, and I'd flag it as a follow-up measurement + before committing either way at the deep end of the ladder. What ⑤ *does* + show is that the upload step (which happens regardless of who colors the + pixels) is cheap; the open question is purely "how long does building the + `Image` take in GDScript at 330K+ cells," which is untested. Given that gap, + defaulting to the known-working CPU path and measuring before the largest + canvas sizes ship is the honest sequencing — not picking shaders on the + strength of "should be faster" without a number. +- **Where shaders clearly win, and where I'd schedule the follow-up:** the + moment T-1175's per-vertex river tapering/width-grammar work starts (source + tapering, tributary-join width ramps), that work wants shader-side or at + minimum `Polygon2D`-strip geometry regardless of what the terrain layer + does — `draw_polyline` is single-width by construction and can't taper. + That's a screen-space-annotation-layer concern (§1), not a terrain-raster + concern, and it's already gated on "after the nature layer stands" per + T-1175's own scheduling note. If shader-side terrain compositing is adopted + later (multiple data textures — morphology/elev/temp/moisture/vegetation/ + glaciation — sampled and colorized in a fragment shader), the toggle + overlays and T-1175's tapering become the same mechanism for free, which is + a genuine architectural win — I'm not against it, I'm against committing to + it on zero cost data for the actual bottleneck (CPU set_pixel at 8.3M + cells) when a known-good fallback exists. +- **Recommendation for the ticket plan:** ship CPU coloring behind the new RTT + structure for the initial step-ladder cutover (lowest risk, matches proven + code), file a measurement ticket for GDScript `Image.set_pixel` cost at + 330K/2.07M/8.3M cells (the missing sixth measurement), and treat the shader + migration as the natural landing spot for T-1175 rather than a + precondition for the ladder itself. + +## 4. The client cache store — in-memory vs disk-backed + +Answering the three candidate shapes against the fact base (SQLite ships +server-side only; client has gdUnit4 + messagepack, no SQLite addon today). + +**My answer: (iii) plain `FileAccess` cache dir + index, not (i) server-side +SQLite and not (ii) godot-sqlite.** In that order of preference, for these +reasons: + +**Against (i) server-side SQLite cache DB.** The "client-primary reads as +local-machine-primary" argument is real — a warm local subprocess is fast — +but it quietly relocates a client-side design decision onto the server's +process boundary and turns every cache read into an IPC round-trip instead of +an in-process Godot call. It also risks exactly the thing the asset-pipeline +golden rule and D-227 both warn about: a second SQLite file living next to +`systems.db` invites confusion about which one is canonical, even with a +different filename, because the *mental model* "the server owns the DB" now +has two meanings (canonical snapshot vs cache). D-227's discipline needs to +be re-proven at a new file rather than reusing an already-well-understood +boundary. Not fatal, but it's the shape with the most governance surface area +for the least architectural gain — premise 9 explicitly frames this as a +*client*-side cache question, and routing it through the server subprocess +undercuts the "client-primary" framing it's supposed to answer. + +**Against (ii) godot-sqlite addon.** A real client-side store, and if the +cache needed relational queries (joins, filtered scans across many keyed +records) I'd pick this without hesitation. It doesn't — the access pattern is +point lookups by a composite key (`body_id:step:center:granularity_v2`, +extending `atlas_window_tile_set.gd`'s existing `make_key()` shape almost +unchanged) plus a periodic TTL sweep over sim-state-tagged entries. That's a +key-value store with expiry, not a relational workload. Pulling in a new +compiled addon (build/platform surface, version-pin maintenance, another +thing that can fail to load headless in CI) to do a job `FileAccess` + +`Time.get_unix_time_from_system()` already does natively is the kind of +dependency I'd only take if the simpler shape measurably couldn't do the job. +It can. + +**For (iii) plain `FileAccess` cache dir + index — the shape:** +- **Directory layout:** one file per cached step-canvas entry under + `user://atlas_cache//`, named by a hash or the same key string + `atlas_window_tile_set.gd` already builds (`make_key()` extended with the + step index) — content is the already-decided wire encoding (PNG-per-field + per measurement ④, the clear winner on size *and* speed), so the disk file + IS the wire payload, no re-encoding for storage. +- **Index:** one small JSON or binary manifest (`user://atlas_cache/index. + .dat` or similar) mapping key → `{written_at, last_read_at, kind: + geometry|sim_state, size_bytes}`. Loaded once per body-open, held in memory + as a `Dictionary` — this is the same LRU-touch shape `atlas_window_tile_set. + gd` already implements (erase+reinsert = move-to-MRU), just backed by files + on disk instead of values in the dictionary, and the dictionary now stores + metadata + a `FileAccess` path instead of the raw window `Dictionary`. +- **Two-tier eviction, matching premise 9's split exactly:** + - **Geometry entries (morphology/elev/moisture/vegetation/glaciation/ + height, everything D-227's determinism guarantee covers): LRU-evict-only, + no TTL**, same as today's `atlas_window_tile_set.gd` — a canvas for a + fixed seed never changes, so "stale" isn't a concept that applies. Global + step (step 0) entries get a **retention floor** (never evicted by the LRU + sweep, only by an explicit clear/uninstall path) — this is the concrete + mechanism for "always keep the global level" from Jeroen's outline, sized + by red flag 2's number (~1.6 MB/body at 5×5 sampling; even at all ~273 + bodies resident that's ~440 MB on disk, which is a completely different + budget conversation than 440 MB in *process memory* — disk is cheap, + RAM/VRAM residency is the thing that needed the ceiling). + - **Sim-state-tagged fields (frozen/flooded, whatever the map-time-axis + ruling lands on): explicit TTL sweep**, a periodic (not per-frame — on + body-open and on a coarse timer) pass over the index removing entries + past their TTL regardless of LRU recency. This is the literal + self-cleaning-records behavior Jeroen asked for ("on disk probably, but + with self cleaning of cache records"), and it's the one place this store + needs logic `atlas_window_tile_set.gd` doesn't have today. +- **Never the source of truth, enforced structurally, not just by + comment:** if the cache directory is deleted, the client's only behavior + change is re-fetching from the server — same guarantee D-227 gives + server-side, now proven at the client tier too. Boot-time behavior: + missing/corrupt index → treat as empty cache, don't crash, don't block + first paint. +- **Sizing from the wire table (④):** at the PNG-per-field encoding + (the clear winner — smallest and fastest at every size), a 330K-gridunit + step canvas is ~638 KB; a full global-tier set across ~273 bodies at the + 5×5 fallback resolution is the ~440 MB figure red flag 2 already computed. + That number is **disk-budget-safe on any target platform** (modern + discretionary disk cache budgets for a locally-installed game are routinely + in the multi-GB range) in a way it is not RAM-safe — which is the + strongest single argument for "disk-backed, not memory-only": premise 9's + "always keep global" instruction is only affordable at all if it's a disk + tier, not permanent process residency. An in-memory-only cache would have + to either violate "always keep global" (evict it under memory pressure, + defeating the snappy-navigation goal) or accept the ~440 MB RAM floor red + flag 2 already calls infeasible. Disk removes that tension entirely. +- **In-memory tier still exists, layered on top, not replaced.** The + `atlas_window_tile_set.gd`-style in-process Dictionary cache remains the + hot path for "the step I'm looking at right now and its immediate + neighbors" — a session-scoped, small (current `DEFAULT_MAX_ENTRIES = 24` + is a reasonable starting point, tunable) LRU exactly as it works today. + The disk store is the tier below it: a miss in memory checks disk before + going to the server. Three tiers total, cheapest-first: in-memory Dictionary + → disk `FileAccess` store → server (which has its own tiers, Dudley's to + answer). This layering is what makes "client-primary reads as + local-machine-primary" actually true without needing the server-SQLite + detour — the disk tier gets you nearly all of that latency win already, + in-process, no IPC. + +**Net recommendation:** (iii), layered under the existing in-memory LRU as a +second tier, two eviction policies (retention-floor+LRU for geometry, +TTL-swept for sim-state), sized directly from measurement ④'s table. No new +Godot addon, no second SQLite file, no server round-trip for a client-local +question. + +## Open items I'm carrying into round 2 + +- **The missing sixth measurement:** GDScript `Image.set_pixel` coloring cost + at 330K/2.07M/8.3M cells — needed before the c1 shader-vs-CPU call can be + made with the same rigor as everything else in this brief. I'd rather name + this gap now than let CPU-coloring become an unexamined default at the + largest canvas sizes. +- **Step-count/canvas-size** is Dudley's/Tyre's number to land, but it + directly sizes my annotation-layer redraw frequency and cache key space — + I'll adjust §1/§4 sizing once that lands in the lead interview. +- **Phase-5 reuse question (Jeroen's #4):** I'd scope the map-drawing + component as **Atlas-only for this pass**. The RTT-terrain + + screen-space-annotation split is a reasonable shape for a future in-world + viewport too, but Phase-5 in-world rendering has different constraints ( + per-frame redraw, character-relative camera, no step-cross discontinuity to + design around) that I don't want to speculatively design against now — that + would be exactly the kind of later-phase drag the cascade discipline warns + against. Build it right for Atlas; revisit reuse when Phase 5 actually + starts. diff --git a/docs/workshops/body-map-viewer/stig-round2.md b/docs/workshops/body-map-viewer/stig-round2.md new file mode 100644 index 000000000..d84f545f7 --- /dev/null +++ b/docs/workshops/body-map-viewer/stig-round2.md @@ -0,0 +1,300 @@ +--- +title: "Body Map Viewer — Stig's Round 2 Position" +description: "Measurement ⑥ (CPU-colorize cost), the final c1 shader-vs-CPU call, the cache-store spec with Jeroen's storage-eviction amendment, and the px-per-gridunit band recommendation" +type: workshop +status: active +workshop: body-map-viewer +created: 2026-07-25 +owner: Stig +--- + +# Stig — Round 2 Position + +Round 1 (`stig-round1.md`) flagged one open item: the c1 shader-vs-CPU call +was made CPU-first but explicitly **pending** the missing `Image.set_pixel` +cost number. Jeroen ruled at lead interview 1 to run it now — done, below, +plus the confirmed cache-store spec incorporating his new storage-eviction +amendment, and the px-per-gridunit band Dudley/Tyre need for the ladder +tables. + +## (a) Measurement ⑥ — headline + +Full doc: `measurements/t-setpixel-c1.md`. Appendix row added to the brief. + +| size | `set_pixel` median | ns/cell | byte-buffer median | ns/cell | +|---|---:|---:|---:|---:| +| 330K | 25.7 ms | 77.5 | 51.0 ms | 153.8 | +| 2.07M | 165.2 ms | 79.7 | 319.2 ms | 154.0 | +| 8.3M | 643.5 ms | 77.6 | 1,261.4 ms | 152.1 | + +Flat per-cell rate across the full 25× size range for both paths (no +cliff — same pattern every prior measurement in this appendix found). +Headless-valid: this is pure CPU `Image`/`PackedByteArray` manipulation, no +RenderingServer call in the path, unlike ⑤ which needed a real display to +avoid the headless dummy renderer's faked GPU uploads. + +**Two findings, one expected, one not:** +1. Expected: cost is real and scales linearly, not free the way upload (⑤) + turned out to be. +2. Not expected: **`Image.set_pixel` beats a hand-rolled `PackedByteArray` + direct write by ~2×**, at every size. I went in assuming the opposite — + skip the per-pixel method call, skip `Color` object construction, write + raw bytes. Measured, GDScript's own per-element indexed `PackedByteArray` + write (four separate indexed writes per cell in the buffer path) costs + more than `set_pixel`'s single per-pixel call. This is worth stating + plainly for whoever implements the terrain layer: **don't hand-roll the + buffer write as a "faster" alternative in GDScript** — the intuition + that's usually right in a compiled language doesn't transfer here. + +## (b) C1 final call — CONFIRMED, CPU-first, with an explicit sizing caveat + +Round 1 said CPU-first-pending-the-number. The number is in. **I'm +confirming CPU coloring (`Image.set_pixel`) as the shipped default for the +terrain layer, not flipping to shaders — but the number changes *how* I'd +frame the recommendation, and it sharpens rather than weakens the case for +Dudley's viewport-sized-canvas policy.** + +**Why confirm, not flip:** + +- **At the sizes the ladder will actually request, colorize is cheap.** + Dudley's round-1 position (§2, "Question 2") is explicit that + viewport-sized canvases at a fixed pixel budget — not the 8.3M-cell figure + — are the real per-step request shape; 8.3M exists in the appendix purely + as the stress-ceiling case answering "does this degrade at scale" (no). + At 330K cells, the size that shape actually produces, `set_pixel` colorize + costs **25.7 ms** — well inside "the player is waiting for a step-cross to + resolve" tolerance, and it's happening once per arrived canvas inside the + hold-fetch-swap sequence (§(c) below refines that sequence's cost + ordering), not per frame. +- **Nothing in ①–⑤ forced shaders architecturally, and ⑥ doesn't either — + it just prices the CPU path honestly instead of leaving it an + unmeasured assumption.** The original round-1 reasoning stands: today's + code already does CPU coloring successfully (`_build_tile_texture`/ + `_rebuild_texture_if_needed`, five toggle overlays shipped this way), and + the biggest structural change this workshop makes to the client (RTT as + universal terrain path, killing `_canvas.scale`) is large enough on its + own without also swapping the coloring mechanism in the same pass without + a forcing reason. +- **Where the number DOES change my framing:** round 1 called the gap + "a real gap" but didn't know if it would come back trivial or + meaningful. It came back **meaningful at the stress-ceiling size** (643 ms + at 8.3M is roughly a third of server derive time at that size — not a + rounding error) **and trivial at the realistic per-step size** (25.7 ms at + 330K). That's not a wash — it's a data point that argues *for* Dudley's + viewport-sizing policy being load-bearing on the client side too, not just + the D-226(d) governance reason he named. If some future caller ever + requested a literal 8.3M-cell canvas as a real step payload (not the + stress-test shape), CPU colorize alone would eat ~640 ms of the arrival + budget — a real, nameable cost that the viewport-sized-canvas policy + structurally avoids by never generating that request in the first place. + I'd rather land this as "the policy is validated from a second angle" than + as a new risk, since Dudley already ruled viewport-sized canvases as the + policy on governance grounds independent of this number. +- **The shader path is not closed, it's sequenced.** Same as round 1: the + natural trigger for shader-side terrain compositing is T-1175's per-vertex + river tapering work (needs shader/Polygon2D-strip geometry regardless of + what the terrain raster does) landing and, separately, if a future step + count/canvas-size decision in round 2's ladder synthesis pushes the + *realistic* per-step canvas size meaningfully above 330K–2.07M (not the + 8.3M stress case, an actual steady-state request shape), that's the + trigger to revisit — cite this measurement, don't re-guess. + +**Net: CPU coloring ships. File a note on the terrain-layer implementation +ticket: use `Image.set_pixel`, not a hand-rolled buffer write (⑥'s 2× +finding), and treat "does the real step-count/size decision ever make 330K +the *small* end rather than the steady case" as the trigger to reopen c1, +not a speculative future pass.** + +## (c) Cache-store spec — final, incorporating Jeroen's storage-eviction amendment + +Lead interview 1 ruling 2 (verbatim, captured in `lead-interview-1.md`): +*"we still may also want to evict non global level geometry based on time to +save storage for planets the player visits but never goes back to"* → +**staleness-eviction and storage-eviction are distinct axes.** Geometry never +goes stale (D-227 determinism — re-derivable, byte-identical forever), but +sub-global geometry still gets evicted on **time-since-last-visit** as a +storage-budget policy. The global tier alone is keep-always. + +This extends my round-1 two-tier design (retention-floor+LRU for geometry, +TTL for sim-state) with a **third eviction mechanism** for sub-global +geometry specifically — it was previously LRU-evict-only (matching +`atlas_window_tile_set.gd`'s existing behavior); it now also gets a +time-since-last-visit sweep. Three mechanisms, three different problems, +kept structurally separate rather than folded into one formula — this +mirrors Dudley's own multiplicative-TTL design for the server tier +(`BASE_TTL[rung] × time_decay × distance_decay`), except my two extra axes +answer different questions (capacity pressure vs. storage-budget thrift) so +I'm keeping them as two independent sweep passes rather than composing them +into one number, for the same legibility reason Dudley cites for keeping his +formula to one tunable per rung. + +### Ratified from round 1, unchanged + +- Shape: plain Godot `FileAccess` cache dir + index — no `godot-sqlite` + addon, no second SQLite file. Confirmed by lead interview 1: *"both + rejected SQLite in every shape independently"* (Dudley's server-side + answer converged on the same rejection from a different angle: a plain + D-203-shaped resource, not SQLite, for the global tier). **Composition, + not competition**, per the ruling: Dudley's server-side global tier + (~174 MB PNG-encoded, D-203-shaped) and my client-side `FileAccess` dir + are two tiers of the same cache stack, not alternatives to each other. +- Wire content = the already-decided wire encoding (PNG-per-field, T-1179's + winner) written to disk as-is — no re-encoding for storage. +- In-memory tier (the `atlas_window_tile_set.gd`-style Dictionary LRU) + remains the hot path in front of the disk tier, unchanged. + +### Three-tier eviction — the concrete spec + +**Tier 1 — Global/step-0 (region-spaced) geometry: retention floor, no +sweep at all.** Never touched by either the LRU-capacity sweep or the +time-since-visit sweep. The only way an entry leaves is an explicit +clear/uninstall action. This is the literal mechanism for "always keep the +global level." Matches Dudley's server-side answer (region-spaced, ~638 KB +PNG-encoded/body, ~174 MB across all ~273 bodies server-side) — the client +disk tier for this rung should be sized the same way, since a client that +has actually visited a body already has the same canvas the server cached, +and re-fetching it from the local warm server subprocess after a client-side +eviction would be needlessly wasteful when disk is this cheap. + +**Tier 2 — Sub-global geometry (every step below global): two independent +sweep passes, both storage-motivated, distinct from staleness.** + +- **(2a) Time-since-last-visit sweep** (Jeroen's new amendment) — a + periodic pass (triggered on body-open + a coarse background timer, never + per-frame) that walks the index and deletes any sub-global entry whose + `last_read_at` is older than a configurable threshold (proposed starting + point: on the order of days-to-weeks of real wall-clock time, tunable — + not a round-2 architecture call, a tuning pass once this ships). This + answers *"the player visited this body once, three sessions ago, and + hasn't been back"* — the entries are still byte-valid (D-227 determinism + means they're never wrong), they're just not worth the disk space for a + body the player has functionally abandoned. +- **(2b) LRU-capacity sweep** (unchanged from round 1) — if total disk usage + for the sub-global tier exceeds a configured budget, evict oldest-touched + entries first, same erase+reinsert-on-touch mechanism + `atlas_window_tile_set.gd` already implements, just backed by the index + file instead of an in-process Dictionary of raw window data. +- These are **two separate triggers checking two separate conditions** + (age-since-visit vs. total-bytes-over-budget), not one merged policy — + keeping them apart means either one can fire independently (a player who + visits many bodies briefly hits 2b before 2a; a player who stays on one + body for a long single session but never returns to old ones hits 2a + before 2b), and each is independently legible/tunable. + +**Tier 3 — Sim-state-tagged planes (frozen/flooded, whatever fields the map +time axis ruling lands on — ruled at lead interview 1, ruling 2, as +"current state via TTL-split"): explicit TTL, staleness-motivated, +structurally separate from tiers 1/2.** These entries carry a real +expiry — re-requested as sim time advances, per Jeroen's original hint made +concrete by the ruling. Distinct index field, distinct sweep condition +(`now > written_at + ttl`), never touched by the LRU-capacity or +time-since-visit sweeps (a sim-state entry doesn't get to live longer just +because disk space is available — it goes stale on its own schedule +regardless of capacity pressure). + +### Index file schema (concrete) + +One manifest per body, `user://atlas_cache//index.dat` (a small +binary or JSON — binary preferred for parse cost at scale, but this is an +implementation-detail choice, not an architecture one), loaded once on +body-open and held in memory as a `Dictionary` for the session — matching +the same "erase+reinsert = move-to-MRU" idiom `atlas_window_tile_set.gd` +already uses, just now also carrying the two extra timestamp/tag fields +tiers 2 and 3 need: + +``` +IndexEntry { + key: String # same composite key shape as atlas_window_tile_set.gd's + # make_key(), extended with step index: + # "::
::" + file_path: String # user://atlas_cache//.png (or + # one file per field, per the wire contract's own + # per-field-PNG framing — Araminta's call, mirrored + # here, not re-decided) + tier: Geometry | SimState + written_at: int # unix time, set once, never updated + last_read_at: int # unix time, updated on every cache hit (drives 2a/2b) + size_bytes: int # drives 2b's budget accounting without a stat() call + sim_ttl: int? # only present when tier == SimState; null/absent + # for Geometry entries (2a/2b apply, 3 never does) + retention_floor: bool # true only for tier-1 (global/step-0) entries; + # short-circuits both sweep passes unconditionally +} +``` + +Directory layout: one PNG-per-field file per cache entry (mirrors the wire +encoding exactly — no format translation between "on the wire" and "on +disk"), named by a hash of `key` to avoid filesystem-unsafe characters +(the `:`/`,` composite key string itself isn't a safe filename on every +target platform); the index maps the human-legible key to that hash. + +### Sweep triggers + +- **On body-open:** load the index; run 2a (time-since-visit) immediately — + cheap (a metadata scan, no file I/O beyond the index itself) and this is + the natural moment ("returning to a body") where stale-by-absence entries + are most likely to exist and least likely to be needed again in the next + few seconds. +- **On a coarse background timer** (not per-frame, not even per-step-cross — + proposed on the order of minutes, tunable): run 2b (LRU-capacity) if + total sub-global bytes exceed budget, and sweep tier 3 for expired + sim-state entries. Both are backgroundable (Rayon-queue-adjacent on the + server side; on the client this is a low-priority deferred call, never + blocking a frame or a step-cross — matches premise 1's "smart precache + allowed, never blocking user output" applied to eviction as much as to + fetch). +- **Never per-frame.** All three sweep mechanisms are explicitly excluded + from the render/input loop — this is bookkeeping, not gameplay-adjacent + work, and belongs nowhere near the 16.6 ms budget ⑤ and ⑥ both care about. + +### Per-tier budgets + +- **Tier 1 (global):** sized to match Dudley's server-side number directly + — ~638 KB/body PNG-encoded, ~174 MB across all ~273 bodies if the client + has visited every body (an upper bound, not a typical player's actual + footprint). No cap needed beyond that — it's the "keep always" tier by + definition, and 174 MB is a trivial disk allocation on any target + platform. +- **Tier 2 (sub-global):** a configurable byte budget (proposed starting + point: same order of magnitude as tier 1, e.g. a few hundred MB to + low-GB range — a tuning-pass number once real play-pattern data exists, + not an architecture call I'm locking here) that 2b enforces directly, with + 2a doing most of the practical work of keeping it under budget without + ever hitting the hard cap in normal play (a player who keeps returning to + the same few bodies never triggers 2a for those bodies' entries; a player + who body-hops constantly generates entries 2a will clear out on its own + schedule before 2b's cap becomes the active constraint). +- **Tier 3 (sim-state):** no byte budget needed — TTL alone bounds it, and + these planes are a small fraction of a step canvas's total field set (per + Araminta's round-1 schema, frozen/flooded reads off the existing + `glaciation`/morphology-water-class fields rather than adding new dense + arrays, so tier 3's actual footprint is small regardless). + +### What stays true from D-227 regardless of which tier + +Every tier is still an evictable cache, never a source of truth — deleting +the entire `user://atlas_cache/` directory at any time, for any reason +(corruption, manual clear, platform storage pressure), changes client +behavior only by causing re-fetches. No tier, no sweep policy, no budget +number in this spec is allowed to become load-bearing for correctness. This +is the same discipline Dudley's server-side tier commits to explicitly in +his round-1 doc, restated here for the client side as it was in round 1. + +## (d) Px-per-gridunit band — sent to Dudley + Tyre + +See the accompanying `SendMessage`. Summary of the recommendation for +their ladder tables: **1×1 px/gridunit at the deepest step (Tile, +D-243-pinned per Tyre's snap ruling), relaxing toward the ~5×5 fallback only +at the shallower/coarser steps where canvas extent (not spacing) is what's +growing.** Reasoning grounded in ⑥ + ⑤ together: crisper display costs +nothing extra in upload (⑤ showed upload is flat-cheap regardless of pixel +density, it's a function of byte count not "how many real gridunits per +displayed pixel") and colorize cost (⑥) is driven by **canvas cell count**, +which is already bounded by Dudley's viewport-sizing policy independent of +the display ratio — so there's no cost reason to fall back to 5×5 at the +steps where fidelity matters most (the deep, ground-level steps where the +player is closest to visible detail). The 5×5 fallback earns its keep at the +*shallow* steps instead, where canvas extent needs to cover a large ground +area and cell count would otherwise balloon for comparatively little +perceptual gain (players aren't scrutinizing per-metre detail from orbital +view). Full band recommendation in the message. diff --git a/docs/workshops/body-map-viewer/troblum-round2.md b/docs/workshops/body-map-viewer/troblum-round2.md new file mode 100644 index 000000000..abcf0982e --- /dev/null +++ b/docs/workshops/body-map-viewer/troblum-round2.md @@ -0,0 +1,920 @@ +--- +title: "Body Map Viewer — Troblum Round 2 (Adversarial Pass)" +description: "Stress-test of the round-1 measured claims and cache/governance proposals against real play patterns — findings ranked by severity" +type: workshop +status: active +workshop: body-map-viewer +round: 2 +owner: Troblum +decision_refs: [D-166, D-226, D-227, D-243, D-203, D-225, D-192, D-005] +--- + +# Troblum — Round 2 Adversarial Pass + +Read the brief (incl. the MEASURED appendix), `lead-interview-1.md`, `round-1-notes.md`, +all four round-1 position files, and all four `measurements/*.md` docs in full before +writing this. No `{agent}-round2.md` files existed at the time this was written — +I flag anywhere that matters below. + +I went into this expecting to find sloppy extrapolation. I didn't find much of +that — the four measurement docs are unusually disciplined about the +MEASURED/ARITHMETIC distinction, and every round-1 agent cites correctly. What +I found instead is more interesting: **two population-scale claims that +were never actually tested at population scale** (they were tested for +*throughput*, not for the *property being claimed*), one arithmetic units +slip, one real governance-purpose gap in the D-226(d) boundary that survives +the letter of the rule, and a cache/eviction design that has never been +pressure-tested against a play session shaped differently than "look at one +step canvas once." Findings below, most severe first. + +--- + +## BLOCKING + +### B1. The "273 bodies" hydrology parallel-throughput bench is the SAME body solved 273 times — it proves nothing about cliff rarity across the population, and the workshop is about to file a Phase-4 ruling that leans on "rare" as a population-level property + +**The claim as stated:** T-1177's headline: *"all 273 bodies Rayon-parallel ~0.7–0.8s"*, cited by Dudley (Q1, Q4), and Tyre's cliff ruling explicitly says *"real planetary heightmaps beyond the one body sampled may differ"* but still files "Phase-4 Atlas scope, not deferred" on the strength of a rarity finding whose own caveat says it's measured on "one real body (GJ1c) plus two synthetic gradients." + +**What actually breaks it:** I read the bench source directly +(`server/tests/hydrology_equilibrium_bench.rs:233-260`, +`bench_parallel_273_bodies_at_512x256`). It calls `gj1c_512x256()` **once**, +then solves that **identical elevation array** 273 times in a `par_iter`: + +```rust +let (elev, sea_level) = gj1c_512x256(); +let body_count = 273usize; +let total_basins: usize = (0..body_count) + .into_par_iter() + .map(|_| { + let result = solve(&elev, 512, 256, sea_level, default_climate()); + result.basins.len() + }) + .sum(); +``` + +This is a **throughput** measurement (can the Rayon pool solve 273 independent +jobs of this size in under a second — yes) wearing a **population-diversity** +measurement's headline ("all 273 bodies"). It answers "is per-body-open +hydrology affordable" (yes, cleanly) — it does **not** answer, and was never +designed to answer, "does cliff carving stay rare across 273 distinct real +terrains." The 273-basin-count sum reported (`total_basins`) is 273× GJ1c's +own 68 basins — a tell, if anyone checks it, that this is one terrain +replayed, not 273 terrains sampled. + +Nobody in round 1 mis-states this — Dudley's text says "512×256 (real GJ1c +working grid)" for the single-body cost row and correctly separates it from +the parallel-throughput row, and Tyre's caveat is honest about the *sampling* +limitation. But the round-1 notes and the lead interview both let "all 273 +bodies" stand unqualified as a headline in the appendix table, and the cliff +ruling (already ratified by silence for the representation, and provisionally +ruled Phase-4 by Tyre) is exactly the kind of decision where "how often does +this fire across the real population" is load-bearing for the wire-cost +argument that makes Phase-4-not-Phase-5 attractive in the first place ("nearly +free... because it's a mostly-zero field"). + +**The mandate's own hint turns out to be directly actionable, and cheaper than +advertised.** The brief's Troblum question asks "is there a cheap pre-filing +check (run the solver across the real 273-body population — the bench exists +and takes ~0.8s)?" — the bench that exists does NOT do this, but a real one is +nearly free to build from what's already checked in: + +``` +$ find wiki/star-systems -iname heightmap.png | wc -l +267 +``` + +267 real heightmap PNGs are already committed (close enough to "273 inhabited +bodies" that the six missing are very unlikely to change the finding +qualitatively). `gj1c_512x256()` already shows exactly the load/downsample +pattern needed (`load_heightmap_png` + `.downsample(512,256)`); a population +survey is: loop the 267 paths, load+downsample+solve each, count +`cliff_edge.iter().filter(|&c| c).count()` per body, report the distribution +(bodies-with-zero-carves vs bodies-with-nonzero, and the max carved-cell count +seen). At ~24ms/body single-threaded (the GJ1c number) or parallelized across +the Rayon pool the same way the throughput bench already demonstrates, +267 REAL bodies solve in well under the same ~0.7–0.8s ballpark. This is not a +"more research needed, defer the decision" finding — it's "run this one +already-half-built script before filing the cliff D-record," and it costs +minutes, not a round-3. + +**Severity and why BLOCKING, not SERIOUS:** the cliff ruling is about to be +filed as a D-record amendment with "rare, cheap to carry" as its central +argument for Phase-4 inclusion. If the real population survey finds even a +double-digit percentage of bodies with genuine carving (plausible — GJ1c's +`tectonic_class`/`hydrosphere` params are one point in a parameter space that +explicitly includes `Volcanic`/high-relief bodies per D-239 §5's morphology +gates, and D-239 §1 explicitly flags `RIVER_THRESHOLD` as eventually +per-body-class-derived, not the fixed 200 this prototype borrowed), the wire- +cost argument doesn't just weaken, the field stops being "mostly zero" and +Araminta's sparse-list encoding choice (near-free at near-zero occupancy) +needs re-costing at whatever the real occupancy rate turns out to be. Filing +now on the untested assumption risks a silent contradiction the moment +someone actually looks at a mountainous body's Atlas map and it should show +gorges that a "rare" assumption undersized the format for. + +**What resolves it:** run the population survey (concrete, minutes of work, +harness 90% exists) before the D-record for the cliff ruling is filed. Report +back: (a) fraction of the 267 real bodies with ≥1 carved cell, (b) max carved- +cell count on any single body, (c) whether any body's carve count is large +enough to threaten the "mostly zero" premise Araminta's sparse-list sizing +assumes. If the finding holds (most bodies still zero, a minority nonzero but +bounded), the existing ruling stands and gets a stronger evidence base for +free. If it doesn't hold, better to know before filing than after. + +--- + +### B2. "Cost does not gate any step in the ladder" is true for derivation but silently assumes independent re-derivation at every step — the one architecture question everyone flagged as unmeasured (seed-chaining vs consuming the coarser output) is exactly the assumption every cost number in the brief depends on, and it is explicitly still open + +**What breaks it:** Dudley states this cleanly himself (Q3, "One thing I did +NOT measure..."): every ns/cell number in T-1178/T-1154 assumes each step +**re-derives independently from `(seed, position)`**, not that a finer step +consumes a coarser step's already-computed values as input. This is flagged +as "an open data-flow question for round 2," not resolved by anyone, and it +does not appear in any of the four round-1 documents as a *ruled* item — it's +listed in round-1-notes.md's OPEN-FOR-SYNTHESIS item 8, still open at the time +I'm reading this. + +Here's why this is BLOCKING rather than a tidy loose end: **the two candidate +architectures have different scaling shapes under step-thrashing** (my +mandate's own named stress scenario), and nobody has priced the one the +outline's own text seems to prefer. Re-read Jeroen's outline verbatim: +*"This at the same time serves as seed information for the deeper cascade."* +That's not "the coarser step happens to also be re-derivable independently" — +it reads as an intentional data-flow claim that the coarser canvas +**functions as seed input** for the next tier. If that's the intended +architecture (not just Dudley's simplifying assumption for benching purposes), +then: + +- **Every measured cost number in ①②③ prices the WRONG architecture.** A + step that consumes the coarser tier's output as an input needs that coarser + tier resident/computed first — which either means step-N+1 has a hard + dependency edge on step-N (serializing what's currently modeled as + independent parallel derivation), or it means the "seed information" framing + is loose language for "informs the RNG stream," which is a different and + much smaller claim than "consumes the values." +- **Step-thrashing (my mandate's named scenario) has opposite costs under the + two models.** Independent re-derivation: zooming out then back in + re-derives step-N from scratch (measured: 63.7ms–1.8s depending on canvas + size) — cache-miss cost is a flat per-step number. Consuming-coarser-output: + zooming out then back in either (a) re-uses the still-cached coarser output + as an input, making a re-zoom-in CHEAPER than the numbers in this brief + suggest, or (b) if the coarser tier itself got evicted (see B3/S1 below, + storage-eviction is now a real design axis), the finer tier's re-derivation + chain-reacts backward through however many rungs got evicted, which is a + cost story NOBODY has measured and that gets worse, not better, exactly in + the play pattern the mandate asks me to stress (a player who steps + in-out-in-out while exploring, potentially re-triggering upstream rungs). + +**This is not a hypothetical distinction — it changes what "measured, not +extrapolated" means for the whole appendix.** If round 2 or round 3 lands on +the consuming-coarser-output model (which reads as closer to what the outline +actually asked for), literally every ①②③④ cost number needs a footnote at +minimum, and possibly a re-bench, because none of them price a dependency +chain. + +**Severity:** BLOCKING because it's not a refinement of an accepted +architecture — it's a fork in the architecture itself that the brief's own +Expected Output 3 ("the step ladder... deepest step's canvas policy") cannot +honestly be finalized without resolving. Filing the D-record for "cost does +not gate any step" without this resolved risks the record citing numbers that +don't describe the shipped system. + +**What resolves it:** explicit round-2/round-3 ruling: does a finer step's +derivation (a) call `derive_at_metres(seed, position, finer_spacing)` +independently, treating the coarser canvas as *display-continuity input only* +(Dudley's assumption, what's actually been measured), or (b) literally sample/ +consume the coarser step's resolved field values as part of its own derivation +(the "seed information for the deeper cascade" reading)? If (b), a fresh cost +pass is needed before the ladder table in Expected Output 3 can cite the +existing numbers as authoritative for that shape. + +--- + +## SERIOUS + +### S1. Evict-then-revisit cost spikes are real and un-costed — Jeroen's storage-eviction amendment interacts with the TTL(detail,time,distance) formula in a way nobody has priced end-to-end + +**The scenario (my mandate's #2/#5):** Jeroen's lead-interview ruling adds +storage-eviction on time-since-last-visit as a second, independent axis from +staleness-eviction — *"we still may also want to evict non-global-level +geometry based on time to save storage for planets the player visits but +never goes back to."* Good instinct, but nobody has run the number on what a +revisit costs once eviction actually fires. + +**The arithmetic (grounded in T-1179's measured derive+encode numbers):** a +single 330K-cell district-rung canvas costs 74.8ms derive + 5.41ms PNG-encode += **80.2ms** to regenerate server-side (before wire transfer and client +decode/upload, which measurement ④/⑤ show add a further ~4-10ms each). A +player who explored, say, 20 distinct district-rung windows of a body during +one long-ago visit, gets storage-evicted on time-since-visit, and returns: +**20 × 80.2ms ≈ 1.6 seconds of pure server derive+encode cost concentrated +into the moment they re-open that body's Atlas view** — before wire and +client costs are even added. That's not catastrophic, but it's also not +"snappy re-navigation," which is the entire stated purpose of the "always keep +global" instinct this eviction rule is explicitly carved out from. And this is +the OPTIMISTIC case — District rung. A player who was deep-stepping (Block or +Tile rung, viewport-sized canvases) across many locations before abandoning +the body has a much larger number of small entries to re-derive, and nobody +has counted how many viewport-sized canvases a typical "explore a city" session +generates. + +**What nobody has specified, and needs to be specified before this ships:** +1. **Does storage-eviction apply per-entry (fine-grained, LRU-by-canvas) or + per-body (coarse, "haven't opened this body's Atlas in N days, drop + everything but global")?** Stig's `FileAccess` design (round 1, §4) says + "two-tier eviction... geometry entries get LRU + a retention floor at + step-0" — that's per-entry LRU, which means a body a player visits + *occasionally* (not "never goes back to," but not "frequently" either) + could have some of its sub-global entries evicted and others not, + producing a **partial-revisit cost** that's neither the full 20-canvas + number above nor zero — an unmeasured middle case that's actually the + MOST common real pattern (players revisit systems they've been to before, + irregularly, not never/always). +2. **Does the TTL formula's `distance_decay` term fight the storage-eviction + rule?** Dudley's TTL formula (`ttl = BASE_TTL[rung] × time_decay(age) × + distance_decay(distance_from_focus)`) already decays entries far from + current focus faster. If storage-eviction is a *second*, independently- + timed sweep on top of that TTL, a body the player is CURRENTLY navigating + (high focus, TTL should be long) could still hit a storage-eviction sweep + timed off calendar/session time rather than in-session focus, evicting an + entry the TTL formula would have kept. Nobody has specified which axis + wins, or whether they're the same mechanism wearing two names (round-1 + notes doesn't resolve this either — it's absent from the OPEN-FOR-SYNTHESIS + list entirely, which itself is a gap: this should have been item 11). + +**Severity:** SERIOUS, not BLOCKING, because the base numbers (80ms/canvas +regen) are cheap enough that even an unoptimized worst case doesn't "catch +fire" — but it's serious because the FEEL goal ("atlas navigation snappy after +first calc," Dudley's own words) is explicitly what this mechanism could +undermine for exactly the play pattern (occasional revisits) that's most +common, and nobody has written down the eviction granularity or the two-axis +interaction rule. + +**What resolves it:** round-2/3 synthesis needs to state explicitly: (a) +eviction granularity (per-canvas vs per-body), (b) which axis (TTL distance- +decay vs storage-eviction time-since-visit) is authoritative when they +disagree, (c) a worst-case revisit number using whatever granularity is +chosen, computed the way I did above but for the actual chosen unit. + +--- + +### S2. The courses-inclusive rate is measured ONCE, at light density (18 courses), on ONE body — every step-thrashing/deep-pan cost projection implicitly assumes this generalizes, and the workshop's own document says not to generalize it + +**What the measurement actually licenses, read carefully:** T-1178's own text +is explicit and correct about the limits of what it found — I want to confirm +this is NOT itself a finding of extrapolation (the document is honest about +its own scope), but flag that the workshop's *use* of the number downstream +risks over-generalizing what the document itself carefully scoped. + +The 195.0 ns/cell "courses-inclusive, real density" number comes from **one** +window on **one** body (GJ1c, district `(7520, -2932)`, 18 courses in a +331,776-cell window) — chosen specifically because it's the densest course +window anyone happened to measure. The document's own H3 finding says the +headline table's square-path numbers are courses-inclusive but SPARSE (3-10 +courses), and explicitly warns: *"The courses-inclusive rate at REAL +production course density is covered only by Cross-check 1... cite that +number, not the headline table, for a courses-representative rate."* + +That's good discipline — but it means the entire ladder's cost story for a +**river-delta body, a body with many parallel drainage channels, or a densely +riverine biome** (all real terrain classes D-239's morphology gates +explicitly support — BraidedDelta, MeanderReach, AlluvialPlain families) is +projected from **one 18-course sample on one body**, not from a density +distribution across real terrain types. 18 courses in a 331K-cell window is +"nearly 2× the synthetic body's density" per the document's own framing — but +"2× the driest measured case" is not the same claim as "representative of the +densest real case." A body whose morphology leans heavily toward +BraidedDelta/MeanderReach (the exact families D-239 §5 names as real, +selectable outcomes) could plausibly carry courses at meaningfully higher +density than GJ1c's 18-in-331K, especially at the deep Tile/Block rungs where +a dense river network's tributaries are all individually resolvable (the +deep-step 83K bench explicitly EXCLUDES courses entirely, per H2 — so the one +number closest to "the actual deepest, most course-dense scenario" is the one +number this whole measurement set has zero data on). + +**Why this matters for step-thrashing specifically (my mandate's named +scenario):** a player panning rapidly around a river delta at Block/Tile +spacing generates many step canvases in quick succession, each paying the +per-cell course cost. The <5% bound is well-established at District spacing +and light-to-moderate density — it has never been checked at Block/Tile +spacing (where course geometry is proportionally a larger fraction of a much +smaller viewport) or at delta-class density (where course COUNT, not just +per-course cost, could be several multiples of GJ1c's 18). + +**Severity:** SERIOUS not BLOCKING. The <5% bound has enough headroom +(measured against a ~5ms District-cap baseline) that even a 3-4x density +multiplier at a river-dense body likely stays affordable — this isn't a +"computer catches fire" risk. But it is a real, named gap in the "measured +not extrapolated" claim the whole batch prides itself on, specifically at the +one spot (deep-step, high-density river terrain) where the excluded cost is +least likely to stay proportionally small. + +**What resolves it:** one additional bench — deep-step (83K-cell, Block or +Tile spacing) window on a body selected for high course density (a +BraidedDelta or MeanderReach-dominant body, if the catalog can be queried for +morphology-family distribution; failing that, a synthetic river network +authored to be denser than GJ1c's). Cheap to add given the harness already +exists; closes the one density/spacing combination the current appendix has +literally zero coverage of. + +--- + +### S3. The D-226(d) whole-body prohibition survives the LETTER at every single step, but the client-side cache accumulation mechanism has no structural ceiling — the purpose of the rule (no metre-resolution whole-body derivation) is defended only by "nobody will pan that much," not by construction + +**The stress test my mandate asked for, run concretely:** Dudley and Tyre's +independently-argued convergence on viewport-sized canvases is correct and I +have no finding against the SERVER-side policy — it holds cleanly, and the +17ms/83K-cell number genuinely is never a whole-body derivation on any single +request. That part clears. + +But the mandate specifically asks whether "pan-assembled coverage over time +amounting to whole-body at fine spacing" is a real boundary risk given +client-side cache accumulation (Stig's `FileAccess` store, premise 9). Here's +the number: covering GJ1c's own 512×256-district working-grid extent +(1,048.576 km × 524.288 km — the equirectangular working-grid footprint, a +reasonable proxy for body coverage) with deep-step viewport tiles (216m × +384m, the measured 82,944-cell shape) requires **~6.6 million tiles**, at +~156 KB PNG-encoded each (interpolated from T-1179's per-cell PNG rate), +totaling **~985 GB** on disk to fully assemble one body at 1m spacing via +client-cached viewport canvases. + +That number is obviously never going to happen by accident in normal play — +which is exactly why I'm calling this SERIOUS, not BLOCKING: no player is +going to pan a Tile-spacing viewport across 6.6 million distinct windows. +**But "obviously not by accident" is a practical-infeasibility argument, not a +structural one**, and the round-1 documents (Dudley's and Tyre's both) argue +the viewport-sizing rule as a *governance necessity*, i.e., as the thing that +keeps the design "legal by construction." It is legal by construction +**server-side** (no single request ever asks for whole-body coverage) — it is +legal only by *practical improbability* **client-side**, once a disk-backed, +retain-forever-until-evicted cache is added on top. If a determined player (or +a QA/agent harness doing exactly the kind of automated systematic sweep +D-226(4)'s `AtlasAgentInterface` explicitly builds for) methodically panned a +body at Tile spacing to completeness — slow, but not physically prevented by +any mechanism in this design — the client's own disk cache would, over time, +assemble the exact near-whole-body metre-resolution artifact D-226(d) exists +to forbid. It would just be assembled as N discrete files rather than one +canonical canvas, which is a difference of *packaging*, not of *information +content* — and the rule's stated purpose (avoid a whole-body metre-resolution +planetary map layer existing) is about information content, not file count. + +**This is exactly the gap the mandate asked me to name: does accumulation +violate the rule's PURPOSE even if not its letter?** My answer: yes, in +principle, though the practical risk is low given the tile count required. +The more concrete risk isn't "a player does this for fun" — it's the +**agent-navigable QA channel D-226 item (4) already built** (`AtlasAgentInterface`, +`observe`/`act`, described explicitly as turning "human-eyeball review into an +agent-automatable QA sweep across the whole Reach"). An automated sweep is +precisely the actor most likely to do a systematic, exhaustive pan — and if +that sweep's client-side cache is retained (the "always keep global" + +LRU-with-retention-floor design does NOT explicitly exclude an automated +client from accumulating sub-global entries without bound), a QA run against +one body at fine spacing could, over enough wall-clock time, produce the +forbidden artifact as an unintended side effect of testing, sitting quietly +in `user://atlas_cache/`. + +**Severity:** SERIOUS. Doesn't block shipping the ladder (the server-side +policy is sound and the practical risk from normal play is genuinely low), +but it's a real gap between letter and purpose that a workshop explicitly +concerned with "not just cost, a governance boundary" (red flag 3's own +framing) should close on paper, not leave to improbability. + +**What resolves it:** state explicitly, as part of the D-226(d) amendment +text, that the **prohibition is a per-request/per-derivation constraint, not +an aggregate-storage constraint** — and separately, add an explicit cap on +client-side cache retention at deep rungs (a maximum resident tile count or +disk quota per body at Tile/Block spacing, independent of the "retention +floor at step-0" rule that already exists for the global tier). This turns +the current "improbable in practice" defense into an actual structural +ceiling, which is the same discipline the rest of this workshop already +applies everywhere else (measured numbers, not vibes). + +--- + +### S4. Sim-state determinism for frozen/flooded is real (D-228 already answers "what serves them"), but the TTL-split's staleness boundary depends on a "phase" granularity nobody in this workshop has named, and the water-height mechanism is NOT wired to the map yet + +**What I verified clears (good news first, per my mandate's instruction to +report clean findings explicitly):** the "are frozen/flooded actually +deterministic per (seed, sim-time)?" half of my mandate's question 5 checks +out cleanly. D-228 (already-filed, not a round-1 invention) states the +mechanism precisely: water-height is *"a region property computed once per +phase (not per tile, not per frame)... a pure function... recomputed on phase +change"* — a seasonal term phased continuously by latitude plus an optional +tidal term. This is genuinely deterministic given `(seed, region, phase)` — +no RNG, no accumulation, matches D-010. **Cleared: sim-state components for +water-height are deterministic per the existing, already-filed D-228 model — +this is not a new risk the body-map-viewer workshop introduces.** + +**What is NOT cleared, and is a real gap for the TTL-split ruling (Jeroen's +lead-interview #2):** "recomputed on phase change" begs the question this +workshop needs an answer to and hasn't produced one: **what triggers a phase +change, and at what granularity does the client's TTL need to re-request to +stay non-stale?** D-228's own text names two clock terms (seasonal — a "year +clock," continuous by latitude; tidal — a lunar/day clock, only present with +a moon) but "computed once per phase" doesn't say how long a phase lasts in +real sim-time, and nothing in the four round-1 documents or the measured +appendix touches this. Jeroen's own TTL hint ("maybe shorter ttl on the +climate sim state components") is a directional instinct, not a number — and +it can't be turned into one without knowing the phase cadence. + +**The concrete failure mode this produces:** if a phase is short (say, a tidal +term on a body with a fast-orbiting moon — plausible given the system catalog +includes many-moon systems) and the client's TTL is tuned assuming a +seasonal-length phase, a player who holds a view over a coastal/tidal-flat +gridunit across a phase boundary sees **stale flooded/dry state** rendered +past its validity window — not a crash, not data corruption, just a quietly +wrong map for however long the TTL overshoots the real phase cadence. The +inverse failure (TTL too short) re-requests a sim-state plane that hasn't +actually changed, which is wasted wire/derive cost but not a correctness bug +— asymmetric risk, meaning erring toward "too short" is the safe default, but +nobody has stated that as a design rule either. + +**Severity:** SERIOUS, not BLOCKING — the determinism substrate is sound +(the good-news half above), and this is a tuning-parameter gap, not an +architecture gap. But it's a real gap: Jeroen explicitly ruled the TTL-split +model as the working answer at lead-interview-1, and the model as ruled has an +unfilled variable (phase cadence) that determines whether it actually holds +in play. + +**What resolves it:** name the phase cadence (or the range of cadences across +body types — tidal-locked/fast-moon bodies vs moonless/seasonal-only bodies +plausibly need different `BASE_TTL` values for the sim-state plane, similar +to how Dudley's own `BASE_TTL[rung]` is already per-rung) before finalizing +the TTL formula's sim-state branch. This is a Dudley/Araminta follow-up, not +something I can resolve from the measured appendix — flagging it as an +unanswered input the round-2 synthesis needs, not asking for a re-bench. + +--- + +### S5. The disk-backed client cache has no schema/version field on cached entries — the one place D-192's "client+server always co-ship, no version skew" guarantee genuinely does NOT hold + +**What I checked, and why this is a real gap not a nitpick:** D-192 (already +filed, confirmed by direct read) explicitly drops the protocol version +handshake on the rationale that *"our actual deployment is a subprocess: the +Godot client launches the Rust server it was built with. They are always in +sync at runtime."* That's true for the LIVE wire protocol, and it means my +mandate's item 6 ("version skew between client and server during the +transition") is mostly a non-issue for the tagged-envelope migration itself +— there is no live-network deployment where an old client talks to a new +server. + +**But Stig's round-1 disk-cache design (§4, ratified by silence at +lead-interview-1 as part of "cache composition... compose, don't compete") +breaks exactly this guarantee, and nobody has named it.** A disk-backed, +self-cleaning, `user://atlas_cache/`-resident cache **persists across game +updates** by construction — that's the entire point of a persistent cache +(survive process restart, survive session boundaries). D-192's "always in +sync at runtime" argument is about the LIVE client-server pair in one running +process; it says nothing about a cache file written by version N of the game +being read back by version N+1 after a patch changes the wire schema (a new +dense field, a changed enum discriminant range, a bumped `SCHEMA_VERSION`- +style change to the payload shape itself). I checked Stig's cache design text +directly (`stig-round1.md` §4) for any versioning discipline on cached +entries — **there is none**: the index schema he specifies is `{written_at, +last_read_at, kind: geometry|sim_state, size_bytes}`, with no field naming +which wire-schema version produced the cached bytes. + +**The concrete failure mode:** a game update changes `DistrictWindowLayer`'s +field set (adds the `cliffs` sparse list this very workshop is about to +introduce, or bumps an enum's discriminant range per T-1150's own precedent +of "unknown values fall back, never trusted from the wire"). A player who has +a warm disk cache from before the update opens the Atlas. If the client +blindly deserializes the stale-schema cached bytes as if they were the new +schema (the most likely naive implementation, since the cache's whole selling +point is "skip the fetch, decode from disk"), this is either a hard decode +error (best case — the mismatch is caught) or, worse, a **silent +misinterpretation** of old bytes as new fields (worst case — exactly the +class of bug D-225's own 2026-06-12 amendment was written to prevent for the +LIVE wire, extended here to the DISK format, which nobody has extended the +same discipline to). + +**Severity:** SERIOUS, not BLOCKING — this is a real, fixable gap, not an +architecture-breaking one, and the fix is cheap (one extra field). But it's +exactly the kind of "invisible until it's wrong" decision Araminta's own +round-1 opening line warned about, applied to a part of the design that +genuinely does cross a version boundary D-192 was written to assume away +everywhere else. + +**What resolves it:** add a schema/version tag to every cached entry (a +`generator_sha`-style stamp, cheap to compute, or simply the game's own +`project.yaml` version string) at write time, and a check at read time — +mismatch = treat as cache miss, re-fetch, don't attempt to decode. This is a +small addition to Stig's already-designed index shape, not a redesign, and it +should land in the same round-2/3 pass that finalizes the cache store rather +than as a later patch once someone hits a stale-cache decode bug in the wild. + +--- + +## NOTE + +### N1. Dudley's "~174 MB" global-tier figure is decimal-MB (SI, 1000×1000), not MiB (1024×1024) — internally consistent but worth flagging before it gets budgeted against actual RAM/disk allocation numbers + +Checked the arithmetic directly: Dudley's own measured per-body numbers +(1,990,693 bytes raw / 638,382 bytes PNG-encoded at 330K cells, T-1179) times +273 bodies: + +- Using MiB (1024²): 273 × 638,382 / 1024² ≈ **166.2 MiB** +- Using decimal MB (1000²): 273 × 638,382 / 1000² ≈ **174.3 MB** ← matches + Dudley's stated "~174 MB" exactly + +Not an error — the arithmetic is internally consistent under the decimal-MB +convention, and it happens to also match red flag 2's own pre-measurement +"~440 MB at 5×5" estimate reasonably (both used the same convention, so they +compare correctly to each other). Flagging only because the moment this +number gets used to size an actual memory allocation or `du`-reported disk +budget, the ~8MB gap (166 vs 174) between conventions is exactly the kind of +silent unit-drift that causes a "why doesn't the number match what `du -h` +shows" ticket three months from now. Recommend the filed D-record state the +convention explicitly (SI decimal MB) the first time this number appears. + +**No action needed beyond a one-line convention note in the filed record** — +this is the "clean bill" item my mandate asked me to report explicitly when a +claimed number checks out. The ~174 MB figure IS what Dudley's own measured +inputs produce, under a stated convention. + +### N2. Region-as-step-0 is still unconfirmed by Araminta (round-1-notes item 9) — I have no `araminta-round2.md` to check against, flagging that this is still open at time of writing + +Per the mandate's instruction to note what I didn't see: no `{agent}-round2.md` +files existed in the workshop directory at the time I read it. Tyre's §1e +flagged "Region likely IS the global/step-0 rung, worth confirming" as +carried into round 2. If this is still unconfirmed when synthesis closes, it +directly affects two of my own findings above — S3's viewport-tile-count +arithmetic assumed the deep end of the ladder starts from a District-scale +"global" tier's working-grid extent as the body-coverage proxy, which is a +reasonable stand-in either way, but the exact numbers would shift slightly if +Region turns out not to be step-0. Not a finding against anyone's work, just +noting the input I was missing. + +### N3. Cleared: the courses-cost bound itself (<5% at District-cap, the one number actually stress-tested at real density) holds up + +Distinct from S2 above (which is about the SCOPE of what's been measured, not +its accuracy) — I directly re-checked the one courses-inclusive-at-real- +density number that exists (Cross-check 1, T-1178): 195.0 ns/cell vs the +synthetic fixture's 192.0 ns/cell is a 1.56% delta, correctly described as +"within 2%." The arithmetic is right, and the <5% District-cap bound +(`+0.09–0.21ms` against `~5ms`) is 1.8–4.2%, also correctly inside the stated +threshold. No finding here — citing this as the "clean bill" companion to S2, +since S2 is about what ISN'T covered, not about the one number that is. + +--- + +## Summary table + +| # | Severity | Finding | Resolution cost | +|---|---|---|---| +| B1 | BLOCKING | "273 bodies" hydrology bench is 1 body × 273 — no real population-scale cliff-rarity survey exists | Minutes — harness 90% exists, 267 real heightmaps on disk | +| B2 | BLOCKING | Seed-chaining data-flow (independent re-derive vs consume-coarser-output) is unresolved and changes every cost number's validity | Architecture ruling, then possibly a re-bench pass | +| S1 | SERIOUS | Storage-eviction × TTL-distance-decay interaction unspecified; evict-then-revisit costs ~80ms/canvas × N canvases, uncosted at realistic N | Specify eviction granularity + axis precedence | +| S2 | SERIOUS | Courses-inclusive rate measured once, light-ish density, one body — zero coverage at deep-step + high-density-river combination | One more bench, harness exists | +| S3 | SERIOUS | D-226(d) holds by letter at every step but not by purpose against unbounded client-cache accumulation (~985GB to fully tile one body — improbable but not structurally prevented) | Add explicit per-body deep-rung cache cap; state prohibition as per-request not aggregate | +| S4 | SERIOUS | Sim-state determinism substrate is sound (CLEARED) but phase-cadence for TTL sizing is an unfilled variable | Name phase cadence(s) per body class | +| S5 | SERIOUS | Disk cache has no schema/version tag — the one place D-192's "always in sync" guarantee doesn't hold (cache persists across updates) | One field addition to Stig's index schema | +| N1 | NOTE | ~174MB figure is decimal-MB not MiB — internally consistent, state convention explicitly | One-line note in filed record | +| N2 | NOTE | Region-as-step-0 still unconfirmed at time of writing (no round-2 files existed yet) | Informational | +| N3 | NOTE | Courses <5% bound itself is accurate where measured (cleared) | None — clean bill | + +Top-line read for the lead interview: nothing here says "computer catches +fire" or "the architecture is wrong." Everything here says "two claims are +resting on measurements that don't cover what they're cited for" (B1, B2) and +"three real interaction effects between mechanisms that were each individually +well-designed haven't been costed together" (S1, S3, S5). All five of the +serious-or-worse findings have cheap, concrete resolutions — none require +walking back a round-1 position, and B1 in particular is likely to make the +cliff ruling's evidence base stronger, not force a reversal, once run. + +--- + +# ADDENDUM (post-round-2-files) + +All four round-2 files (`dudley-round2.md`, `araminta-round2.md`, +`stig-round2.md`, `tyre-round2.md`) now exist and have been read in full. +Short pass per the coordinator's four items. Verifying my own findings +against them, not re-litigating what's already settled. + +## 1. Finding B2 (seed-chaining fork) — RESOLVED. Dudley's ruling closes it cleanly, and I verified the mechanism against source, not just his prose. + +**Dudley's round-2 §(b) ruling: independent re-derivation from `(seed, +position)`; a finer step may call a coarser rung's derivation FUNCTION, never +read a cached RESPONSE.** I checked this against the actual code rather than +taking the argument on faith, because "this closes the cost-shape concern" is +exactly the kind of claim that needs verifying, not just reading. + +Read `server/src/atlas/district_profile.rs:1705-1732` directly — +`derive_at_metres` (the function every T-1178/T-1154 bench calls) computes +`region_baseline_c` via `region_profile::region_baseline_at_district(...)` +**inline, every call, with the comment stating explicitly: "No pre-built +cache here — the on-demand path derives the four surrounding region +baselines directly. Pure, deterministic, cheap."** This is not a +generalization from a different code path — it is the *exact* function every +measured ns/cell number in the appendix already exercises. So Dudley's claim +that "my own cost numbers... are validated as the correct model" isn't an +assertion resting on the ruling being correct in principle — the nested-call +cost is *already baked into every measured number*, because the benchmarked +function already does this. **This fully closes my concern: the ①②③ cost +basis is validated, not just argued to be validated.** + +**What remains for Jeroen, stated precisely, per the coordinator's ask:** +Jeroen's own outline sentence — *"This at the same time serves as seed +information for the deeper cascade"* — is genuinely ambiguous between two +readings, and interview 2 is where that ambiguity needs to close, not before. +Dudley's ruling asserts his function-composition reading is what Jeroen +*meant*, grounded in the fact that this pattern already ships +(region-baseline-feeds-district). That's a strong circumstantial argument +(it's the only reading consistent with existing shipped code and with D-227), +but it is still an inference about authorial intent, not a confirmation from +Jeroen himself. **The interview-2 item, precisely stated: ask Jeroen to +confirm "seed information for the deeper cascade" meant "the finer +derivation calls the coarser rung's derivation function as a sub-computation, +the way region-baseline-feeds-district already works today" — not "the finer +step consumes the coarser step's already-computed response bytes as a +literal input."** If he confirms, B2 is fully closed with authorial +agreement, not just Dudley's reading of the code precedent. If he meant the +second reading, the cost model needs the re-bench I originally flagged — but +given the mechanism is architecturally forced by D-227 (a response-consuming +model would require solving the cache-dependency-chain problem Dudley's +argument-point 1 lays out, which nothing in the outline text asks for), I +think this is very likely to confirm cleanly. Downgrading this from BLOCKING +to a one-line interview-2 confirmation item, not a re-open. + +## 2. The envelope design — attacked directly, one real angle found, one attack that didn't pan out (reporting both, per my mandate). + +**Attack 1 (didn't pan out — reporting the clean bill): does `StepCanvasRequest` +risk a structural collision with legacy `AtlasLayerRequest` in the `ShapeProbe` +demux, given `AtlasLayerRequest` has no boolean discriminator (unlike +`star_map`/`city_names`/`browse`)?** Checked directly against +`server/src/bridge/mod.rs:96-152`. `AtlasLayerRequest`'s shape-identity in +`ShapeProbe` is `body_id.is_some() && up_to.is_some()` (`up_to` is a +*required*, non-`#[serde(default)]` field on the struct itself, confirmed by +reading `layer_proxy.rs:510-512`). Dudley's `StepCanvasRequest` struct +carries `body_id` but never `up_to` — so no minimal well-formed +`StepCanvasRequest` payload can satisfy `AtlasLayerRequest`'s two-field +identity check, regardless of try-order placement. Dudley's text also +explicitly extends `ShapeProbe` with a `step_canvas` field and folds it into +the mutual-exclusivity sum — so a payload carrying both `step_canvas` and +`up_to`+`body_id` would be correctly rejected as ambiguous by the existing H1 +defensive check, not silently misrouted. **Cleared: the demux design holds +under a targeted collision attack, including the one case (legacy shape has +no discriminator) that looked like the most likely soft spot.** + +**Attack 2 (the "no coexistence needed" claim, stress-tested against +version-skew, not just live-wire skew — this is where a real gap was, and +it's already been found and closed by the workshop before I could report +it):** I went looking for exactly the gap I flagged in my original S5 +(disk cache persists across a version boundary D-192's "always in sync" +argument doesn't cover) and found `tyre-round2.md` §(a.9) already cites it +verbatim as "Troblum S5" with a complete, record-ready fix (schema/version +tag on every cache entry, tag-mismatch = cache miss, never a decode attempt). +Verified the fix is structurally sound: it correctly scopes the claim (D-192's +live-wire reasoning stands unchanged; only the persistent-cache boundary needs +the new tag) and correctly treats a version mismatch as "just another +eviction case" consistent with D-227's existing discipline, not a new failure +mode. **No further attack needed here — this is closed, and closed well.** + +**Attack 3 (genuinely new, not previously flagged by me or found addressed in +round 2): the migration's own sequencing claim — "the server-side sixth-shape +addition and the client-side viewer rewrite don't have to land in the same +PR" — is correct for the LIVE wire (D-192 co-ship holds at every commit +because client+server build together), but says nothing about the +**disk cache accumulated during the OLD viewer's lifetime being read by the +NEW viewer** once the cutover lands.** Concretely: a player has an existing +`user://atlas_cache/` populated entirely by the retiring `AtlasWindowViewer` +(old `district_window`-keyed entries, old key shape +`make_key()` without a step index per Stig's round-1 text). The new stepped +viewer ships, checks the cache directory, and — assuming Stig's `FileAccess` +index is keyed by the *new* composite key shape +(`body_id:step:center:n:granularity_v2`, per his round-2 `IndexEntry.key` +spec) — simply won't find any hits against old-format keys. This isn't a +decode-corruption risk the way S5 was (different key shapes don't collide, +they just miss), so it's lower severity than S5 was — but it does mean the +cutover moment is a **guaranteed 100% cache-cold start** for every player +with pre-existing Atlas usage, not a graceful degrade. Nobody has stated +whether the old `user://atlas_cache/` directory (if the legacy viewer even +had a disk cache — checking Stig's round-1 text, the *old* `AtlasViewer` +only had the in-memory `atlas_window_tile_set.gd` Dictionary LRU, no disk +tier at all, since the disk-backed store is this workshop's own new +proposal) gets cleared, ignored, or orphaned on cutover. **This is a NOTE, +not a SERIOUS finding, once traced fully** — because the legacy viewer never +had a disk cache to begin with (confirmed: Stig's round-1 §1 describes only +`atlas_window_tile_set.gd`'s in-memory Dictionary as the pre-existing cache), +so there's no stale-format directory to orphan; the new disk cache starts +genuinely empty on first use regardless of cutover timing. Downgrading this +from a finding to a **clean bill**: the "no coexistence needed" framing +holds for the disk cache specifically because there was no prior disk cache +to migrate away from. Flagging only so the implementation ticket doesn't +need to write dead migration code for a cache tier that never existed. + +## 3. Ladder tables — Option B's zero-asterisk claim: mostly holds, one small asterisk found inside it; D-226(d) letter-compliance and the Region→District seam argument both check out. + +**The zero-asterisk claim, checked row by row against what was ACTUALLY +measured this session vs. inherited from an earlier baseline document.** +Option B's five rows: Region (MEASURED, T-1178's 8.3M-cell District-spacing +row reused — see caveat below), District (MEASURED, T-1178), Quarter +(same-band), Block (MEASURED, T-1154's 128m sweep), voxel (MEASURED, T-1154's +83K-cell deep-step bench). Four of five are genuinely, directly measured +THIS session. **The Quarter row is the one soft spot**: T-1154's own text +frames Quarter's per-cell rate as "the existing... Quarter (1.823 µs/cell) +row[s] already in `atlas-zoom-ladder-t1143.md` §7" — i.e., Quarter's per-cell +rate is inherited from an **earlier session's baseline document**, not +re-measured fresh this workshop. T-1154 confirms Block/Tile land in the +*same band* as that inherited District/Quarter baseline, which is a real and +valid cross-validation — but it means Quarter was never put through this +session's own 8.3M-cell parallel sweep the way District and Block were +(checked directly: T-1178's 8.3M square-canvas row is explicitly District +spacing only, `t1178-t1154-derive-bench.md:148`). **This is a small +asterisk, not a real problem** — the same-band confirmation mechanism (flat +~190-220 ns/cell parallel rate holding across every rung this session DID +test at every size) makes it very likely Quarter holds too, and Dudley's own +table already marks it "~1,827 ms (T-1154 same-band confirmation)" rather +than a flat unqualified number, so the workshop's own documentation is +honest about this — I just want it named explicitly as "one row is +same-band-inferred, not this-session re-run at 8.3M," since "zero asterisks" +as a literal claim slightly overstates it. Doesn't change the recommendation +(Option B is still clearly the best-evidenced skeleton) — this is a +precision note, not a reversal. + +**D-226(d) letter-compliance at each skeleton's deepest step:** checked all +three options (A/B/C) — every one bottoms out at voxel (1 m), display-ratio- +sized (216×384 m, 82,944 cells), never the fixed 3840×2160/8.3M-cell +canonical shape. This holds the D-226(d) letter identically across all three +skeletons — the deepest-step canvas policy doesn't vary by which skeleton +wins, only the *path* to reach it (how many intermediate steps) varies. No +skeleton-specific compliance risk found; this was already the corrected +convention Dudley applied uniformly per his own note in §(c) ("noted +consistently in all three tables below, not just this one"). + +**The Region→District seam argument:** Dudley's claim that the ÷100 jump at +step 0→1 "matters for serving... lines up with a real architectural +boundary" (the global/sub-global cache tier split) — checked this against +the actual cache-tier spec (both his §(d) and Stig's round-2 §(c)): yes, Tier +1 (global, keep-always) is Region-spaced exactly, and Tier 2 (storage- +evictable) starts at District. The architectural seam and the display-factor +seam are the same boundary by construction, not a coincidence — this holds +up. **Cleared.** + +## 4. TTL-split planes (araminta-round2.md §a) and three-tier cache spec (stig-round2.md §c) against my S1/S5 — both specs now address the concerns; S1 substantially resolved, S5 fully resolved (see item 2 above, already covered by tyre-round2.md §a.9). + +**S1 (evict-then-revisit cost spike, TTL-distance-decay vs storage-eviction +precedence unspecified):** Dudley's round-2 §(d) resolves the precedence +question by **eliminating the ambiguity structurally**, not by picking a +winner — his revised formula drops `distance_decay` entirely +(`evict_if: time_since_last_visit(entry) > STORAGE_TTL[rung]`), reasoning +that distance-from-focus is redundant with time-since-visit for a +storage-thrift purpose. This closes the "which axis wins" half of S1 +cleanly — there's only one axis left for storage-eviction, so there's +nothing to arbitrate between. **Stig's round-2 §(c) independently confirms +the same simplification** (his Tier 2 spec: "(2a) time-since-last-visit +sweep" + "(2b) LRU-capacity sweep," two independent triggers, no distance +term at all) — two independently-designed specs converging on dropping +`distance_decay` is good evidence the simplification is correct, not just +convenient. + +**What S1 asked for that's still not fully delivered: the actual revisit-cost +number.** I computed ~80ms/canvas (74.8ms derive + 5.41ms PNG-encode, +T-1179) × N canvases for a realistic revisit scenario — neither round-2 cache +spec states an expected N (how many canvases a typical explored-then- +abandoned body accumulates) or runs that arithmetic explicitly. Stig's spec +does now name eviction granularity precisely (per-entry, the composite key), +which was the other open half of S1 — so the mechanism is fully specified, +just not the end-to-end cost consequence of a real revisit. **Downgrading +S1 from SERIOUS to NOTE**: the mechanism gaps (granularity, axis precedence) +that made this SERIOUS are now closed; what's left is a tuning-pass number +(expected revisit cost under realistic N), which both specs correctly treat +as implementation-time tuning rather than an architecture question — Stig's +own text explicitly defers the STORAGE_TTL/budget constants as "a tuning +pass once real play-pattern data exists, not an architecture call I'm +locking here." That's the right call; I'd only ask that the ticket plan +name "measure actual revisit-pattern cost against a real play session" as a +post-ship validation step, not a pre-filing blocker. + +**S5 (disk cache schema/version tag):** already covered in item 2 above — +`tyre-round2.md` §(a.9) resolves this completely and correctly, citing my +finding by name. Checked Stig's round-2 `IndexEntry` schema +(`stig-round2.md:206-223`) to confirm it does NOT yet carry the tag itself +(it doesn't — the schema shown is pre-amendment) — but Tyre's text is +explicit that "it lands in Stig's `FileAccess` spec at implementation," i.e. +this is correctly sequenced as a record-level requirement now, schema +addition at implementation, not a gap in Stig's document. **Fully resolved.** + +## Summary of the addendum + +| Original finding | Status after round 2 | +|---|---| +| B1 (population-scale cliff survey never run) | **STILL OPEN** — tyre-round2.md §(a.7) correctly gates the sparse-list cost claim on it running, but it has not been run yet (checked: no new measurement doc, Dudley's round-2 text doesn't mention it). Still the one thing I'd block filing on. | +| B2 (seed-chaining fork) | **RESOLVED**, verified against source (`district_profile.rs:1705-1732` — the nested-call pattern is already what every measured ns/cell number exercises). One precise interview-2 confirmation item remains (does Jeroen's phrasing match this reading), stated above — downgraded to NOTE-level follow-up, not a blocker. | +| S1 (evict-then-revisit spike, axis precedence) | **Mechanism fully resolved** (both specs converge on dropping distance_decay, granularity now per-entry). Downgraded to NOTE: the actual end-to-end revisit-cost number is still unstated, correctly deferred as a tuning-pass/post-ship-validation item by both specs. | +| S2 (courses density coverage gap) | **STILL OPEN** — not addressed in any round-2 document. Deep-step + high-river-density combination remains unmeasured. | +| S3 (D-226(d) letter vs purpose, cache accumulation) | **RESOLVED** — tyre-round2.md §(a.8), same framing, same risk actor (`AtlasAgentInterface`), concrete structural fix (per-body deep-rung cache cap). | +| S4 (sim-state phase cadence unspecified) | **Not directly addressed by name in round 2** — araminta-round2.md §(a) does much of the adjacent work (TTL-split field assignment, static-vs-sim-state reasoning per field) but does not name a phase-cadence number or range. Still open, lower priority than B1/S2. | +| S5 (disk cache version tag) | **RESOLVED** — tyre-round2.md §(a.9), complete and correct fix. | +| N1 (decimal-MB units) | Unchanged, still just a note-in-record-text item. | +| New (envelope demux collision attack) | **CLEARED** — attacked directly, design holds. | +| New (cache-format cutover / stale-key orphaning) | **CLEARED on inspection** — no prior disk cache existed to orphan; downgraded from a worry to a documented non-issue. | +| New (Option B's "zero asterisks" has one small inherited-baseline asterisk on the Quarter row) | **NEW NOTE** — Quarter's rate is same-band-confirmed, not this-session-remeasured at 8.3M; doesn't change the recommendation. | + +**Net: of the 7 original findings, 4 are now resolved (B2, S3, S5, and S1's +mechanism half), 2 remain genuinely open (B1, S2) and are the two I'd still +flag as blocking/serious for interview 2, and 1 (S4) is partially addressed +but not closed.** The round-2 team's own adversarial handling of my findings +was accurate and thorough — every fix I checked against source or +cross-spec actually holds, not just reads well. + +--- + +# FINAL UPDATE (post-B1-survey) + +Verified `bench_population_survey_all_committed_bodies` +(`server/tests/hydrology_equilibrium_bench.rs:273-`) directly against source, +the same way I verified everything else in this pass — not taking the +coordinator's summary or the measurement doc's prose at face value. + +**Confirmed genuine, not a repeat of the original defect:** the new bench's +`par_iter` closure calls `load_heightmap_png(path, &body_id, 0.3)` **inside** +the closure, once per discovered path — a distinct file read, distinct +`body_id` (derived from the parent directory name), and distinct +`small.sea_level` (read from each PNG's own embedded `sea_level` metadata, +not a shared constant) per iteration. This is structurally the opposite of +`bench_parallel_273_bodies_at_512x256`'s defect, where `gj1c_512x256()` was +called once *outside* the `par_iter` block and every iteration reused the +same `elev`/`sea_level` binding. The new bench also carries a `heightmap_paths.len() +> 200` assertion (guards against a silently-empty discovery walk) and a +built-in determinism spot-check (`assert_eq!` on `cliff_edge`/ +`channel_depth_scaled` for the top-by-carving outlier, re-solved a second +time) that would fire the moment any future body actually carves. Basin +arithmetic checks out (22,270 total = 21,240 overflow + 1,030 endorheic, +confirmed by direct addition). The measurement doc's `cliff_edge`/ +`carved_cells` filter logic in the bench (`BasinOutcome::Overflow` with +`channel_depth_scaled[b.spill_cell] > 0`, `cliff_edge.iter().filter(|&&c| c)`) +matches what the original T-1177 module actually computes — not a redefined +or looser carving criterion invented to force a zero result. + +**One precision note, not a discrepancy:** the coordinator's/Tyre's cited +"31–187 basins/body" range is the min/max **within the printed top-15 table** +(GJ1075c=31, GJ1e-m1=187), not a separately-stated population-wide min/max — +the measurement doc's own prose illustrates variety with a different example +pair (GJ103c=40 to GJ1e-m1=187) rather than stating the true extremes. Both +are consistent with the same data; this is not an error, just noting the +"31" figure comes from reading the table rather than an explicit population +min/max statement in the doc. Doesn't affect the finding. + +**B1 → RESOLVED.** The population-scale claim now rests on real, distinct +evidence (267 real bodies, each independently solved, real terrain, real sea +levels) rather than one body's cost profile standing in for the population. +The zero-carving result is stronger than what was being claimed before my +original finding (100% zero vs. "structurally rare, one-body-verified"), and +the epistemic framing in `tyre-round2.md` §(a.7) is honest about the +remaining limit ("not observed yet ≠ cannot occur," carving arithmetic +stays unit-proven for future bodies) rather than overclaiming "impossible." +This is exactly the outcome I'd hoped running the survey would produce, and +it did. + +## Scorecard, final, for interview 2 + +Of the seven original findings plus the addendum's follow-ups, **two remain +open** and are the only items I'd carry into interview 2: + +1. **S2 — courses-inclusive derive cost has zero measured coverage at the + deep-step (Block/Tile spacing) × high-river-density combination.** The + one courses-inclusive-at-real-density number that exists (T-1178 + Cross-check 1: 195.0 ns/cell, 18 courses, District spacing, one body) is + accurate where measured (confirmed clean in my original pass), but the + deepest step's own realistic-canvas bench (T-1154's 83K-cell/17ms number) + explicitly excludes courses entirely by construction (H2's + `rect_window_replica` is courses-empty), and no round-2 document adds a + deep-step/river-dense bench. Not blocking (the existing <5% District-cap + bound has enough headroom that a multiple-density increase likely stays + affordable), but it is the one combination in the whole measured appendix + with literally zero data point, at exactly the spot (deepest step, most + individually-resolvable tributary geometry) where the excluded cost is + least likely to stay proportionally small. **Ask: run one more bench — + deep-step canvas on a body selected/authored for high course density — + before or shortly after filing; not a blocker for filing itself.** + +2. **S4 residual — sim-state TTL phase-cadence is still an unnamed + variable.** Araminta's round-2 §(a) fully resolved the *field-assignment* + half of my original finding (which fields are sim-state vs static, and + why, stated per-field rather than inferred from name — `glaciation`/ + `flooded` sim-state, `temp_dc` static, with a genuinely sharp + non-arbitrary argument for each). What remains unresolved is the + **numeric half**: D-228's "computed once per phase" water-height/ + glaciation clock model doesn't state a phase duration, and nothing in + round 2 names one (or a per-body-class range) for sizing the sim-state + plane's actual TTL. The determinism substrate is sound (confirmed clean + in my original pass) — this is purely a missing tuning input, the same + category Stig/Dudley both correctly deferred their own STORAGE_TTL/ + budget constants as "a tuning pass once real data exists." **Ask: either + fold this into the same tuning-pass deferral (acceptable, and consistent + with how the other unresolved constants in this workshop were handled), + or if Jeroen wants a concrete number before filing, that's a Dudley/ + Araminta follow-up question at interview 2, not something the measured + appendix can answer as-is.** + +Everything else — B1 (population survey, now run and resolved), B2 +(seed-chaining, resolved and verified against source), S1 (eviction +mechanism, resolved; end-to-end cost number correctly deferred as tuning), +S3 (D-226(d) accumulation gap, resolved with a structural fix), S5 (cache +version tag, resolved with a complete fix), N1 (units note), N2 (informational, +now moot — all round-2 files exist), N3 (clean bill, unchanged) — is closed +or was never a live concern. The workshop's round-2 handling of every +finding I could verify against primary sources held up exactly as +documented, with no gap between what was claimed fixed and what the code/ +doc actually shows. diff --git a/docs/workshops/body-map-viewer/tyre-round1.md b/docs/workshops/body-map-viewer/tyre-round1.md new file mode 100644 index 000000000..4f3f3ee2d --- /dev/null +++ b/docs/workshops/body-map-viewer/tyre-round1.md @@ -0,0 +1,469 @@ +--- +title: "Body Map Viewer — Tyre's Round 1 Position" +description: "Governance amendment ratification, gridunit↔D-243 reconciliation, determinism boundary, and cliff ruling — against the measured ①–⑤ appendix" +type: workshop +status: active +workshop: body-map-viewer +created: 2026-07-25 +--- + +# Tyre — Round 1 Position + +*cracks knuckles* — the measurements came back clean. All four of the things I +flagged as "could kill this" in the implications pass (hydrology cost, derive +throughput at scale, wire size, deep-step cost) landed as **GO**, not +**maybe**. That changes my tone here: I'm not hedging architecture around +unknowns anymore, I'm ratifying specific amendment text against specific +numbers. Four questions, in the brief's order. + +--- + +## 1. Governance delta — ratified as concrete amendment texts + +The implications pass named five amendment targets. I said "surface as a +workshop question, don't pre-decide" for the sharpest one (gridunit↔D-243) — +that's §2 below. The other four are ready to write as filing text now; the +measurements didn't change their *direction*, only confirmed their necessity +and gave me the numbers to cite. + +### 1a. D-166 corollary — repoint, don't delete + +**Current text (2026-07-21 amendment):** *"Display at every rung samples the +derivation at canvas resolution (the ladder is a continuous field, not a +stack of fixed display rasters)."* + +**Amendment text (proposed):** + +> **Amended 2026-07-25 (body-map-viewer workshop — stepped ladder).** The +> zoom mechanism is superseded from continuous to **stepped** (T-1143 ruling +> 3, itself superseded — see §1b). The corollary's intent — no magnified +> interpolation of a coarser composite; no undersampling below the source +> floor — is **preserved, re-expressed per-step**: each step's data canvas is +> a derivation sampled at that step's native gridunit spacing (never coarser +> than the step's own floor, never finer than the display's own tunable +> ratio, measurement ④'s 1×1-to-5×5 px/gridunit band). Display *within* a +> step holds that step's canvas at a fixed, texel-exact ratio (measurement ⑤: +> upload cost 0.03–4.6 ms across every canvas size tested, never a frame-budget +> risk) — the "continuous field" framing is retired as literally false (it never +> was continuous once server-side per-step canvases replaced client +> `_canvas.scale`), but the guarantee it protected is intact, just discretized. +> The one honest new gap this creates — magnification of the *held* canvas in +> the interval just before a step-cross — is a **named, bounded** exception, +> not a silent violation: see §1e below and Red Flag 1. The corollary's +> anti-oversample/anti-undersample clause now reads: *"a gridunit is never +> derived coarser than its step's floor and never displayed finer than the +> tunable ratio; between-step magnification is bounded by construction to one +> step interval and is the ladder's only sanctioned display-time scaling."* + +### 1b. T-1143 ruling 3 — superseded for zoom transport + +**Amendment text (proposed):** + +> **Superseded 2026-07-25 (body-map-viewer workshop).** T-1143 ruling 3 +> ("continuous cursor-anchored zoom... wheel-zoom carries the view from the +> orbital frame down through regional granularities continuously") is +> superseded for the **transport mechanism**: zoom is **stepped** — scroll +> clicks through discrete gridunit-spacing levels, one server-canvas fetch +> per crossed step boundary. What SURVIVES from ruling 3, unchanged: (a) +> cursor-anchored centering, (b) edge-scroll pan, (c) the HARD condition that +> a full zoom-out resets to the canonical orbital frame and location. A +> client-side morph/tween between held step canvases is an *investigation +> item* — cosmetic interpolation of already-arrived data while the next +> step's canvas is in flight, never re-derivation, never a second source of +> positional truth (see §3, determinism boundary). Ruling 3's "progressive +> capped-density tiling riding the generalized `district_window` carrier, no +> forced tagged-envelope migration" clause is **also superseded** — see §1d, +> the tagged envelope is no longer avoidable and this workshop names it +> expected scope rather than something to keep dodging. + +### 1c. `select_rung` / T-1143 §2–§5 rung model — replaced + +**Amendment text (proposed):** + +> **Superseded 2026-07-25.** The shipped `WindowGranularity` enum +> (Quarter/District/Region, coverage-ceiling-walked via `select_rung` keyed +> on `MAX_COVERAGE_M`) is replaced by the **stepped gridunit ladder**: a +> discrete step index (§3, the step ladder) selects gridunit spacing +> directly, not via a coverage-walk over a fixed rung enum. Rungs-as- +> derivation-granularity survive conceptually — every step still names an +> absolute-metre spacing — but the selector is gone, replaced by "which step +> is the viewport currently on." `resolve_window_granularity`'s whitelist +> discipline (T-1150: finer-than-district integer multiples only, unknown +> values fall back, never trusted from the wire) is the right *shape* for the +> step index's own validation and should be reused, not reinvented. + +### 1d. D-226 T-1124 §2 windowed-family ceiling — tagged-envelope migration triggered + +**This is the one measurement ④ makes unambiguous, not marginal.** The +brief's own framing ("likely triggers") undersells what T-1179 found: PNG +per-field, the *best* candidate encoding measured, is **21× the ~30 KB +ceiling at the smallest step canvas (330K gridunits) and 563× at the +largest (8.3M)**. Bit-packed and RLE are worse (55×–1,731×). There is no +encoding trick in the measured set that gets a step canvas anywhere near +"exactly one windowed field" territory — this isn't "the ceiling might need +raising," it's "the payload is two to three orders of magnitude past a +ceiling sized for a 4,096-cell window." T-1156's precedent (river skeletons +riding the whole-body family, explicitly *not* triggering the migration) and +T-1170's precedent (courses riding the windowed payload as *content*, not a +*new field*, also not triggering it) both worked because they stayed inside +the existing one-field shape. A step canvas cannot — its cell count alone +(330K–8.3M vs. the 4,096-cell design point) is off by two to three orders of +magnitude before encoding is even considered. + +**Amendment text (proposed):** + +> **Amended 2026-07-25 (body-map-viewer workshop, superseding T-1143 ruling +> 2's "no forced tagged-envelope migration").** The windowed-family ceiling's +> **purpose** — prevent uncorrelated concurrent windowed queries needing +> per-field request correlation — survives untouched. Its **current +> mechanism** (exactly one windowed field, `district_window`, sized for a +> ≤4,096-cell served window) cannot carry a step canvas: measurement ④ +> (T-1179) shows the best available encoding at 21×–563× the ~30 KB reference +> across the three measured canvas sizes (330K/2.07M/8.3M gridunits), with no +> encoding in the measured set closing that gap. The **tagged-envelope +> migration D-225 deferred** (`decode_inbound`'s shape-based demux → a +> required marker field / tagged envelope) is hereby the sanctioned path: +> the step-canvas payload becomes its own tagged message type on the existing +> IPC stream (not a second socket, not a `district_window` growth), landing +> D-225's 2026-06-12 constraint ("the next inbound message type must +> introduce a tagged envelope") on its intended target. Framed as **expected +> scope, executing a planned deferral** — tier: challenging but doable, not a +> risk. The ceiling's rule ("exactly one windowed-query field") is +> re-scoped: it now governs the *legacy* `district_window` carrier only (kept +> alive for whatever windowed traffic doesn't migrate — TBD in round 2's wire +> contract), while the tagged step-canvas envelope is a **new, separate** +> carrier the ceiling rule does not apply to by construction (it isn't a +> second windowed field on the old shape — it's the new shape). + +### 1e. river-courses-t1170.md carrier rule — survives, terminology repointed only + +No change to the rule's substance. I said in the implications pass this was +"robust to the pivot... cut on the axis the server relocation doesn't touch" +and the measurements don't disturb that: rule (iii) — "rung-indexed invented +detail rides the windowed payload" — still holds; "windowed payload" now +names the **tagged step-canvas envelope** (§1d) rather than the legacy +`district_window` field, but the *content* discipline (courses are payload +content, not a new query field) transfers unchanged. Ruling (i)/(ii) (skeleton +rides whole-body; continuous per-metre fields ride the window) are also +unaffected — Region-rung skeleton still needs the presentation-frame clip +T-1170 §3g describes until Region itself steps onto the ladder (§3, step +ladder — Region likely IS the global/step-0 rung, worth confirming in round +2's synthesis with Araminta). + +**Amendment text (proposed):** none required beyond a terminology note — +"windowed payload" in `river-courses-t1170.md` is glossed as "the per-step +data canvas payload (formerly `district_window`, now the tagged step-canvas +envelope, §1d)" at the top of that document, so a future reader isn't +misled by the retired field name. + +--- + +## 2. Gridunit ↔ D-243 reconciliation — the load-bearing call + +Called this "the single most load-bearing vocabulary reconciliation" in the +implications pass and refused to pre-decide it. Now I have the numbers. +**Ruling: gridunit spacing snaps to D-243 rung spacings. It does not float +freely per viewport/step.** + +### The argument + +**Why free-floating breaks first:** if gridunit spacing were `viewport_px ÷ +tunable`, two different monitor resolutions (or the same monitor at a +different step-transition moment) would derive **different absolute-metre +sample spacings at the same step**. That's not a cosmetic difference — the +derivation is `derive_at_metres(seed, body, position, spacing)`, and D-227's +whole determinism/cache-validity model rests on *the same inputs producing +the same output forever*. A gridunit spacing keyed to the requesting +client's viewport size means the server-side cache key must include viewport +dimensions (or the cache is wrong for the next client), which contaminates +D-227's "evictable cache, never source of truth, always valid on +recompute" cleanliness with a presentation-layer parameter. It also breaks +the client-side retention premise (premise 9): "a canvas for a fixed seed +never changes" stops being true if the canvas's own grid depends on the +window you happened to have open when you fetched it. + +**Why snapping is free, not a compromise:** D-243's ladder already gives us +six absolute-metre rungs below the elastic seam (voxel 1 m, chunk 64 m, +block 128 m, quarter 512 m, district 2,048 m, region ~205 km) — measurement +③ (T-1154) independently confirms block and tile spacing cost the *same* +per-cell rate as District/Quarter (~1.8 µs/cell single-thread, no cutoff +discount below block). There's no derivation-cost reason to invent a +seventh, viewport-relative spacing value — the fixed rungs are already cheap +at every depth the ladder needs. **A step's gridunit spacing IS one of +D-243's absolute rungs** (or, at the very deepest step, the D-243 voxel/tile +itself, 1 m — already the ladder's own floor). Snapping doesn't cost +anything the ladder wasn't already going to pay. + +**What DOES float, and this is where premise 5's "tunable" language was +doing real work:** the **px-per-gridunit display ratio** — 1×1 ideal down to +~5×5 acceptable — is the free parameter, and it's a client-side presentation +concern, not a derivation-side one. A 4K monitor and a 1080p monitor +requesting the *same step* get the *same absolute-metre canvas* (same +gridunit spacing, same derived content, same cache entry, shareable) — they +just display it at different px-per-gridunit ratios, which is a pure texture +scale-to-viewport operation, not a re-derivation. This is precisely the +canonical-vs-viewport question (red flag 3) wearing a different hat: the +canvas's *metre extent* should be sized to cover the largest viewport the +step needs to fill at that step's D-243 spacing (measurement ③'s "realistic +deep-step canvas," 216×384 m at 1 m spacing, 82,944 cells, 17 ms parallel — +already computed as a fixed metre-extent shape, not a viewport-px-shape), +and any monitor smaller than that extent just doesn't need all of it. + +### Concrete ruling + +- **Step → D-243 rung mapping is 1:1 or a documented fixed multiple**, + chosen in round 2's step-ladder synthesis (question 3 below feeds it). No + step invents a spacing D-243 doesn't already name. +- **Gridunit = "the per-step data cell," and its metric size is always a + D-243 absolute-metre value** — the vocabulary clarification I flagged in + the implications pass resolves cleanly: gridunit is not a *new* spatial + unit, it's a *role* ("the cell a given step's canvas is sampled at") + played by whichever D-243 rung that step is pinned to. At the deepest step, + gridunit and tile (D-243's voxel) coincide exactly — which is exactly what + "10 px per tile" in the outline already assumes. +- **The px-per-gridunit ratio (1×1 to 5×5) is a display-side tunable**, + decoupled from spacing. It governs texture-to-viewport scale and canvas + *extent* sizing, never canvas *spacing*. +- **D-243 gridunit vocabulary entry (filing text):** + + > **gridunit (D-243 amendment, additive).** The per-step data-canvas cell. + > Not a new spatial rung — a *role* name for whichever D-243 absolute-metre + > rung (voxel through region) a given zoom step is pinned to. Gridunit + > spacing is always one of the ladder's fixed metre values; it never floats + > with viewport size or display resolution. The **display ratio** + > (screen-px per gridunit, 1×1 ideal, ≥5×5 acceptable per the body-map-viewer + > workshop) is the free, client-side, viewport-dependent parameter — kept + > terminologically and architecturally separate from gridunit spacing + > itself. + +This is the "that's actually easier than it sounds" moment I was hoping for +going in: the reconciliation isn't a compromise between two systems, it's +recognizing gridunit was never a competing unit — it's D-243 wearing a +per-step hat, with exactly one genuinely free parameter (display ratio) that +was never a spatial-ladder concern in the first place. + +--- + +## 3. The determinism boundary — where the client may interpolate without creating a second truth + +D-010 principle 4 (server owns simulation/derivation state) and D-227 +(derive-don't-store, pure function of seed+position) together draw this line +sharply, and the T-1170 course-invention ruling already demonstrates the +right shape in miniature — I want to generalize it rather than invent a new +rule. + +### The rule + +**The client may interpolate/tween *only* values that are already fully +resolved server-side and arrived on the wire as concrete endpoints or +control geometry.** It may never invent a sample the server hasn't computed, +and it may never smooth/blend *across* a boundary the server treats as +discontinuous (a step boundary, a rung's own truncation floor, a cliff edge). + +Concretely, three buckets: + +**(A) Legal client interpolation — presentation-only, zero determinism risk.** +- **Step-cross morph/tween** (premise 3, explicitly an investigation item): + animating from the held (arrived, resolved) coarser-or-adjacent canvas + texture toward the newly-arrived finer canvas texture. Both endpoints are + server truth; the client is blending two *already-true* images for visual + continuity during the fetch window. This is a pure `Tween`/shader + cross-fade — no new sample is invented, and the final resting state is + always the server's canvas, never a lerped hybrid held as "the" data. +- **River/road tweening between wire-carried control points**: T-1170's + Stage A/B course points ship as `Vec<(i32,i32)>` world-metre polyline + stations. The client draws a smooth curve *through* those points (spline + interpolation between server-given anchors) — this is cosmetic curve + fitting of a fully-specified polyline, not invention of new geometry. The + server has already decided where the river bends; the client is just not + drawing it as a jagged polyline of straight segments between stations. + This is exactly what T-1175 (per-vertex tapering, Polygon2D strips) is + scoped to do, and it's legal by this rule as written today. +- **Texture-to-viewport scaling** (the display ratio from §2): resampling a + fixed-spacing canvas to fit a monitor's px-per-gridunit ratio is a GPU + presentation resize, not a derivation. NEAREST/LINEAR filter choice + (already precedented per-rung in `_filter_for_granularity_v2`) lives + entirely in this bucket. + +**(B) Illegal client interpolation — would create a second source of truth.** +- **Inventing a sample the server never computed** — e.g., a client-side + guess at what lies between two arrived gridunits at a *finer* spacing than + the server sent (upsampling terrain detail client-side). This is exactly + the T-1143 error class (magnified interpolation of a coarser composite) + the whole workshop exists to kill. The corollary amendment (§1a) forbids + it structurally — there is no client code path that samples "between" + gridunits at invented resolution, because the render baseline draws the + canvas 1:1 (or at the tunable ratio) via RTT, texel-exact by construction. +- **Smoothing across a step boundary**: blending step-N's canvas with + step-N+1's canvas as if they were one continuous field (rather than + cross-fading two discrete textures per bucket A) would reintroduce exactly + the composite-magnification problem — the boundary between two + differently-sampled canvases is real (different gridunit spacing on each + side), and pretending otherwise fabricates data. +- **Client-side course/terrain re-derivation** — the T-1170 ruling already + rejected this explicitly (1a: "Client-side GDScript invention is rejected + on the determinism surface... a byte-exact two-language mirror... is a + standing liability"). This generalizes to every invented-detail field: + hydrology (channel_depth/cliff_edge, §4 below), coast crinkle, vegetation + massifs — all CPU/Rust server-side only, per the workshop's own locked + premise 2. GPU is presentation only (the c2 pre-empt from the implications + pass, unchanged). + +**(C) The one genuinely gray case — flagged, not resolved here.** Named- +feature *label placement* (settlement name text, POI glyphs) when two +adjacent gridunits at a coarse step both nominally "contain" the same named +feature — is a small client-side de-duplication/placement heuristic (avoid +drawing the same city name twice at a seam) legal presentation logic, or does +it risk client and server disagreeing about "which gridunit owns this +feature's label"? My read: legal, IF the server always ships feature +*identity* (a stable id) rather than the client inferring identity from +proximity — then client-side de-dup is deciding *where to draw one already- +identified thing*, not deciding *what things exist*. This is Araminta's +named-feature-encoding call (question 1) to confirm in round 2; I flag it +here because it's adjacent to the determinism boundary but is really a wire- +schema question (does the id ship) more than an interpolation question. + +### Why this generalizes cleanly + +Every "legal" case in bucket A shares one property: **the interpolation +input set is closed and server-supplied** (two full textures, a finite list +of wire-carried points). Every "illegal" case in bucket B shares the mirror +property: **the interpolation would need to invent a new sample outside +that closed set**. That's the same test D-227 already applies to +derive-don't-store generally ("cache is a bonus, never truth") — I'm not +proposing a new principle, I'm stating the existing one precisely enough +that "tween rivers/roads" (Jeroen's outline phrase) has an unambiguous +yes/no per case instead of being a vibe. + +--- + +## 4. Cliff/multi-height ruling — building on Dudley's measurement + +T-1177's finding changes the shape of this decision more than I expected +going in. Two things I flagged as open in the implications pass are now +answered by data, not architecture judgment: + +1. **The representation question** (min/max height pair vs. dominant height + + channel-depth + cliff-edge flag) is **settled by what the solver actually + emits** — `HydrologyResult` already produces `channel_depth_scaled` and + `cliff_edge` per cell, directly, no re-derivation needed. Dudley's own + argument against min/max (loses the transition *shape* — is the drop a + point or does it span the gridunit — and requires synthesizing two heights + from one number the solver never computed) is correct and I'm not + relitigating it. **Ruling: dominant-height + channel_depth + cliff_edge is + the adopted representation.** This was "the first option red flag 4 + named" and it wins because it's the one that requires zero invented + derivation downstream — the wire field is a direct carry of solver output. + +2. **The frequency question changes the stakes.** I framed red flag 4 as "the + data model needs to represent gorges because hydrology produces them." + T-1177's honest finding — **zero carved cells across all three + production-scale benches (512×256, 768×432, 8.3M)**, with a clearly + articulated structural reason (priority-flood's true-minimum-rim property + makes single-basin carving mathematically impossible; genuine carving + needs a rare two-independently-sealed-basins-plus-single-cell-corridor + geometry) — means gorge cells are a **rare, not routine**, occurrence at + the scales this system actually runs at. That doesn't remove the need for + the field (the model must be able to represent what the algorithm + produces when it does fire, and "structurally rare" isn't "never" — real + planetary heightmaps beyond the one body sampled may differ), but it + substantially weakens the wire-cost argument for worrying about it: this + is a mostly-zero, sparse, `#[serde(default)]`-safe optional field, not a + dense per-cell cost driver. + +### Ruling, stated for filing + +**Phase-4 Atlas scope, not deferred to Phase-5.** Two reasons this call +goes *into* scope rather than out: + +- **It's nearly free once carved.** The representation is a direct carry of + existing solver fields — no new derivation, no new invented-detail + category, and (per the rarity finding) usually a no-op (`channel_depth: + 0`, `cliff_edge: false` for the overwhelming majority of gridunits). This + isn't "cliffs are hard, defer them" — it's "cliffs are already computed, + carrying them costs approximately nothing." +- **Deferring it to Phase-5 in-world geometry with only a "steep" + classification would silently violate D-227's determinism-critical + framing** of hydrology as a *settled equilibrium*: if the Atlas map shows + a lake with a smooth shoreline where the settled hydrology solver actually + computed a carved overflow channel, the map is showing something other + than what "settled hydrology" means. The whole point of red flag 4 in the + implications pass was "hydrology can't be settled as producing gorges + while the data model can't represent one" — now that the solver + demonstrably does produce them (even if rarely), showing a flattened + version on the map is exactly that contradiction, just less frequently + triggered. + +**Wire shape (for Araminta's round-1 payload schema, ④):** + +``` +elevation: existing dominant-height field, unchanged for the non-gorge case +channel_depth: u16 (quantized), 0 for the overwhelming majority of gridunits +cliff_edge: bool (or a bit folded into an existing classification byte) +``` + +**What this does NOT resolve, staying honest about scope:** +- Whether `channel_depth`/`cliff_edge` ride the *tagged step-canvas envelope* + (§1d) as two more per-cell array fields, or get folded into a bit of an + existing field — Araminta's wire-schema call, not mine, but I'd steer + toward "new arrays" over "steal a bit" given how sparse the data is (a + sparse-friendly encoding, e.g. PNG-per-field per measurement ④'s own + finding that DEFLATE handles near-constant fields exceptionally well — + `morphology`/`vegetation` at 0.0% run density in the RLE table are the + same shape channel_depth's near-all-zero distribution would have — + actually *helps* this field cost less than its raw byte width suggests). +- The client-side render treatment (cliff-face style transition vs. gradient) + — Stig's call, not architecture's; I note only that `cliff_edge` gives the + map-art function exactly the signal it needs to make that choice without + inventing anything (bucket A interpolation, §3: the client is styling a + server-supplied boolean, not deciding where a cliff is). +- Whether Block/Tile rungs (where gorge-scale features would actually be + visible at all, per T-1154's spacing) are the step ladder's floor or an + intermediate step — that's the step-count/step-ladder synthesis, §3 of the + brief's expected outputs, round 2 territory. + +**One honest caveat to carry into round 2 (Troblum's adversarial pass should +stress this):** T-1177 sampled hydrology on exactly one real body (GJ1c) plus +two synthetic gradients. "Structurally rare at production scale" is a +strong, well-argued finding, not a survey across the ~273-body population. +If a body with genuinely different terrain character (e.g., heavily +tectonic, high-relief, many small nested basins) produces carving far more +often than GJ1c did, the "usually a no-op" cost argument softens — still +correct architecturally (the field costs nothing when zero, something +reasonable when nonzero), but the "rare" framing shouldn't be read as +"provably rare across the whole body population." Doesn't change my ruling; +does mean I wouldn't cite "rare" as load-bearing for anything beyond the +wire-cost argument above. + +--- + +## Summary — my four calls, plainly + +1. **Governance delta**: five amendment texts drafted and ready to file + (§1a–1e) — the D-166 corollary repoint, T-1143 ruling-3 supersession, the + `select_rung` replacement, the tagged-envelope migration as **triggered, + not merely likely** (measurement ④ makes this unambiguous — 21×–563× over + the ceiling), and the T-1170 carrier rule surviving with a terminology + gloss only. +2. **Gridunit ↔ D-243**: **snaps, does not float.** Gridunit is a per-step + *role* played by a D-243 absolute-metre rung, never a viewport-derived + spacing. The only genuinely free parameter is the display ratio + (px-per-gridunit), which is a client-side presentation concern cleanly + separated from spacing. +3. **Determinism boundary**: client may interpolate only within a closed, + server-supplied input set (two arrived textures, wire-carried control + points) — never invent a sample outside it, never smooth across a step or + rung-truncation boundary. Three concrete legal cases, two illegal, one + gray case flagged for Araminta. +4. **Cliff ruling**: dominant-height + `channel_depth` + `cliff_edge`, + **in Phase-4 Atlas scope** (not deferred to Phase-5) — nearly free given + T-1177's direct-carry representation and rarity finding, and deferring it + would contradict "settled hydrology" itself. Caveat: rarity is measured + on one real body, not the population — don't over-lean on it for anything + beyond wire cost. + +Feasible across the board. The measurements did their job — nothing here is +extrapolated, and nothing surfaced a reason to walk back the "architecturally +cleaner than what ships today" read from the implications pass. The tagged- +envelope migration is the one genuine chunk of new work or this list, and +it's exactly the size we already knew it would be. diff --git a/docs/workshops/body-map-viewer/tyre-round2.md b/docs/workshops/body-map-viewer/tyre-round2.md new file mode 100644 index 000000000..267bad6a4 --- /dev/null +++ b/docs/workshops/body-map-viewer/tyre-round2.md @@ -0,0 +1,1233 @@ +--- +title: "Body Map Viewer — Tyre's Round 2 Synthesis" +description: "Final amendment texts (record-ready), tagged-envelope governance disposition, step-ladder tables, and the deprecation-sweep draft (Expected Output 2)" +type: workshop +status: active +workshop: body-map-viewer +created: 2026-07-25 +--- + +# Tyre — Round 2 Synthesis + +*cracks knuckles* — interview 1 upheld both my load-bearing round-1 calls +(gridunit snaps to D-243; cliffs Phase-4) and added two rulings I now fold in: +the **map-time TTL-split with the storage/staleness axes distinction** (Jeroen's +new eviction amendment), and **cliffs as a sparse `CliffSegment` feature list** +(aligning my round-1 "new fields, not stolen bits" text with Araminta's sparse +shape and Jeroen's ruling). This document is the filing package: paste §(a) into +`governance/decisions/architecture.md` with only ID-claiming left, run the §(d) +sweep at wrap-up. + +Four sections, in the coordinator's order: (a) final amendment texts, (b) +envelope mechanics + the ceiling-rule disposition (with Dudley on wire/serving), +(c) step-ladder tables (with Dudley on costs), (d) deprecation-sweep draft. + +--- + +## (a) AMENDMENT TEXTS — FINAL, record-ready + +These incorporate every interview-1 ruling. Each is written to paste directly +into the named record. **A new D-record** (the body-map-viewer render +architecture) is claimed separately at filing — these are the *amendments to +existing records*; the new-record text lives at the end of this section as +§(a.0) so the whole package is in one place. + +### (a.0) NEW RECORD — body-map-viewer render architecture (claim a D-id at filing) + +> **D-NNN — Body-map-viewer stepped render architecture (supersedes the +> T-1143 continuous-ladder mechanism).** *Claimed [date]. Domain: +> architecture.* +> +> - **Decision:** The Atlas map handler is rebuilt on three locked premises: +> **(1) content determination is server-side** — the CPU/Rust server answers +> "what is at this world coordinate at this zoom step" as a per-step data +> canvas; the client never invents geometry. **(2) The client is a map-art +> function** — it colorizes, styles, and annotates the server's canvas via a +> render-to-texture terrain layer (texel-exact, drawn at the display ratio) + +> an unscaled screen-space sibling layer for vector annotations. **(3) Zoom is +> stepped** — discrete gridunit-spacing levels, one server-canvas fetch per +> crossed step boundary, cursor-anchored, with edge-scroll pan and a hard +> full-zoom-out reset to the canonical Global (rung-0) body-surface frame. +> - **The step ladder** is **six levels** — the **Global** map opener (rung 0) +> plus **five fixed metre rungs** (Dudley's Option D, every fixed rung +> measured): **Global (rung 0, body-surface view) → Region (204.8 km, rung 1) +> → District (2,048 m, rung 2) → Quarter (512 m, rung 3) → Block (128 m, +> rung 4) → Chunk (64 m, rung 5, deepest)**. **Rung 0 (Global) is the map +> opener and is *variable*-extent, not a fixed metre rung** (Jeroen, interview +> 2 correction): it is the whole body surface at **one gridunit per region**, +> so its canvas *is* the body's region grid — variable per body (~19K gridunits +> on an Earth-class body; the D-243 elastic seam made visible, since region +> count floats per `body_radius_km`). **Region (rung 1) is the largest +> *fixed*-size rung** — viewport-sized like every fixed rung below it, *not* +> the always-kept tier (that is Global). Tile/voxel (1 m) +> is **dropped from the Atlas ladder** (Jeroen, interview 2: *"the actual tile +> level rung seems unusable, maybe replace with 64"* — a 10-px-per-tile +> full-screen view is ~192×108 m of ground, which is in-world viewport content, +> Phase-5's scope, not an Atlas map); voxel is reserved for Phase-5 in-world +> rendering. Every fixed rung's **gridunit spacing is one of D-243's +> absolute-metre rungs** — never a viewport-derived spacing (see the D-243 +> gridunit amendment, §(a.4)). **Deepest bottom-out rule: 1 screen px per 64 m gridunit, no +> magnification margin** — the old "10 px per tile" margin does *not* carry +> over (it existed because a 1 m unit is sub-readable at 1×1; a 64 m gridunit +> is already a legible map feature — a city block / short street — so chunk +> uses the plain fixed-canvas budget with no display-ratio-sized exception, +> §(c)). +> - **Canvas policy:** **rung 0 (Global) is the sole canonical, always-keep +> tier** — the body-surface region-grid canvas, one gridunit per region, +> variable-extent per body. D-226(d)-legal by construction: at one gridunit per +> region it is *coarser* than the region grid, never a sub-region +> metre-resolution whole-body derivation. **Every fixed rung — Region (rung 1) +> through Chunk (rung 5) — is viewport-sized and evictable** (Region included: +> it is the largest fixed rung, not the canonical tier), bounded to a fixed +> canvas-pixel budget (not a per-monitor echo), which keeps the deep +> ladder legal under D-226(d) **per request** by construction (a viewport-sized +> chunk-spacing canvas is never a whole-body metre-resolution derivation on any +> single request). **The D-226(d) prohibition is stated as a per-request / +> per-derivation constraint, not an aggregate-storage constraint** (§(a.8), +> Troblum S3) — the letter and the purpose are both held server-side; the +> client-cache *accumulation* path is closed by the deep-rung retention cap in +> the Cache bullet, not left to practical improbability. +> - **Wire:** the per-step canvas rides a **tagged-envelope** carrier (§(a.5), +> §(b)) — the ceiling-forcing payload D-225 deferred, now built. Dense +> classification fields ship PNG-per-field (measurement ④'s winner on size and +> speed); sparse feature lists (`courses`, `cliffs`) stay MessagePack-native. +> - **Cache:** three tiers, cheapest-first — client in-memory LRU → client +> disk-backed `FileAccess` store (self-cleaning TTL for sim-state entries) → +> server (D-203-shaped resident global tier + TTL(detail, time, distance) for +> finer rungs). Determinism (D-227) makes every geometry tier a pure cache, +> never a source of truth. **Staleness and storage are distinct eviction +> axes** (§(a.6)). **The server global (rung-0) tier is the body-surface +> region-grid canvas — one gridunit per region, ~19K gridunits on an +> Earth-class body — and costs ~8.85 MB PNG-encoded (27.61 MB raw) across the +> entire real ~267-body population** (Dudley, measured: 4,825,615 total cells, +> avg 18,073/body; largest GJ325Ac at 25,200 cells/47.4 KB, smallest a moon at +> 253 cells; Earth-class reference 195×97 = 18,915, matching the ~19K framing). +> This supersedes the earlier ~174 MB figure (which mis-priced a fixed 4K-class +> Region-*spacing* canvas per body, ~440× too many cells). At ~8.85 MB the +> keep-always tier is **trivially process-resident, not merely disk-safe** — +> the "always keep global" policy gets *easier*, not harder, under the +> rung-identity correction. Convention stated: **SI decimal MB**, so the figure +> isn't "corrected" against a `du`-reported MiB number later (Troblum N1). +> **Derive cost — the "snappy after first calc" number (Dudley, measured on +> real per-body extents, not extrapolated):** ~16–21 ms single-thread per body +> (~830–910 ns/cell over ~18K cells) — trivially interactive on a fresh +> Atlas-open. The tier **populates lazily, once per body, on first Atlas-open** +> via the D-206 background queue (the same pattern every other cached layer +> uses), so the ~4.0 s all-267-summed single-thread figure is a sanity ceiling +> only, never paid synchronously in one batch (same shape as T-1177's +> per-body-vs-summed hydrology framing). **Two client-cache hardening requirements, both +> stated in the record, both implemented in Stig's `FileAccess` spec:** (i) a +> **per-body deep-rung retention cap** (a maximum resident chunk/block-spacing +> tile count or disk quota per body, independent of the rung-0 retention floor) +> — this is the structural ceiling that closes the D-226(d) accumulation gap +> (§(a.8)); (ii) +> every persistent cache entry carries a **schema/version tag** (the game's +> `project.yaml` version string or a `generator_sha`-style stamp), checked at +> read time — a mismatch is treated as a cache miss and re-fetched, never +> decoded (§(a.9), Troblum S5). +> - **Rationale:** the T-1143 error class (client compensating for a +> zoom-scaled canvas — `_canvas.scale`, `_zs`/`_zs_stroke`/`_zs_ring_radius`, +> the line-rasterizer floor) is dissolved at the root: there is no +> zoom-scaled canvas anymore, so per-call compensation cannot occur. Every +> pre-workshop cost gate (①–⑤) came back GO with no extrapolation. +> - **Cross-reference:** D-166 (cascade + the amended zoom-ladder corollary), +> D-226 (the ceiling this migrates past; D-226(d) the deep-step boundary), +> D-227 (derive-don't-store — the cache's discipline), D-243 (the rung ladder +> gridunit snaps to), D-225 (the tagged-envelope migration this executes), +> D-010 (determinism — server-owns-derivation, client-art-function). +> - **Dissent:** None. + +### (a.1) D-166 corollary — repoint (owns between-step magnification) + +Paste as an amendment on D-166, after the 2026-07-21 amendment: + +> **Amendment (2026-07-25, body-map-viewer workshop — stepped ladder +> supersedes continuous).** The 2026-07-21 corollary sentence — *"Display at +> every rung samples the derivation at canvas resolution (the ladder is a +> continuous field, not a stack of fixed display rasters)"* — is **repointed, +> not deleted.** The zoom mechanism is now **stepped**: the server generates +> one data canvas per discrete zoom step, sampled at that step's native D-243 +> gridunit spacing. The corollary's *guarantee* survives, re-expressed +> per-step: **a gridunit is never derived coarser than its step's own rung +> floor, and never displayed finer than the display-ratio tunable** (1×1 ideal, +> ≥5×5 px/gridunit acceptable — measurement ④). The "continuous field" framing +> is retired as literally false (it never was continuous once server-side +> per-step canvases replaced the client `_canvas.scale` model). The corollary +> now reads: *"each zoom step's data canvas is a derivation sampled at that +> step's native gridunit spacing; display within a step holds that canvas at a +> fixed, texel-exact ratio; between-step magnification of the held canvas is +> bounded to one step interval and is the ladder's only sanctioned display-time +> scaling."* **This amendment owns the one honest new gap it creates:** in the +> interval just before a step-cross, the held (coarser or adjacent) canvas is +> magnified to fill the new step's viewport for the fetch duration — the exact +> operation the original corollary was written against, now a **named, bounded +> exception** rather than a silent violation. The bound is joint in two knobs: +> step count (more steps → smaller per-step magnification factor) and the +> display ratio. The fetch interval is short — measurement ⑤ shows step-canvas +> texture upload at 0.03–4.6 ms across every size, never a frame-budget risk, +> and measurement ④ shows an uncached 330K-gridunit step derives + encodes +> server-side in ~80 ms — so the magnified hold is a double-digit-millisecond +> transient, not a resting display state. **Ladder extent (interview 2, +> §(a.8b)):** the 2026-07-21 amendment's *"down to tile scale"* phrasing is +> narrowed — the Atlas ladder bottoms out at **chunk (64 m)**, not tile (1 m); +> tile/voxel is Phase-5 in-world content, not an Atlas rung. The per-step +> derivation guarantee above applies to every rung from region down to chunk. + +### (a.2) T-1143 ruling 3 — superseded for zoom transport + +Paste as an amendment on the D-226 record's T-1143 ruling block (or on D-166, +wherever the T-1143 rulings are anchored — they live in the D-226 amendment +chain): + +> **Superseded 2026-07-25 (body-map-viewer workshop).** T-1143 ruling 3 +> ("continuous cursor-anchored zoom... wheel-zoom carries the view from the +> orbital frame down through regional granularities continuously") is +> superseded for the **transport mechanism**: zoom is **stepped** — scroll +> clicks through discrete D-243-rung gridunit-spacing levels, one server-canvas +> fetch per crossed step boundary. **Survives unchanged:** (a) cursor-anchored +> centering, (b) edge-scroll pan, (c) the [HARD] condition that a full +> zoom-out resets to the canonical Global (rung-0) body-surface frame and +> location. A +> client-side morph/tween between held step canvases is an **investigation +> item** (cosmetic interpolation of already-arrived textures while the next +> step's canvas is in flight — never re-derivation, never a second positional +> truth; see the determinism-boundary ruling, §(a.7)). **T-1143 ruling 2** +> ("progressive capped-density tiling riding the generalized `district_window` +> carrier — no forced tagged-envelope migration") is **also superseded**: the +> tagged-envelope migration is now triggered (§(a.5)) — measurement ④ shows the +> step-canvas payload at 21×–563× the windowed ceiling across the three +> measured sizes, off by two to three orders of magnitude before encoding is +> even considered, which no additive-param reuse of `district_window` can +> carry. + +### (a.3) `select_rung` / rung-model — replaced by the step index + +Paste as an amendment note (on the same T-1143-rulings block): + +> **Superseded 2026-07-25.** The shipped rung selector — `select_rung()`'s +> unified per-rung coverage-ceiling walk keyed on `MAX_COVERAGE_M` (Quarter → +> District → Region → tile-mode mosaic; `atlas-zoom-ladder-t1143.md` §6 "Final +> model, round 3") — is **replaced by a discrete step index** into the stepped +> gridunit ladder. Rungs-as-derivation-granularity survive conceptually (every +> step still names an absolute-metre D-243 rung), but the *selector* is no +> longer a coverage walk over viewport extent — it is "which step the viewport +> is currently on," advanced/retreated one notch per scroll. `select_rung`, +> `MAX_COVERAGE_M`, the coverage-ceiling walk, and the `compute_tile_grid()` +> whole-body Region-tile mosaic that composed the beyond-Region extent are +> retired. The whitelist-validation *discipline* from `resolve_window_granularity` +> (T-1150: finer-than-district integer multiples only, unknown values fall back, +> never trusted from the wire) is the correct shape for the **step index's own +> validation** and should be reused, not reinvented. + +### (a.4) D-243 — gridunit vocabulary entry (additive) + +Paste into D-243's item (5) vocabulary block, or as a labelled amendment: + +> **Amendment (2026-07-25, body-map-viewer workshop) — `gridunit` added to the +> locked vocabulary (additive; no rung changes).** **gridunit (at zoom step):** +> the per-step data-canvas cell. **Not a new spatial rung** — a *role* name for +> whichever D-243 absolute-metre rung the Atlas ladder pins a given zoom step +> to. **At every *fixed* rung, gridunit spacing equals one of the ladder's fixed +> metre values and never floats with viewport size or display resolution** — a +> fixed step's gridunit is a D-243 metre rung, full stop. **The Atlas ladder's +> fixed rungs run from Region (~205 km, rung 1) down to chunk (64 m, rung 5, the +> deepest Atlas rung); tile/voxel (1 m) is NOT an Atlas gridunit** — it is +> dropped from the ladder (§(a.8b), Jeroen interview 2) and reserved for Phase-5 +> in-world rendering. **The one exception to fixed-metre snapping is rung 0 +> (Global)**: it is the *variable* map-opener, one gridunit per region, so its +> gridunit is a whole region (~205 km) but its *canvas extent* floats with the +> body's region count (the D-243 elastic seam). Global's gridunit is still not +> viewport-derived — it is the region grid, a fixed property of the body — so the +> "never floats with viewport/display" discipline holds; only the body's own +> region count varies. So +> the outline's *"10 px per tile"* bottom-out is **superseded** by **"1 screen +> px per 64 m gridunit, no magnification margin"** (Dudley's chunk bench): the +> Atlas bottoms out at the chunk rung at the workshop's own 1×1 ideal ratio, +> with *no* extra magnification, because a 64 m gridunit is already a legible +> map feature (city block / short street) — unlike a 1 m unit, which needed the +> 10× margin only to be readable at all (which is exactly the in-world-viewport +> problem chunk is defined to sit above). The **display ratio** (screen-px per +> gridunit — 1×1 ideal, ≥5×5 acceptable) is the free, +> client-side, viewport-dependent parameter, kept terminologically and +> architecturally separate from gridunit spacing: two monitors at different +> resolutions requesting the same step receive the **same** absolute-metre +> canvas (same rung, same derived content, same cache entry — shareable) and +> merely display it at different px-per-gridunit ratios (a GPU +> texture-to-viewport resize, never a re-derivation). This is what keeps the +> derivation cache key free of any presentation parameter (D-227: "a canvas for +> a fixed seed never changes" stays true). **The display ratio is +> step-dependent, not flat across the ladder** (Stig, measurement ⑥ — +> `Image.set_pixel` 77.5 ns/cell flat, so coloring cost is driven by cell count, +> never display density): **1×1 at the deepest steps** (Block/chunk — full +> fidelity where the player is closest to visible detail; chunk-64 m is now the +> deepest Atlas rung, §(a.8b)), **1×1 preferred at the mid steps** +> (Quarter/District) while the realistic canvas stays under ~2M cells, and +> **~5×5 reserved for the shallow rungs** (Region/rung 1 — and the Global/rung-0 +> opener, whose region-grid canvas is tiny in cell count regardless of ratio) +> where the metre *extent* grows large. There is no measured reason to sacrifice +> fidelity where the player is nearest the detail; the ~5×5 fallback is a +> cost-relief valve for the coarse steps only. This changes *nothing* about the +> snap rule or the cache key — the ratio still governs only presentation (canvas +> *extent* sizing + on-screen scale), and per-step ratio selection is a +> client-side policy, never a spacing or derivation input. + +### (a.5) D-226 T-1124 §2 windowed-family ceiling — tagged-envelope migration triggered + +Paste as an amendment on D-226 (extends the amendment chain that already holds +the T-1156/T-1170 carrier notes): + +> **Amended 2026-07-25 (body-map-viewer workshop — tagged-envelope migration +> triggered, superseding T-1143 ruling 2's "no forced migration").** The +> windowed-family ceiling's **purpose survives untouched**: prevent +> uncorrelated concurrent windowed *queries* needing per-field request +> correlation. Its **current mechanism** — "exactly one windowed-query field on +> `AtlasLayerResponse` (`district_window`), sized for a ≤4,096-cell served +> window" — **cannot carry a step canvas**: measurement ④ (T-1179) shows the +> best available encoding (PNG-per-field) at 21×–563× the ~30 KB reference +> across the three measured canvas sizes (330K/2.07M/8.3M gridunits), with no +> encoding in the measured set closing that gap (it is a cell-count gap of two +> to three orders of magnitude, not a codec gap). The **tagged-envelope +> migration D-225 deferred** (2026-06-12 constraint: *"the next inbound message +> type must introduce a tagged envelope"*) is hereby the sanctioned path and is +> executed: the step-canvas payload becomes its own **tagged message type on +> the existing IPC stream** (not a second socket, not a `district_window` +> growth) — the exact shape §(b) fixes. **The ceiling rule is re-scoped, not +> lifted:** "exactly one windowed-query field" now governs the **legacy +> `district_window` carrier only**; the tagged step-canvas envelope is a +> **separate carrier the ceiling rule does not apply to by construction** — it +> is not a second windowed field on the old shape, it is the new shape. The +> ceiling's original job (no uncorrelated concurrent windowed queries) is +> preserved *within* the envelope by the same echo-key/staleness discipline +> `district_window` uses today (`(body, step, center, granularity)` echoed on +> every response, latest-wins, stale-discardable). Framed as **expected scope, +> executing a planned deferral** — tier: challenging but doable, not a risk. +> **Concrete shape (Dudley, §(b)):** inbound is a discriminator-field +> `StepCanvasRequest { step_canvas: bool, body_id, step_index, center, extent, +> min_wl_m }` — a sixth `Inbound` variant extending the existing +> `star_map`/`city_names`/`browse` `ShapeProbe` pattern (the required-marker +> form D-225's 2026-06-12 amendment named); outbound is a **dedicated +> `StepCanvasResponse` message** (not a field on `AtlasLayerResponse`) + +> `SimBridge::send_step_canvas_response`. The legacy `district_window` carrier +> survives **byte-unchanged** — the envelope carries only new step-canvas +> traffic; `district_window` goes cold when its only client (the retired +> `AtlasWindowViewer`) is deleted, then is removed in a follow-up cleanup +> ticket (D-005/D-192 co-ship — no old-client/new-server window). **The ceiling +> rule is therefore RE-SCOPED, not retired:** it keeps governing the live legacy +> carrier verbatim. + +### (a.6) Map-time axis + storage/staleness eviction split (Jeroen's interview-1 amendment) + +New — records interview-1 ruling 2 verbatim-grounded. Paste as an amendment on +D-227 (the derive-don't-store record whose cache discipline this instantiates) +and cross-reference from the new render-architecture record: + +> **Amendment (2026-07-25, body-map-viewer workshop — map time axis; and +> staleness vs storage as distinct cache axes).** The Atlas map shows **current +> state via a TTL-split**: **static geometry** (morphology/elevation/moisture/ +> vegetation/glaciation/height — everything derive-don't-store covers) is +> **cached indefinitely-fresh** (determinism: re-derivation is byte-identical, +> so "stale" is not a concept that applies to geometry); **sim-state planes** +> (frozen/flooded, and any future climate-sim-driven field) are carried as +> **separately-cached short-TTL planes**, re-requested as sim time advances. +> **Two distinct eviction axes (Jeroen, interview 1, verbatim: *"we still may +> also want to evict non global level geometry based on time to save storage +> for planets the player visits but never goes back to"*):** **(1) +> staleness-eviction** applies only to sim-state planes (they genuinely go +> stale as sim time moves); geometry never goes stale. **(2) +> storage-eviction** applies to *all* sub-global cache entries including +> geometry — **every fixed rung, Region (rung 1) through Chunk (rung 5), +> included** — evicted on time-since-last-visit purely as a **storage-budget +> policy**, not because the data is wrong (a re-derive on next visit is +> byte-identical and cheap — measurement ②/③, ~190–220 ns/cell parallel flat at +> every rung). **The global tier — rung 0 (Global, the body-surface region-grid +> canvas), NOT Region — alone is keep-always** — exempt from both axes, the +> resident tier that makes "atlas navigation snappy after first calc" true. +> (Region is rung 1, the largest *fixed* rung; it is viewport-sized and +> storage-evictable like every rung below it — the always-kept tier is the +> variable Global opener above it.) This makes Dudley's `TTL(detail, time, distance)` +> formula precise: the `time` term is *storage-eviction* for geometry (evict a +> long-unvisited body's fine-rung canvases to reclaim disk) and *staleness* +> for sim-state planes (re-fetch a flooded/frozen plane whose sim-time TTL +> expired) — two different reasons an entry leaves the cache, never conflated. + +### (a.7) river-courses-t1170.md carrier rule + cliff sparse-list — terminology repoint + cliff addition + +Paste as a note at the top of `docs/architecture/river-courses-t1170.md` and as +a one-line D-227 cross-reference update: + +> **Note (2026-07-25, body-map-viewer workshop).** The carrier three-way rule +> (Ruling 1c) survives unchanged in substance; "the windowed payload" is +> repointed terminologically to "the per-step data-canvas payload — formerly +> the `district_window` field, now the tagged step-canvas envelope (D-226 +> 2026-07-25 amendment, §(a.5))." Rule (iii) — *rung-indexed invented detail +> rides the windowed payload regardless of geometric kind* — now has a **second +> vector member alongside `courses`: `cliffs: Vec`.** Carved-gorge +> geometry (dominant `elevation` unchanged + per-segment `channel_depth` + +> `cliff_edge`, T-1177's direct solver-output carry) is **rung-indexed invented +> detail** exactly as course geometry is (hydrology settles once per body, but +> the *carved representation drawn at a step* is a windowed-rung concern the +> same way course geometry is), so it rides the step-canvas envelope as a +> **sparse feature list — zero-length when nothing is carved** (measurement ①: +> gorge carving is structurally rare, zero carved cells across all three +> production-scale benches), `#[serde(default)]`, parallel to `courses`. It is +> **not** a dense per-gridunit array and **not** a stolen bit of an existing +> classification byte. **Scope: Phase-4 Atlas** (Jeroen, interview 1, ruling 3 +> — a map showing a smooth shoreline where the settled solver computed a carved +> channel would misrepresent the "settled hydrology" the workshop premised; +> including it is nearly free given the direct-carry representation). **The +> sparse-list encoding's "near-zero occupancy" cost is SURVEY-CONFIRMED +> population-wide (Troblum B1, resolved strong):** the pre-filing population +> survey ran all **267 committed real heightmaps** independently (each body's +> own PNG-embedded sea level, real 512×256 working grid, ~0.86 s total, +> byte-exact determinism across two runs) and found **zero carved-outlet +> basins, zero `cliff_edge` cells, 0/267 bodies with any carving** — across +> genuinely varied terrain (31–187 basins/body), not a flat population. The +> `cliffs` field ships **empty on every currently-committed body**; the +> "near-free" claim is now population-backed, not one-body-backed. **Framing +> held deliberately at "not observed yet ≠ cannot occur":** the carving +> arithmetic (`channel_depth = original − spill_level`, the `cliff_edge` flag) +> stays unit-proven in isolation for any future body whose terrain does produce +> the narrow two-independently-sealed-basins-plus-single-cell-corridor geometry +> carving requires — the field-set and Phase-4 scope stand because the model +> must represent what the solver *can* produce, and the sparse-list is the +> right shape at zero occupancy (empty vec, `#[serde(default)]`) and remains a +> correct shape at any future nonzero occupancy. **Survey scope limit to carry +> into the filed record:** the survey used uniform `moisture_q = 55` (per-body +> climate wiring is out of scope) — this affects only the endorheic/overflow +> *split*, never carving (carving is elevation-gated, never moisture-gated), so +> the zero-carving finding is independent of the climate simplification. Full +> addendum: `measurements/t1177-hydrology.md` "POPULATION SURVEY" section. + +### (a.8) D-226(d) whole-body prohibition — per-request framing + client-cache accumulation cap (Troblum S3) + +Paste as an amendment on D-226 (the item-(d) prohibition), alongside the ceiling +amendment §(a.5): + +> **Amended 2026-07-25 (body-map-viewer workshop — accumulation gap closed, +> Troblum adversarial pass S3).** The item-(d) whole-body prohibition ("chunk/ +> tile/voxel output never appears as a whole-body planetary map layer") is +> **stated explicitly as a per-request / per-derivation constraint, not an +> aggregate-storage constraint.** The stepped ladder's viewport-sizing policy +> holds it **by construction server-side**: no single `StepCanvasRequest` ever +> derives whole-body coverage at sub-Region spacing (the deepest step is a +> chunk-64 m viewport canvas — see §(a.8b) for the tile/voxel-drop narrowing). +> But the client-side persistent cache (Stig's `FileAccess` store) introduces an +> *accumulation* path the letter of the rule does not cover: a systematic +> exhaustive pan at the deepest (chunk) spacing — most plausibly the **D-226 +> item-(4) `AtlasAgentInterface` automated QA sweep**, not normal play — could +> assemble a near-whole-body chunk-resolution artifact as N discrete cached +> files over wall-clock time (the tile-spacing case that produced the ~985 GB / +> 6.6M-tile figure is now moot at the Atlas ladder — tile is dropped, §(a.8b) — +> but the *accumulation principle* still applies at chunk/block spacing, just at +> a coarser resolution and a proportionally smaller aggregate; impossible by +> accident, but not *structurally* prevented). The rule's purpose is about +> information content, not file count, so packaging it as N files does not +> exempt it. **Closed structurally, not by improbability:** a **per-body +> deep-rung client-cache retention cap** (a maximum resident chunk/block-spacing +> tile count or disk quota per body, independent of the rung-0/Global retention +> floor) bounds sub-global accumulation so the aggregate can never approach whole-body +> metre-resolution coverage. The automated QA channel inherits the same cap — +> an agent sweep's client cache is bounded identically, so a QA run cannot +> silently deposit the forbidden artifact in `user://atlas_cache/`. The exact +> cap value is a tuning constant sized in Stig's cache spec at implementation; +> the *requirement that a cap exist* is record-level. + +### (a.8b) D-226(d) floor — partial restore at chunk (Jeroen's interview-2 narrowing of his own interview-1 opening) + +Paste as an amendment on D-226, immediately after the T-1143 ruling 1 block +(which opened item-(d)'s floor for the ladder) — this narrows that opening: + +> **Amended 2026-07-25 (body-map-viewer workshop, interview 2 — the opened +> floor lands at chunk; tile/voxel re-close for the Atlas).** T-1143 ruling 1 +> (2026-07-21) opened item-(d)'s floor "toward block/tile granularity" for the +> Atlas ladder, on the BHAG that *"we set a new BHAG so old restrictions are up +> for debate."* Interview 2 **scopes that opening — it does not reverse it.** +> Jeroen (verbatim): *"the actual tile level rung seems unusable. maybe replace +> with 64?"* Reasoning (matching the workshop's own seam logic): a 10-px-per-tile +> full-screen Atlas view is ~192×108 m of ground — that is **in-world viewport +> content (Phase-5 scope)**, not a map. The Atlas zoom ladder therefore bottoms +> out at **chunk (64 m)**, and **tile/voxel (1 m) output returns to +> never-Atlas-mapped** — exactly item-(d)'s original prohibition, now restored +> for the tile/voxel tier specifically. **Precise post-interview-2 state of the +> floor:** the opening is *used* down to chunk (64 m) — block (128 m) and chunk +> (64 m) are legal Atlas rungs, riding the viewport-sized windowed carve-out +> (§(a.8) per-request framing) — and *re-closed* below chunk: tile (1 m) and +> voxel/subvoxel output are Atlas-invisible, verified by the +> believability/derivation harnesses and shown **in-world in Phase 5**, exactly +> as item (d) originally required. This is a **deliberate narrowing of the +> BHAG's scope, not a walk-back of the BHAG** ("a Reach a character can travel +> through" is unchanged; what changed is that the *Atlas map* stops one rung +> above the in-world viewport, because that's where a map stops being a map). +> The harness-verification path for tile/voxel L5 fill remains primary, as it +> was before the opening — the opening's practical effect is now limited to the +> block and chunk rungs, which is the range where a whole-viewport canvas still +> reads as a *map* rather than a *scene*. + +### (a.9) Client-cache schema/version tag — the one D-192 gap (Troblum S5) + +Paste as an amendment on D-227 (the cache-discipline record) and cross-reference +from D-192: + +> **Amendment (2026-07-25, body-map-viewer workshop — persistent-cache version +> discipline, Troblum adversarial pass S5).** D-192 drops the client/server +> protocol version handshake on the subprocess-co-ship rationale ("the Godot +> client launches the Rust server it was built with — always in sync at +> runtime"). That holds for the **live** wire, and for the tagged-envelope +> migration itself (no live network deployment where an old client meets a new +> server). It does **not** hold for a **disk-backed persistent cache** (Stig's +> `user://atlas_cache/` store, premise 9): a cache file written by game version +> N and read back by version N+1 **survives a game update by construction** — +> exactly the version boundary D-192 assumes away everywhere else. A patch that +> changes the step-canvas payload shape (adds a field — e.g. this workshop's own +> `cliffs` list — or shifts an enum discriminant range per T-1150's "unknown +> values fall back, never trusted from the wire") would have an N-schema cache +> decoded as N+1-schema: a hard decode error at best, a silent +> misinterpretation of old bytes as new fields at worst (the exact class D-225's +> 2026-06-12 tagged-envelope constraint prevents for the *live* wire, now +> extended to the *disk* format). **Requirement (record-level):** every +> persistent client-cache entry carries a **schema/version tag** at write time +> (the game's `project.yaml` version string, or a `generator_sha`-style stamp), +> and a **read-time check**: a tag mismatch is treated as a **cache miss — +> re-fetch, never decode.** This extends the existing cache-index shape +> (`{written_at, last_read_at, kind, size_bytes}` + `schema_version`) by one +> field; it lands in Stig's `FileAccess` spec at implementation, but the +> *requirement* is stated here so it is not discovered as a stale-cache decode +> bug in the wild. Consistent with D-227's "cache never truth, evict → recompute +> always valid" — a version-mismatched entry is just another eviction case. + +### (a.10) Seed-chaining — the "cache-accelerated pure function" model (Jeroen interview-2: he meant consuming coarser output) + +Paste as an amendment on D-227 (this is the one place derive-don't-store meets +"a finer step reads a coarser step's output") — **for Jeroen's final +ratification against his phrase "serves as seed information for the deeper +cascade."** Governance framing is mine; the cost/impl side is +**Dudley-confirmed** (`dudley-interview2-response.md` §2) — his A/B mechanism +distinction is folded in, and the `[DUDLEY]` bracket is resolved. + +> **Amendment (2026-07-25, body-map-viewer workshop, interview 2 — seed-chaining +> resolved as a cache-accelerated pure function).** Jeroen's outline — +> *"[the step canvas] at the same time serves as seed information for the deeper +> cascade"* — is ruled at interview 2 to mean the finer step **consumes the +> coarser step's resolved output** (not merely that the coarser rung is +> independently re-derivable). This is reconciled with D-227 derive-don't-store +> **without weakening it**, via the **cache-accelerated pure function** model: +> +> - **What "consuming coarser output" means precisely — mechanism B, not +> mechanism A** (Dudley §2, load-bearing to prevent a subtle violation). +> **(A) Reading the coarser step's RESOLVED CLASSIFICATION** (e.g. taking a +> District-spacing `morphology_zone` as a shortcut for a Chunk cell's zone) +> is **ruled OUT** — it violates Araminta's categorical-field re-derivation +> rule (§(a.6)/round-1: coarser steps re-derive morphology/vegetation/ +> glaciation as a fresh dominant-mode pick at their own spacing, never inherit +> a different spacing's classification — the same-vocabulary/different-meaning- +> per-scale contradiction). **(B) Reading the coarser step's underlying +> CONTINUOUS PRIMITIVE** (the region/district *baseline blend* at a world +> position) as an input to the finer step's OWN fresh classification is what +> "seed information for the deeper cascade" means — and it is **already the +> shipped pattern** (`invent_primitives`'s district call reads a region +> baseline today). The cache-accelerated model applies to (B): cache the +> continuous baseline blend, not the resolved classification. +> - **The DEFINITION stays pure.** Every gridunit value remains +> `derive(seed, position)` — a pure function of fixed inputs, byte-identical +> on every evaluation (D-010/D-227 unchanged). "Consuming the coarser output" +> does not make the finer step a function of *mutable state*; the coarser +> baseline it consumes is itself `derive(seed, position)` at the coarser rung — +> a pure value, not a stored fact. +> - **The IMPLEMENTATION may read a resident coarser canvas as an +> ACCELERATION,** with a **derive-fresh fallback** when that canvas isn't +> resident. Reading the cached coarser canvas is faster than recomputing it; +> not having it costs a fresh derive of exactly the shape Dudley already +> benched. +> - **This is an OPTIMIZATION, not a SEMANTIC DEPENDENCY — the load-bearing +> governance distinction.** The coarser canvas being read is *itself evictable +> derived data* (a pure function of the same seed). So correctness never +> depends on the cache being warm: if the coarser entry is evicted, the finer +> step derives it fresh and gets **byte-identical** input, producing the same +> output. That is precisely D-227's own standing test — *"eviction → +> recompute, always valid."* A semantic dependency would mean "the finer step +> is *wrong* without the coarser cache" — never true here, because the coarser +> value is re-derivable to the same bytes. The cache is still **never truth, +> only an accelerator**: derive-don't-store holds intact. +> - **Mandatory determinism test (the proof that makes this airtight):** the +> cache-hit path (read resident coarser canvas) and the cache-miss path +> (derive the coarser input fresh) **must produce byte-identical finer-step +> output** — both are the same pure function of the same seed. This is a +> required test, the same shape as T-1170's window-independence invariant. If +> it passes, the two paths are indistinguishable except in speed, which is the +> definition of "optimization, not dependency." If it ever failed, that would +> be a determinism bug (a non-pure derivation), caught here rather than +> corrupting a save (D-227's determinism-is-save-critical clause). +> - **Benched numbers survive as worst-case ceilings (Dudley-confirmed).** Every +> ①②③ cost number measured the *independent re-derivation* path — which is +> exactly the cache-miss fallback. So the measured costs are the **cache-cold +> ceiling**: the shipped system is never slower than the appendix, and is +> faster whenever the coarser baseline is resident. This closes Troblum B2's +> "every cost number prices the wrong architecture" worry — the numbers price +> the *fallback*, and the fallback is the worst case, so they remain +> authoritative as an upper bound. +> - **No chain-reaction on eviction (Dudley-confirmed).** A deep-step (chunk) +> cache miss does **not** walk backward through evicted District/Region cache +> state — each rung's fresh-derive fallback is fully self-contained +> (`derive(seed, position)` takes nothing but those two inputs, by +> construction), so a miss costs *one* fresh derive at that rung's own inputs, +> never a cascade through every evicted ancestor. Troblum B2's chain-reaction +> worry **dissolves** — there is no dependency to chain, because the fallback +> was never built to depend on any other rung's cache state. +> - **Acceleration magnitude is unquantified but not architecture-gating +> (Dudley-confirmed honest caveat).** The saving from reading a cached +> continuous baseline vs. recomputing it (mechanism B) is *very likely real +> but modest* — no bench isolates the region-baseline-blend sub-cost from the +> full `derive_at_metres` call (most of the per-cell cost is the octave-sum +> detail-scatter work mechanism B does *not* propose caching). This is a +> **future implementation-time measurement**, not a gate on the architecture +> ruling: the ①②③ ceiling numbers already answer the load-bearing yes/no, and +> the "expected" (as opposed to worst-case) latency is sized later. +> - **Cross-reference:** supersedes Dudley's round-2 §(b) "independent +> re-derivation, never reads a cached response" *as the exclusive rule* — that +> ruling survives as the **fallback path's** description (function-composition +> on a cache miss); interview 2 adds the cache-read **fast path** (mechanism B) +> on top, governed by the byte-identical-paths test above. + +### (a.11) Lakes — settled-hydrology sourcing of the existing `MorphologyZone::Lake`; endorheic cue via outflow-course presence (FINAL, converged) + +**FINAL — fully converged on both sides.** The endorheic-cue residual is closed: +Dudley picked Araminta's **outflow-course presence** (his doc updated with proper +supersession — the stale 4-state-`water` sections, a crossed-in-flight reply to +Araminta's *original* questions, are marked SUPERSEDED-BY-CROSSING and traceable, +not deleted; his corrected section carries her 8-dense + 2-sparse struct with a +no-dissent confirm). No open clauses remain in this amendment. + +Paste as an amendment on D-227 (the derive-pipeline record — the fill is a +data-source fix, not a wire change) with a D-239 §6 cross-reference. Converged +sources: Araminta's encoding ruling (`araminta-round2.md` §(e), where she verified +Dudley's code-read at source — `district_profile.rs:556-565`, +`generator.rs:1198-1223`, `features.rs:104-105` — and withdrew her earlier +new-field proposal) + Dudley's pipeline ruling + endorheic-cue pick +(`dudley-interview2-response.md`, "LAKES CONVERGENCE", corrected section). + +> **Amendment (2026-07-23, body-map-viewer workshop — lakes sourced from settled +> hydrology; endorheic cue via outflow-course presence).** The outline's lake +> behaviour (settled fill, overflow, endorheic sinks — measurement ①/T-1177) +> reaches the Atlas map with **no new wire field and no vocabulary change**: +> +> - **Lakes ride the EXISTING `MorphologyZone::Lake`** (discriminant 1, already +> in the frozen 17-zone D-239 §6 vocabulary — *not* a new zone; **no new wire +> field for the fill itself**). The gap was never a missing vocabulary entry; it +> was a missing **data source**. Today `derive_morphology_zone` emits `Lake` +> from a crude heightmap threshold (`ocean_fraction_q >= 60`, a bilinear sample +> of `elev < sea_level`), with **zero connection to `HydrologyResult`'s +> settled-equilibrium basins** (the code's own comment already flags this as a +> known gap). **The fix:** `derive_morphology_zone` sources the `Lake` emission +> from settled hydrology instead of the heightmap heuristic, **falling through +> to today's `ocean_fraction_q` heuristic where no basin exists.** +> - **Pipeline sourcing — sample the continuous `filled_scaled` field, NOT +> projected basin-cell membership (Dudley's ruling, adopted).** Water derives +> from `HydrologyResult`'s continuous `filled_scaled` field **sampled bilinearly +> at each gridunit's world position** (the same mechanism `ocean_fraction_q`/ +> `sea_level` already use) — *not* from basin-cell membership projected as a +> discrete 512×256 lookup, which would produce a **blocky, non-refining lake +> edge — exactly the magnified-coarser-composite artifact D-166's corollary +> forbids** (§(a.1)). This is **seed-chaining mechanism B (§(a.10)) applied to +> hydrology exactly as it already applies to `sea_level`**: sample a coarser +> rung's continuous primitive fresh per rung, never re-solve — so the lake edge +> *refines* with zoom (a genuine finer curve at each rung), not a coarse grid +> magnified. Sea vs. Lake is unchanged (both already-existing discriminants; the +> `OpenOcean` tier is untouched). +> - **Static, distinct from the sim-state flooded plane.** Lake basin geometry is +> a settled equilibrium — a pure function of seed + terrain (D-227), cached +> indefinitely-fresh like all geometry. It is **not** the short-TTL +> frozen/flooded sim-state plane (§(a.6)); a lake's *existence and extent* are +> static, while the flooded plane is the sim-time-varying water-height overlay. +> The two do not conflate. +> - **Endorheic-vs-overflow cue = OUTFLOW-COURSE PRESENCE. No wire bit, no new +> zone (FINAL).** The distinction (a lake that drains vs. a closed sink) ships +> as a **client-side read of whether a lake has a river course exiting it**: an +> overflow basin's outlet edge appears in `courses` (T-1170); an endorheic +> basin's does not. **Three reasons this is the right carrier, not a dense +> marker:** +> - **Proportionality.** Endorheic is **1,030 of 22,270 basins = 4.63%** in the +> real population. A per-cell bit under morphology-fold would force either an +> 18th `MorphologyZone` arm — a **D-239 frozen-vocabulary change every consumer +> inherits forever** (including the one-colorizer-family guarantee) — or a new +> wire field, for a **fewer-than-1-in-20 distinction**, when a zero-marginal- +> cost alternative exists. +> - **The work is required regardless.** Overflow lakes **must** show their exit +> rivers to be hydrologically honest (a lake that drains but shows no outlet +> river is simply wrong on the map) — so the basin-outlet→course wiring is +> built no matter what. The endorheic cue is a **side effect of that required +> work, not a feature it pays for.** +> - **Definitionally sound, no misfire case.** Every `Overflow` basin has a +> guaranteed non-empty `outlet_path` (tested: +> `overflowing_basin_has_nonempty_outlet_path`); every `Endorheic` basin has +> none. The presence/absence read is exact — there is no basin class that +> drains-but-shows-no-course or is-closed-but-shows-a-course. +> The **cliffs list is NOT the cue** (T-1177's survey: zero carved cells across +> all 267 real bodies — a cliff-wired cue would fire nowhere). +> - **Honest scope — until the outlet-wiring ticket ships, there is NO visible +> endorheic cue.** `HydrologyResult`'s basin `outlet_path`/`BasinOutcome` is +> **not currently threaded into `RiverNetwork`/`courses`** (D8-network-derived, a +> different computation; T-1177's own scope note confirms `HydrologyResult` is +> prototype-only, unwired to any payload). So the two deliverables are distinct: +> the `Lake` **sourcing fix** (self-contained — the map shows lakes as water) and +> the **outlet-wiring** (the exit-river distinction). **Between them, the map +> shows lakes but not the drains-vs-closed distinction** — state this plainly; +> it is not a gap in the design, it is the honest sequencing. The outlet-wiring +> is a **real, small, additive follow-up ticket** (§(d)), **pre-cleared by +> T-1170 Ruling 7b** (the reserved `TERMINAL` downstream sentinel: *"an interior +> sink becomes an additive drainage-extraction change, not a wire migration... +> the work is in D8 sink retention and lake morphology, and every piece this +> batch builds consumes them without modification"*), and +> `river_course::build_edges` is already **no-op-safe on `TERMINAL`** (Dudley's +> confirm) — additive, not a migration. +> - **Visual treatment:** lakes draw as water at every rung, same colorizer family +> as sea/ocean (one-colorizer-family guarantee, Araminta round-1 §3 — a `Lake` +> discriminant colours identically whether sourced by heuristic or basin); +> endorheic-vs-overflow reads via **outlet-course presence** at the lake shore +> (no distinct fill/texture on the lake body, no new styling rule for Stig — the +> present-or-absent exit river *is* the visual difference). + +--- + +## (b) TAGGED-ENVELOPE MECHANICS — governance disposition (with Dudley on wire/serving) + +**Division of labor:** I own the record framing + the D-225/D-226 §2 disposition +(what happens to the ceiling rule). Dudley owns the wire/serving design (message +shape, demux, legacy coexistence). **Dudley has ruled** (`dudley-round2.md` §(a), +grounded in a direct read of `server/src/bridge/mod.rs`); his three answers are +folded in below and §(a.5)'s bracketed clause is now resolved. + +### The ceiling-rule disposition (mine, final) + +The windowed-family ceiling (D-226 T-1124 §2, [HARD]) has two separable parts, +and only one moves: + +- **Its purpose — "no uncorrelated concurrent windowed queries needing + per-field request correlation" — is preserved in full.** This is why the rule + existed: two independently-in-flight windowed queries would each need their + own request→response correlation, and the hand-rolled shape-demux couldn't + carry that. The tagged envelope *strengthens* this: a tagged message has an + explicit correlation handle by construction (the tag + echo key), which is + strictly better than the shape-demux the ceiling was protecting. +- **Its mechanism — "exactly one windowed field on `AtlasLayerResponse`" — is + re-scoped to the legacy carrier only.** The rule was a *fence around a + hand-rolled demux*, not a first-principles limit on windowed data. Once a + proper tagged envelope exists, the fence is obsolete for anything inside the + envelope: the envelope can carry as many correlated fields as its schema + declares, because the tag makes correlation explicit rather than structural. + +**So the disposition is: the ceiling rule keeps governing `district_window` +(the legacy shape-demuxed carrier) verbatim, and does not extend to the tagged +step-canvas envelope, which is a new carrier with its own explicit correlation +discipline.** This is the same move T-1156 and T-1170 made in miniature (both +kept traffic *off* the ceiling by choosing the right carrier) — the difference +is those two stayed inside the one-field shape because they *could* (skeleton is +whole-body; courses are content of the existing field), whereas the step canvas +*cannot* (cell count alone forces a new carrier), so this one builds the +envelope the others didn't need. + +**D-225's constraint is landed on its intended target.** D-225's 2026-06-12 +amendment said the *next* inbound message type must introduce a tagged envelope +rather than extend shape-based detection. Every carrier since (T-1124's window +params, T-1131's BrowseRequest — "the fifth and last map-shape probe") deferred +it by riding existing shapes. The step canvas is the one that can't ride an +existing shape, so it is correctly the one that pays the deferred cost. This is +a *planned* migration executing on schedule, not an emergency. + +### Three slots — Dudley's rulings, folded in + +Dudley read the actual `decode_inbound`/`Inbound` code and found the +"tagged-marker" pattern D-225 asked for is **already implemented five times** +(each newer inbound shape — `StarMapRequest`/`CityNamesRequest`/`BrowseRequest` +— carries a mandatory boolean discriminator, and `ShapeProbe` already enforces +at-most-one-discriminator). So the migration extends a proven pattern rather +than inventing a new envelope format — which sharpens my governance framing: at +the *code* level this is "doable," and the "challenging" part lives in the +client rebuild (Stig) and the cache/ladder work, not the demux. + +1. **Message shape → discriminator field on the existing stream.** Inbound: a + new `StepCanvasRequest { step_canvas: bool, body_id, step_index, center, + extent, min_wl_m }` as a sixth `Inbound` variant, extending the existing + `ShapeProbe` mutual-exclusivity chain. Outbound: a **dedicated + `StepCanvasResponse` message** (not a field on `AtlasLayerResponse` — D-226 + T-1124 §2 names this exact "dedicated response message by rule" case) + + `SimBridge::send_step_canvas_response`. **My §(a.5) text stands verbatim** — + "its own tagged message type on the existing IPC stream" is exactly a + discriminated inbound variant + dedicated response message; D-225's language + was "a tagged envelope *(or a required marker field)*", and the required + marker field is what ships. The ceiling disposition is identical to what I + drafted. + +2. **Legacy coexistence → `district_window` survives unchanged; the envelope + carries only the new step-canvas traffic.** Dudley's call matches my lean: + no retirement message, no dual-write, no migration of existing windowed + traffic. The legacy carrier keeps working byte-for-byte until Stig's stepped + viewer replaces its only client, at which point it goes cold and gets deleted + in a follow-up cleanup ticket (D-005/D-192 co-ship guarantee means no + old-client/new-server window). **Therefore the ceiling rule is RE-SCOPED (my + §(a.5) primary text), not fully retired** — it keeps governing the live + legacy `district_window` carrier verbatim; the envelope is the new carrier + outside it. The §(d) deprecation table's parenthetical "(if Dudley rules + one-carrier-subsumes-all → SUPERSEDE)" branch does **not** fire — re-scope is + final. + +3. **Rung-0 (Global) and rung-1 (Region) both ride the NEW envelope** — at + `step_index: 0` (Global, the variable body-surface region-grid canvas) and + `step_index: 1` (Region, 204.8 km fixed), **not** the legacy whole-body + `region_grid`/`district_grid` `Option` fields. Dudley's reasoning is the one + I'd have given: putting the opener on the legacy family would force the new + stepped client to speak two response protocols (envelope for the deeper rungs, + old family for the opener) for zero architectural gain — one request shape, + one response shape across the whole ladder is simpler on both sides and is + what premise 8 ("step boundaries = compute-chunk boundaries") implies + uniformly. The only per-rung difference is that rung 0's canvas *extent* is + variable (the region grid) rather than a fixed pixel budget — a field value on + `StepCanvasRequest`, not a different message shape. **Render-architecture + record "Wire" bullet updated accordingly** (§(a.0): the envelope carries every + rung including the Global opener; the legacy whole-body layers keep serving + whatever non-stepped consumers survive the cutover, if any). + +**Response framing (Araminta's relayed dense/sparse question, Dudley answered):** +one flat tagged `StepCanvasResponse` carrying all fields together (six dense +PNG-per-field + `settlement_id` dense + `courses`/`cliffs` sparse +MessagePack-native), **not** a dense/sparse split into separately-tagged +sub-messages. The progressive-paint UX case (terrain first, annotations after) +is real but belongs client-side (paint the RTT layer on decode, defer the +annotation draw a frame) — no wire-protocol complexity for a cost split the +measurements don't show (courses +0.09–0.21 ms, negligible). This matches the +whole-payload-together precedent D-225 set for `Layer1Output`. + +*Filing note: §(a.5) and §(a.0) are now final — no bracketed clauses remain.* + +--- + +## (c) STEP-LADDER TABLES — snap-rule constraints (mine) + costs (Dudley's) + +**INTERVIEW-2 UPDATE — tile/voxel dropped, chunk-64 m is the new deepest rung, +and Dudley's Option D is DELIVERED (every row measured).** Jeroen ruled the tile +rung "unusable" (§(a.8b)); chunk (64 m) is the deepest Atlas rung. Dudley +benched the never-measured chunk rung (`dudley-interview2-response.md` §1) and +built Option D — **this is now the live ladder table** (below). Options A/B/C +are superseded and retained only for the carried-forward reasoning. + +### Option D — the adopted ladder (six levels: Global opener + 5 fixed rungs; chunk-64 m deepest, every fixed rung measured) + +**Six levels after Jeroen's interview-2 rung-identity correction: rung 0 = +Global (the variable body-surface region-grid map opener, always-kept), then five +fixed metre rungs Region→Chunk.** + +| Rung | Level | Spacing | Factor (from prev) | Canvas | Cells | World extent | Derive cost (parallel) | +|---:|---|---:|---:|---|---:|---|---:| +| 0 | **Global** (opener) | 1 gridunit / region (~205 km) | — | **variable — the body's region grid** | ~18K (avg/body; ~19K Earth-class) | whole body | **~16–21 ms/body single-thread** (Earth-class 18,915 cells = 15.9–16.2 ms; largest 25,200 = 20.9–21.3 ms; ~830–910 ns/cell); **~8.85 MB PNG all 267 bodies** | +| 1 | Region | 204.8 km | — (variable→fixed seam) | 3840×2160 | 8.3M | whole body (capped mosaic) | 1,827 ms | +| 2 | District | 2,048 m | 100× | 3840×2160 | 8.3M | 7,864 × 4,424 km | 1,827 ms | +| 3 | Quarter | 512 m | 4× | 3840×2160 | 8.3M | 1,966 × 1,106 km | ~1,827 ms (same-band) | +| 4 | Block | 128 m | 4× | 3840×2160 | 8.3M | 491 × 276 km | ~1,827 ms (measured) | +| 5 (deepest) | **Chunk** | **64 m** | **2×** | 3840×2160 | 8.3M | **245.8 × 138.2 km** | **1,724–1,733 ms (measured)** | + +**Rung 0 (Global) is categorically different from the fixed rungs:** its canvas +is the body's region grid (one gridunit per region), so its *extent* is variable +per body (the D-243 elastic seam — region count floats per `body_radius_km`, +~19K gridunits Earth-class, far smaller than a fixed 4K canvas) and it is the +sole always-kept tier. Rung 1 (Region) is the largest *fixed* rung — 204.8 km +spacing, a fixed 3840×2160 viewport canvas, evictable like every rung below. The +**Global→Region transition (rung 0→1) is the variable→fixed seam**, not a metre +factor; the first metre factor is Region→District (rung 1→2, ÷100). + +**Every fixed row (1–5) is directly measured or in a directly-measured band — +the "zero asterisks" property the old Option B earned by *skipping* chunk, now +earned by *measuring* it.** (Rung 0 is Dudley-measured too: ~16–21 ms/body +single-thread over ~18K region-grid gridunits, ~8.85 MB PNG across all 267 +bodies — trivially cheap to derive and store.) Two things Option D settles that +the old tables couldn't: +- **The deepest-step canvas-sizing special case is GONE.** The old voxel/tile + deepest step needed a display-ratio-sized canvas (216×384 m at 10 px/tile, + *not* the fixed-budget convention every other step used, because the two were + incompatible at 1 m spacing). **Chunk has no such incompatibility** — at 1×1 + px/gridunit the fixed 3840×2160 budget and the "no magnification margin" + bottom-out rule are the *same* rule, so chunk uses the plain fixed-canvas + convention like every other rung. Dropping the special case is a genuine + simplification, a free consequence of Jeroen's own ruling. +- **Deepest-step derive cost rose two orders of magnitude in absolute terms + (17 ms → ~1.7 s) — and that is correct, not a regression.** Chunk's canvas + covers a vastly larger world area (216×384 m → 245.8×138.2 km) at the *same + cell count*, so it costs the same as every other rung's full canvas + (1.7–1.8 s parallel band) — which is exactly what "no special case" means. + 1.7 s parallel is comfortably inside the step-cross tolerance every number in + this workshop uses. Cost still does not gate this rung. + +**Bottom-out rule (record-level, §(a.0)/§(a.4)):** *1 screen px per 64 m +gridunit, no additional magnification margin.* The old 10×-per-tile rule was a +legibility margin for a sub-readable 1 m unit; a 64 m gridunit is already a +legible map feature, so it does not carry over — applying it would needlessly +shrink coverage 10× for no legibility gain. + +**My snap constraints (hard, from the snap rule + interviews 1 & 2, corrected for +the Global/Region rung-identity fix):** +- **Every *fixed* rung's gridunit spacing = one of D-243's metre rungs.** The + Atlas ladder's fixed rungs are region ~205 km (rung 1), district 2,048 m, + quarter 512 m, block 128 m, chunk 64 m (rung 5) — tile/voxel (1 m) is NOT an + Atlas rung (§(a.8b), Phase-5 in-world content). +- **Rung 0 = Global** — the *variable* map opener (one gridunit per region, the + body's region grid; D-243 elastic seam), the whole-body canonical / always-keep + tier. This is the ONE variable-extent level; it is not viewport-derived (its + gridunit is a whole region, a fixed property of the body). +- **Rung 1 = Region** (~205 km) — the largest *fixed* rung, viewport-sized and + evictable like every rung below it; **not** the always-kept tier (that is + Global). +- **Deepest rung (5) bottoms at chunk (64 m)**, at 1×1 px/gridunit, no margin — + fixed 3840×2160 canvas like every other fixed rung (no display-ratio-sized + exception; that oddity died with the tile rung). +- No fixed rung invents a spacing D-243 doesn't name. +- Display ratio (px/gridunit, 1×1 → 5×5) is the free client knob, decoupled + from spacing — it sizes the canvas *extent* and the on-screen scale, never + the spacing. + +--- + +**The material below (Options A/B/C + convention notes) is pre-interview-2 +(voxel-deepest) reasoning, superseded by Option D above.** Retained for the +carried-forward logic (factor-unevenness analysis, the zero-asterisk argument) +— every "voxel 1 m / 216×384 m / 10 px/tile" row is void. + +> **The tables and convention notes below are pre-interview-2 (voxel-deepest) +> reasoning, retained for the carried-forward logic (canvas convention, factor +> analysis, zero-asterisk argument). Options A/B/C and every "voxel 1 m / 216×384 +> m / 10 px/tile" row are SUPERSEDED by Dudley's Option D (chunk-64 m deepest). +> The display-ratio-split *logic* below still holds — it just now lands the +> "display-ratio-sized deepest step" at the chunk rung, with Dudley's chunk +> bench supplying the deepest-row numbers that replace 216×384 m / 17 ms.** + +**Canvas convention (Dudley's correction, adopted — reads at chunk now, not +voxel):** the fixed 3840×2160 canvas-pixel budget holds at every step **except +the deepest** — at the deepest rung the fixed-px-budget convention and the +display-ratio contract are mutually incompatible (a fixed 3840×2160 canvas at +metre-scale spacing covers little ground yet holds many cells, oversized for the +bottom-out display density), so the deepest step is **display-ratio-sized +instead**. *(Pre-interview-2 this was voxel 1 m → 216 × 384 m / 82,944 cells, +measurement ③; post-interview-2 the deepest rung is chunk 64 m and the extent is +Dudley's Option-D chunk display band.)* This is a convention-level split +(fixed-budget shallow, display-ratio deep), not a skeleton-specific one. + +**Per-step display ratio (Stig, measurement ⑥):** the px-per-gridunit ratio is +step-dependent, not flat — **1×1 at Block/chunk** (full fidelity where the +player is nearest visible detail; ⑥ confirms `set_pixel` at 77.5 ns/cell flat, +so cost is cell-count-bound, not display-density-bound), **1×1 preferred at +Quarter/District** while the realistic canvas stays under ~2M cells, **~5×5 at +the shallow rungs (Region and the Global opener)** where extent grows. This +tightens the extent column above: at +the shallow steps the ~5×5 ratio means a coarser realistic canvas than the +fixed 3840×2160 worst-case (fewer cells, cheaper), so the 8.3M/1,827 ms figures +in the tables are the *fidelity-ceiling* cost, not the typical one — the actual +shallow-step canvas at ~5×5 is proportionally smaller. The deep steps stay at +1×1 (the 17 ms/83K deep-step number is already at full 1×1 fidelity). Cost only +*improves* against the table where the ratio relaxes; nowhere does it worsen. + +**Three candidate ladders — Dudley's cost fills folded in** (all costs +MEASURED or same-band-confirmed against ①–④; the one honest gap, chunk at 64 m, +is flagged not guessed): + +### Option A — one-rung-per-step (6 steps, clean 1:1 to D-243) + +| Step | Gridunit rung | Spacing | Factor | Viewport extent | Cells | Derive cost (parallel) | +|---|---|---:|---:|---|---:|---| +| 0 | region | 204.8 km | — (canonical) | whole body (capped-tile mosaic) | 8.3M | 1,827 ms MEASURED | +| 1 | district | 2,048 m | ÷100 | 7,864 × 4,424 km | 8.3M | 1,827 ms MEASURED | +| 2 | quarter | 512 m | ÷4 | 1,966 × 1,106 km | 8.3M | ~1,827 ms same-band (T-1154) | +| 3 | block | 128 m | ÷4 | 491 × 276 km | 8.3M | ~1,827 ms MEASURED (T-1154) | +| 4 | chunk | 64 m | ÷2 | 246 × 138 km | 8.3M | **UNMEASURED** — never benched; cutoff-mechanism reasoning strongly implies same-band, but Dudley refuses to report a number he didn't run | +| 5 | voxel | 1 m | ÷64 | 216 × 384 m (display-ratio) | 83K | 17 ms MEASURED (③) | + +*My concern with A, now confirmed by cost:* chunk (64 m) is the one rung nobody +measured, and it's D-243's *stream/derive unit*, not a natural display rung — +its ÷2 factor is a barely-perceptible zoom notch that adds a fetch and an +asterisk for no visual gain. + +### Option B — skip-chunk (5 steps) — the joint recommendation + +| Step | Gridunit rung | Spacing | Factor | Viewport extent | Cells | Derive cost (parallel) | +|---|---|---:|---:|---|---:|---| +| 0 | region | 204.8 km | — (canonical) | whole body | 8.3M | 1,827 ms MEASURED | +| 1 | district | 2,048 m | ÷100 | 7,864 × 4,424 km | 8.3M | 1,827 ms MEASURED | +| 2 | quarter | 512 m | ÷4 | 1,966 × 1,106 km | 8.3M | ~1,827 ms same-band | +| 3 | block | 128 m | ÷4 | 491 × 276 km | 8.3M | ~1,827 ms MEASURED | +| 4 (deepest) | voxel | 1 m | ÷128 | 216 × 384 m (display-ratio) | 83K | **17 ms MEASURED (③)** | + +*Why B wins, and it's an evidence argument, not a taste one (Dudley + me +converge here independently):* **every row is directly measured or same-band- +confirmed — zero asterisks.** That's a direct consequence of chunk never having +been benched: B is the only one of the three skeletons that doesn't lean on the +unmeasured rung. The one genuinely large factor (block→voxel ÷128) lands at the +*deepest* step where between-step magnification matters *least* (max detail +already; no finer step to under-serve). The ÷100 Region→District factor at +0→1 is the one that maps onto a real seam — see the factor note below. + +### Option C — coarse-doubled (4 steps, skip District too) + +| Step | Gridunit rung | Spacing | Factor | Viewport extent | Cells | Derive cost | +|---|---|---:|---:|---|---:|---| +| 0 | region | 204.8 km | — (canonical) | whole body | 8.3M | 1,827 ms MEASURED | +| 1 | quarter | 512 m | ÷400 | 1,966 × 1,106 km | 8.3M | ~1,827 ms same-band | +| 2 | block | 128 m | ÷4 | 491 × 276 km | 8.3M | ~1,827 ms MEASURED | +| 3 (deepest) | voxel | 1 m | ÷128 | 216 × 384 m (display-ratio) | 83K | 17 ms MEASURED | + +*Why I list but don't recommend C:* **it costs identically to B row-for-row** +(same rungs, same measured rates — District's absence is not a cost saving, +it's purely a pacing choice, Dudley confirms). Its distinguishing feature is the +÷400 Region→Quarter jump at 0→1 — a brutal between-step magnification interval +(the held Region canvas magnified ~400× to approximate a Quarter view before the +fetch lands is the D-166-corollary artifact at its worst). Only viable if fetch +latency makes that magnified hold imperceptible — and since costs don't separate +B from C at all, this is a pure UX-pacing call for Jeroen, not a cost tradeoff. + +### The factor-unevenness note (Dudley's serving answer, folded in) + +**The rung-identity correction makes the serving-seam story *cleaner*, not +messier** — the architectural boundary now lines up with a genuine rung boundary +instead of being buried inside a metre factor. The two seams that matter are now +*distinct*: +- **The variable→fixed / global→sub-global seam is rung 0→1 (Global→Region).** + This is the one real serving-side boundary: Global (rung 0) is variable-extent + and always-kept; Region (rung 1) is the first fixed, viewport-sized, evictable + rung. The cache-tier split (keep-always vs. storage-evictable, Dudley's §(d)) + falls exactly on this seam — cleaner than the old framing, which had the cache + boundary and the ÷100 metre factor collapsed onto one "step 0→1" edge. +- **The metre factors (÷100, ÷4, ÷4, ÷2) are all *within* the sub-global fixed + ladder** (Region→District ÷100, then ÷4/÷4/÷2). None crosses a serving seam — + Region, District, Quarter, Block, chunk are all sub-global geometry, same + eviction policy, same derive path, same flat per-cell cost. The ÷100 + Region→District jump is the largest metre factor but it is *pure UX pacing* + now that it no longer coincides with the cache boundary (which moved up to + rung 0→1). Option D's gentle bottom factors (largest sub-global jump ÷4, deepest + transition a barely-perceptible ÷2) are *strictly better* for the between-step + magnification red flag than any prior option. + +### Recommendation — Option D (adopted) + +The Option D table at the top of §(c) is the adopted ladder: **six levels** +(Global opener + 5 fixed rungs: Region→District→Quarter→Block→Chunk), every fixed +rung measured, no unmeasured rung, no deepest-step canvas-sizing special case, +and the gentlest factor profile of any candidate. It inherits everything the old +Option B earned (measured-not-inferred rows) and removes Option B's one remaining +awkwardness (the voxel deepest-step special case) for free, as a consequence of +Jeroen's own +tile-drop ruling. *(The old A/B/C ranking below is superseded — Option D is what +the chunk-deepest ruling produces, and it dominates all three.)* **The one call +left for Jeroen is confirming the six-level shape** (Global opener + 5 fixed +rungs) — Option D is Dudley's and my joint recommendation, no B-vs-C fork +survives the tile drop. + +--- + +## (d) DEPRECATION-SWEEP DRAFT (Expected Output 2) + +Every DQR record and ticket that conflicts with the decided setup, each with a +disposition one-liner — **ready for the Clerk consistency audit at wrap-up.** +The audit's job is to catch conflicts this list *didn't* name; this list is the +known-conflicts starting set (the governance delta's named items + the round-1 +ticket reconciliations the brief called out). + +### DQR records — amend / supersede + +| Record | Disposition | +|---|---| +| **D-166 corollary (2026-07-21)** | AMEND — repoint the "continuous field, not fixed display rasters" sentence to per-step (§(a.1)); it now owns between-step magnification as a bounded exception. Not deleted. | +| **D-226 T-1124 §2 windowed-family ceiling** | AMEND — re-scope "exactly one windowed field" to the legacy `district_window` carrier only; the tagged step-canvas envelope is a new carrier outside the rule (§(a.5)). Purpose preserved, mechanism narrowed. (Re-scope is **final** — Dudley ruled `district_window` survives, envelope carries only new traffic, §(b); the SUPERSEDE branch does not fire.) | +| **D-226 T-1143 ruling 2** ("no forced tagged-envelope migration") | SUPERSEDE — migration now triggered (§(a.2), §(a.5)). | +| **D-226 T-1143 ruling 3** (continuous cursor-anchored zoom) | SUPERSEDE for transport (stepped); cursor-centering/edge-scroll/full-reset survive (§(a.2)). | +| **D-226 T-1143 §6 `select_rung` / `MAX_COVERAGE_M` coverage-walk model** | SUPERSEDE — replaced by the discrete step index (§(a.3)). | +| **D-226 T-1143 ruling 1** (item-(d) floor opened "toward block/tile") | AMEND / NARROW (interview 2, §(a.8b)) — the opening now lands at **chunk (64 m)**; tile/voxel (1 m) re-closes for the Atlas (Phase-5 in-world content). A deliberate scoping of the BHAG, not a reversal. | +| **D-243 item (5) vocabulary** | AMEND (additive) — add `gridunit` as a role name for a rung; add the display-ratio-vs-spacing distinction; **the Atlas ladder bottoms at chunk (64 m), tile/voxel not an Atlas rung** (§(a.4), interview 2). No D-243 rung *definitions* change — the metre ladder is intact; only which rungs the *Atlas* uses is narrowed. | +| **D-225 (2026-06-12 tagged-envelope-deferral constraint)** | SATISFY (note, not supersede) — the deferred constraint is now executed by this workshop's envelope; annotate D-225 that its "next inbound message type must be a tagged envelope" constraint is discharged here (§(a.5)). | +| **D-226 item (d) whole-body prohibition** | AMEND (Troblum S3 + interview 2) — state the prohibition as a **per-request / per-derivation** constraint, not aggregate-storage; add the per-body deep-rung client-cache retention cap (§(a.8)); and record the floor's partial-restore at chunk (§(a.8b) — the accumulation case is now chunk/block spacing, tile/voxel Atlas-invisible again). The prohibition's *substance* survives; this hardens it and re-narrows its floor. | +| **D-227 (derive-don't-store)** | AMEND (additive) — (1) the map-time TTL-split + staleness-vs-storage eviction-axes distinction (§(a.6)); (2) the persistent-cache schema/version-tag requirement (§(a.9), Troblum S5); (3) **the seed-chaining "cache-accelerated pure function" model** (§(a.10), interview 2) — a finer step may read a resident coarser canvas as an *optimization* with derive-fresh fallback, staying inside derive-don't-store because the coarser value is itself re-derivable to identical bytes (optimization, not semantic dependency); (4) **the lake sourcing fix** (§(a.11)) — `MorphologyZone::Lake` sourced from `HydrologyResult`'s continuous `filled_scaled` (mechanism B), a derive-pipeline data-source change, **not** a D-239 vocabulary change (the frozen 17-zone set is reused verbatim — see the D-239 note below). All four are additive; derive-don't-store's core is unchanged and, for (3), *reinforced* (the byte-identical-paths determinism test is D-227's own eviction-validity property applied to a fast path). | +| **D-192 (no client/server version handshake)** | CROSS-REF note (Troblum S5) — annotate that the co-ship "always in sync" guarantee does not extend to the persistent disk cache; the schema/version tag (§(a.9)) is where that boundary is handled. Not a change to D-192's live-wire reasoning, which stands. | +| **river-courses-t1170.md carrier rule (Ruling 1c)** | AMEND (note) — terminology repoint ("windowed payload" → step-canvas envelope) + add `cliffs` as rule (iii)'s second vector member (§(a.7)). Substance unchanged. | + +**Records confirmed SURVIVING UNTOUCHED** (state explicitly so the Clerk audit +doesn't over-rewrite): D-010 (four principles — the server relocation +*reinforces* principle 4); D-166 cascade + phase gating (still Phase-4 Atlas); +D-169/D-170 (implant UI — the map component still lives in the implant Atlas +app, occludes gameplay via HudGroups); D-223 (`atlas_city_names` — Araminta's +settlement-name lookup rides it unchanged); D-239 (the 17-zone morphology + +vegetation/glaciation vocabulary — frozen across every step per Araminta's +one-colorizer-family ruling; **the §(a.11) lake fix explicitly does NOT touch it +— it reuses the existing `MorphologyZone::Lake` discriminant, changing only its +data source, and the endorheic cue rides `courses` precisely to avoid an 18th +zone**). *(D-226 item (d) moved out of this list — it is +now AMENDED per Troblum S3, above.)* + +### Tickets — cancel / re-scope / repurpose + +| Ticket | Current status | Disposition | +|---|---|---| +| **T-1176** (design discussion: revise the Atlas map render mechanism) | in_progress | CLOSE as delivered — this workshop *is* its output; the render-architecture D-record + amendments are the deliverable. Mark done at filing, referencing the new D-record. | +| **T-1158** (decompose `atlas_window_viewer.gd` — canonical-frame + rung-reselection cluster) | backlog | CANCEL with supersession note (Stig's round-1 call, confirmed): the canonical-frame state machine T-1158 wanted to extract is exactly what changes shape under stepped zoom (no continuous zoom floor, no `_canonical_fit_zoom()`); extracting code about to be deleted is waste. The new component's three-piece decomposition (input/pan, step-cross orchestration, two draw layers) is decided fresh in the implementation ticket, not by reviving T-1158. Note: "superseded by body-map-viewer render architecture (D-NNN); decomposition folded into the new component's implementation." | +| **T-1175** (Atlas map-reading polish — river/nature presentation, single large pass) | backlog | RE-SCOPE + note the styling-engine dependency: T-1175's per-vertex river tapering/width-grammar work lands on the **screen-space annotation layer** (§ Stig round-1) via a Polygon2D-strip mechanism (`draw_polyline` can't taper). **The c1 blocker is now cleared** — measurement ⑥ landed (Stig: `Image.set_pixel` 77.5 ns/cell flat) and the c1 call resolved **CPU-first** (terrain raster colorized CPU-side; shaders deferred, not a ladder precondition). T-1175's tapering is a screen-space-annotation-layer concern independent of the terrain c1 call, so it depends only on **(a) the new annotation layer landing** — the ⑥-gated dependency is discharged. Not cancelled; re-homed onto the new component, unblocked. | +| **T-1157** (redesign atlas visual-capture goldens for the zoom ladder) | backlog | RE-SCOPE as the new mechanism's verification story: the dead-goldens problem T-1157 targets is a direct consequence of the `AtlasViewer`/orbital-mosaic split that the stepped model *collapses* (global zoom = step 0, not a separate code path — Stig round-1). Re-scope to "capture harness for the stepped render architecture" — goldens key on (body, step) canvases, not the retired continuous-zoom frames. Keep, don't cancel: the capture harness is more needed than ever (it's the eyeball-check substrate for every step canvas). | +| **T-1174** (batch vs window derive paths sample different world positions for the same DistrictPos) | backlog | KEEP, unchanged in intent — this is a *derivation-correctness* bug (two derive paths disagreeing on the same position) that is **orthogonal to the render pivot** and survives it: the tagged envelope still needs batch and window derive to agree cell-for-cell (it's the same `derive_at_metres` correctness both carriers depend on). If anything the stepped model *raises* its priority (more derive paths sharing the same position math). No supersession; note only that it now blocks the step-canvas serving path too. | +| **T-1153** (continuous cursor-anchored zoom ladder in the client) | done | Its shipped CODE is superseded (the continuous zoom transport, `_view_zoom` float, progressive cross-rung refinement machinery) — the ticket stays `done` (it delivered what it was scoped for), but its *code* is on the `_canvas.scale` retirement path (below). No ticket action; flag its shipped surfaces in the retirement-path note so the implementation ticket knows what it's replacing. | +| **T-1152** (derived planetary rung — progressive capped-density tiling; AtlasViewer heightmap texture retires) | done | Its `compute_tile_grid()` whole-body Region-tile mosaic is superseded by the **rung-0 Global canvas** (the body-surface region grid, one gridunit per region — the map opener; there is no beyond-Region mosaic, and Region itself is now rung 1, a viewport-sized fixed rung, not the top). Ticket stays `done`; its mosaic code is on the retirement path. The heightmap-texture retirement it *did* land (the `reliefmap.png` display path) stays retired — that survives the pivot (heightmaps were already demoted to derivation input by the D-166 corollary). | +| **T-1153/T-1152 test suites** | — | The zoom-ladder goldens and rung-selection tests those two stories shipped are **retired with their code** (they test continuous zoom + coverage-walk selection + the Region-tile mosaic — all superseded). T-1157's re-scoped capture harness (above) is their replacement. Concretely: the `select_rung`/coverage-walk unit tests, the continuous-zoom seam tests, and the `compute_tile_grid` mosaic tests are deleted in the same change that deletes the code they cover; new step-index + step-canvas tests land under the re-scoped T-1157. Do NOT leave the old goldens asserting against retired frames (the T-1157 dead-goldens problem, repeated). | +| **NEW — lake morphology sourcing** (§(a.11) converged core) | new | CREATE — source the existing `MorphologyZone::Lake` emission from `HydrologyResult`'s continuous `filled_scaled` field (bilinear per-rung sampling, seed-chaining mechanism B), replacing the `ocean_fraction_q >= 60` heightmap heuristic as the authoritative `Lake` trigger with heuristic fallback. Self-contained; no wire change, no vocabulary change. Blocks nothing; consumes T-1177's `HydrologyResult` (currently prototype-only). | +| **NEW — hydrology basin-outlet → D8 wiring** (§(a.11) endorheic follow-up) | new | CREATE — thread a `HydrologyResult` overflow basin's resolved `outlet_path`/spill point into `RiverNetwork`/`courses` as a D8 downstream continuation (so an overflow lake's outlet edge appears in `courses`, an endorheic basin's absence reads as "no course"). **Pre-cleared additive by T-1170 Ruling 7b** (reserved `TERMINAL` sentinel; `river_course::build_edges` already no-op-safe on `TERMINAL`) — not a wire migration. This is the deliverable the endorheic-vs-overflow distinction ships on: the cue is **outflow-course presence** (§(a.11) FINAL — no bit, no zone), so until this ticket lands the map shows lakes but not the drains-vs-closed distinction. Required regardless (overflow lakes must show exit rivers to be hydrologically honest); the endorheic cue is a side effect of that work. | + +### The `_canvas.scale` retirement path (explicit, for the implementation ticket) + +Retired **in the same change** that lands the new two-layer component (Stig +round-1 §1; sequenced so close inspection is never stranded, the T-1138 +discipline): +- `_canvas.scale` / `_canvas.position` transform model + `_apply_transform()` + (continuous scale) — replaced by the step-canvas anchor (a fixed per-step + texture draw, not a continuous scale node). +- `_zs()` / `_zs_stroke()` / `_zs_ring_radius()` compensation family in + `atlas_window_geometry_nature.gd` — deleted wholesale; the unscaled + screen-space annotation layer makes them structurally impossible to need. +- `select_rung()` + `MAX_COVERAGE_M` + `compute_tile_grid()` (T-1152/T-1153 + code) — replaced by the step index. +- The `AtlasViewer`/orbital-mosaic-vs-window two-code-path split — collapses to + "the Global opener is rung 0" (one viewer, one path; global zoom is the rung-0 + body-surface canvas, not a separate code path). +- `_view_zoom` float + `_canonical_fit_zoom()` continuous fit — replaced by the + discrete rung index + the rung-0 (Global) canonical frame. + +**Surviving client surfaces** (Stig round-1, not on the retirement path): +`_filter_for_granularity_v2()` (NEAREST/LINEAR per rung still a real RTT +draw-time question), `atlas_window_tile_set.gd`'s LRU *shape* (right skeleton +for the new client cache, plus the two-tier eviction), and the +compositing/legend/overlay-bar chrome (call-site updates only, no structural +rewrite). + +--- + +## Summary — 100% COMPLETE, Clerk-audit-ready + +All governance calls are decided and Jeroen-ratifiable, and every cross-agent +input is folded in — Dudley's interview-2 response (chunk-64 m display band + +Option D + seed-chaining confirm + S2/S4) and his corrected global-tier byte +number (**~8.85 MB PNG all-bodies**), Jeroen's **Global/Region rung-identity +correction** (below), on top of round 2's Dudley/Araminta/Stig/Troblum inputs. +**No `[DUDLEY]` brackets and no held clauses remain.** **§(a.11) (lakes) is +FINAL** — both parts converged on both sides: (1) the **core** — lakes reuse the +existing `MorphologyZone::Lake` (no new field/zone, a data-source fix) sourced +from `HydrologyResult`'s continuous `filled_scaled` field sampled bilinearly per +rung (lake edges *refine* with zoom, not blocky-magnified — seed-chaining +mechanism B); and (2) the **endorheic-vs-overflow cue = outflow-course +presence** (Dudley picked Araminta's (i); no wire bit, no 18th zone). Rationale +recorded: proportionality (4.63% endorheic doesn't justify permanently widening +the D-239-frozen vocabulary), the outlet-wiring is required regardless (overflow +lakes must show exit rivers to be honest — the cue is a side effect), and the +inference is definitionally sound with no misfire (every Overflow basin has a +non-empty `outlet_path`, every Endorheic none). Honest sequencing stated: until +the outlet-wiring ticket ships, the map shows lakes but not the drains-vs-closed +distinction. The stale "4-state water field" crossing is documented (superseded +by Araminta's withdrawal, Dudley's doc marked SUPERSEDED-BY-CROSSING) — never +adopted. + +**Jeroen's rung-identity correction (post-briefing-back) — folded in +throughout.** Round 2 conflated the map opener's *spacing* with its *rung +identity*: it called Region "step 0 / the canonical tier." Corrected to **six +levels**: **rung 0 = Global** (the body-surface map opener — *variable* extent, +one gridunit per region, the D-243 elastic seam made visible; **this** is the +canonical always-kept tier), **rung 1 = Region** (the largest *fixed*-size rung, +viewport-sized and evictable like every rung below it), then District (2), +Quarter (3), Block (4), Chunk (5). Corrected everywhere: §(a.0) ladder/canvas/ +cache bullets, §(a.1)/§(a.2) full-zoom-out reset (→ Global frame), §(a.4) +gridunit entry (Global is the one variable-extent level, fixed rungs snap to +metres), §(a.6) keep-always tier (→ rung 0, not Region), §(a.8) retention floor +(→ rung 0), §(b) envelope carriers (both Global and Region ride it), §(c) Option +D table + snap constraints + factor note, and the T-1152 disposition. **The +correction makes the serving-seam story cleaner:** the global/sub-global cache +boundary now falls on a real rung edge (rung 0→1, the variable→fixed seam) +instead of being buried in the ÷100 Region→District metre factor. **Byte-math +consequence (Dudley, measured):** the always-kept global tier is the tiny +variable region-grid canvas — **~8.85 MB PNG-encoded across all ~267 bodies** +(27.61 MB raw; avg ~18K cells/body), not a fixed 4K-class canvas. This supersedes +the ~174 MB figure (~440× too many cells) and makes the tier **trivially +process-resident, not merely disk-safe** — "always keep global" gets *easier* +under the correction. Sub-global (Tier 2) is unchanged: disk + storage-eviction. + +**Interview-2 reworks are folded in (two changes, two ratifications):** +- **RATIFIED as-is:** the ceiling re-scope (§(a.5)/§(b), files as drafted) and + everything in the package not touched by the two reworks below. +- **REWORK 1 — ladder bottom → chunk (64 m), Option D delivered:** tile/voxel + dropped (Jeroen: "the actual tile level rung seems unusable, maybe replace + with 64"). New amendment §(a.8b) records this as a **deliberate interview-2 + narrowing** of his interview-1 floor opening — used to chunk, tile/voxel + *re-closes* for the Atlas (Phase-5 in-world content). Updated consistently + across §(a.0), §(a.4) (the "10 px per tile" bottom-out superseded by **"1 px + per 64 m gridunit, no magnification margin"** — Dudley's chunk bench), §(a.1), + §(a.8), and §(c). **Option D is the adopted ladder** (six levels: Global opener + + 5 fixed rungs Region→District→Quarter→Block→Chunk; every fixed rung measured + — chunk deepest = 245.8×138.2 km / ~1.7 s parallel). Its bonus: the + deepest-step canvas-sizing *special case is gone* — chunk uses the plain + fixed-3840×2160 budget like every fixed rung (a free simplification), and its + factor profile (÷100/÷4/÷4/÷2, all within the sub-global fixed ladder) is the + gentlest of any candidate, strictly best for the between-step magnification + flag. +- **REWORK 2 — seed-chaining → "cache-accelerated pure function":** Jeroen + ruled he meant *consuming* the coarser output, not independent re-derivation. + New amendment §(a.10) reconciles this with D-227 **without weakening it**: the + definition stays pure (`derive(seed, position)`), the implementation may read + a resident coarser canvas as an **optimization** with derive-fresh fallback, + and it stays inside derive-don't-store because the coarser value is itself + re-derivable to identical bytes (optimization, not semantic dependency — + proven by a mandatory byte-identical-paths determinism test). Dudley's §(b) + independent-re-derivation ruling survives as the *fallback path*; the benched + numbers survive as the **cache-cold worst-case ceiling**, which closes Troblum + B2. **Dudley-confirmed** (`dudley-interview2-response.md` §2): the model is + D-227-sound, no chain-reaction on eviction (each rung's fallback is + self-contained), and his **A/B mechanism distinction** is folded into §(a.10) + — "consuming coarser output" means reading the coarser **continuous primitive + baseline** (mechanism B, the shipped district-reads-region pattern) as input + to a *fresh* classification, **not** reading the coarser *resolved + classification* (mechanism A, ruled out — it would violate Araminta's + categorical re-derivation rule). Acceleration magnitude is unquantified but + not architecture-gating (a future impl-time measurement). + +**Final and record-ready (§a, §b, §c, §d) — 1 new-record draft (§(a.0)) + twelve +amendment texts (§(a.1)–§(a.11), including §(a.8b)):** §(a.1) D-166 corollary; +§(a.2) T-1143 rulings 2/3 supersession; §(a.3) select_rung→step-index; §(a.4) +D-243 gridunit; §(a.5) D-226 §2 ceiling→envelope; §(a.6) D-227 TTL-split; §(a.7) +T-1170 carrier + cliff; §(a.8) D-226(d) per-request+cap; §(a.8b) D-226(d) +floor→chunk (interview 2); §(a.9) D-227/D-192 cache schema-tag; §(a.10) +seed-chaining (interview 2); §(a.11) lakes — FINAL (morphology-fold + +`filled_scaled` sourcing + endorheic cue via outflow-course presence, interview +2). §(a.7) cliff cost is survey-confirmed (0/267 carve). +§(c) carries the adopted **Option D** ladder (six levels: Global opener + 5 fixed +rungs, chunk-deepest, every fixed rung measured). Deprecation sweep: **12 DQR +dispositions** (adds D-226 T-1143-ruling-1 narrow + the D-227 seed-chaining line) ++ 10 ticket dispositions (adds the two lake tickets — morphology-sourcing + +basin-outlet D8 wiring) + the `_canvas.scale` retirement path. **No `[DUDLEY]` +brackets, no held clauses, no open items remain** — the global-tier byte total +is in (~8.85 MB PNG all-bodies, Dudley measured; supersedes the ~174 MB figure, +tier now trivially process-resident) and §(a.11)'s endorheic cue is finalized +(outflow-course presence). **The package is 100% complete and Clerk-audit-ready.** + +**For Jeroen's final ratification** (governance calls, decided text ready): +(1) **§(a.10) seed-chaining** against his phrase "serves as seed information for +the deeper cascade" — the cache-accelerated-pure-function model (mechanism B) is +the reconciliation of his "consuming coarser output" intent with D-227; +(2) **Option D's six-level shape** (Global opener + Region→District→Quarter→Block +→Chunk) — the tile-drop removed the old B-vs-C fork, so this is a single confirm, +not a choice. +The migration is the one real chunk of new work; Dudley's code read puts it at +"doable" (one `Inbound` variant + one `SimBridge` method on a pattern proven five +times), with the "challenging" part in Stig's client rebuild. + +*Troblum's adversarial pass is now **fully dispositioned**: B1 (cliff survey) +resolved 0/267 → §(a.7) survey-confirmed; B2 (seed-chaining) resolved by §(a.10); +**S2 (deep-step course density) run by Dudley** — a real finding (+38%–87% cost +at chunk/block vs. District's <5%, traced to course point-count scaling inversely +with rung spacing; still affordable in absolute terms, flagged as an +implementation consideration — a possible rung-independent station-spacing cap — +for the ticket plan; the measurement is filed as the "S2 ADDENDUM" in +`measurements/t1178-t1154-derive-bench.md`, alongside the chunk-64 m +"INTERVIEW-2 ADDENDUM" — both already written by Dudley); **S4 (sim-state phase +cadence) resolved** by a Dudley+Araminta joint formula (`SIM_STATE_TTL` bound to +1× the field's own driving sim bucket — tidal-or-seasonal for flooded, seasonal +for glaciation; 1× confirmed cost-affordable). S1 (evict-then-revisit costing) +remains a Dudley cache-spec tuning number, not a governance-text change — my +§(a.6) TTL amendment already separates the two eviction axes it probes. S2's +finding may warrant a one-line note in the §(a.7)/render-record wire discussion +if course rendering at chunk needs the station-cap; flagging for the Clerk audit. +Full adversarial-pass disposition is visible for the audit.* diff --git a/docs/workshops/body-map-viewer/workshop-outcomes.md b/docs/workshops/body-map-viewer/workshop-outcomes.md new file mode 100644 index 000000000..812af6b8a --- /dev/null +++ b/docs/workshops/body-map-viewer/workshop-outcomes.md @@ -0,0 +1,424 @@ +--- +title: "Workshop Outcomes" +description: "Final outcomes: D-255 (new) + 12 amendments, the six-level Atlas step ladder (Global opener + Region-District-Quarter-Block-Chunk), the tagged-envelope wire contract, cache-tier architecture, the lakes/hydrology sourcing fix, and the full deprecation sweep." +type: workshop +status: archived +workshop: body-map-viewer +agent: qatux +round: 0 +created: 2026-07-25 +decision_refs: [D-010, D-166, D-169, D-170, D-192, D-223, D-225, D-226, D-227, D-228, D-239, D-243, D-253, D-255] +--- + +# Body Map Viewer Workshop — Outcomes + +**Workshop:** Body Map Viewer (the stepped map handler) +**Dates:** 2026-07-25 (round 1) – 2026-07-23 (final ratification and filing) +**Rounds:** 2 core rounds + two lead interviews, extended with a post-ratification +addendum round (the lakes/hydrology gap Jeroen raised after reviewing the +as-built briefing) — the workshop was kept open across all three review passes +rather than closed on partial convergence, per standing team practice. +**Participants:** Dudley, Araminta, Stig, Tyre, Troblum (round 2 + adversarial), +Qatux (documenting), SI (reviewing ticket outputs) +**Decisions produced:** D-255 (new) + 12 amendments (D-166, D-192, D-225, D-226 +×4, D-227 ×4, D-243, river-courses-t1170.md note) +**Compiled by:** Qatux + +--- + +## 0. Workshop trace + +| Stage | Document | Status | +|---|---|---| +| Brief | [body-map-viewer-workshop-brief.md](body-map-viewer-workshop-brief.md) | complete | +| Prep: outline, clarifications, implications | [jeroen-outline.md](jeroen-outline.md), [clarifications.md](clarifications.md), [tyre-implications.md](tyre-implications.md) | complete | +| Pre-workshop measurements ①–⑤ | `measurements/t1177-hydrology.md`, `t1178-t1154-derive-bench.md`, `t1179-wire-table.md`, `t1180-imagetexture.md` | complete | +| Round 1 positions | dudley/araminta/stig/tyre-round1.md | complete | +| Round 1 synthesis | [round-1-notes.md](round-1-notes.md) | complete | +| Lead interview 1 | [lead-interview-1.md](lead-interview-1.md) | complete | +| Measurement ⑥ (post-interview-1) | `measurements/t-setpixel-c1.md` | complete | +| Hydrology population survey (post-interview-1) | `measurements/t1177-hydrology.md` §POPULATION SURVEY | complete | +| Round 2 synthesis | dudley/araminta/stig/tyre-round2.md | complete | +| Round 2 adversarial pass | [troblum-round2.md](troblum-round2.md) (incl. ADDENDUM + FINAL UPDATE) | complete | +| Round 2 synthesis notes | [round-2-notes.md](round-2-notes.md) | complete | +| **Lead interview 2** (ladder-bottom narrowing, seed-chaining ruling) | rulings folded into [dudley-interview2-response.md](dudley-interview2-response.md), [tyre-round2.md](tyre-round2.md) §(a.8b)/§(a.10) | complete | +| Chunk-64m bench, Option D, S2 courses-density bench, S4 TTL formula | [dudley-interview2-response.md](dudley-interview2-response.md) §1–4 | complete | +| **Post-ratification review** (Global/Region rung split, lakes gap) | [dudley-interview2-response.md](dudley-interview2-response.md) POST-RATIFICATION ADDENDUM + LAKES CONVERGENCE | complete | +| Lakes convergence (message-crossing + resolution) | [araminta-round2.md](araminta-round2.md) §(e), [dudley-interview2-response.md](dudley-interview2-response.md) §5/LAKES CONVERGENCE, [tyre-round2.md](tyre-round2.md) §(a.11) | complete | +| Final governance package | [tyre-round2.md](tyre-round2.md) (final, §(a)–(d)) | complete | +| As-built plain-language briefing | [architecture-briefing-final.md](architecture-briefing-final.md) | complete | +| Filing (D-record + 12 amendments) | this document §2 | **complete — D-255 claimed** | +| Deprecation sweep / Clerk audit | this document §5 | complete | +| Ticket plan | this document §6 | complete (12 DQR + 10 ticket dispositions, drafted by Tyre, SI review per standing workflow) | +| This document, final | — | **complete** | + +--- + +## 1. Interview rulings — the full sequence + +Three review passes ruled on this workshop's open items, in order. Each is +recorded here as process history — the corrections across passes are not +noise; they are exactly the kind of thing this document exists to make +visible rather than silently overwrite. + +### Interview 1 (post round 1) + +Full record: [lead-interview-1.md](lead-interview-1.md). Ratified by silence: +tagged-envelope migration triggered; viewport-sized canvases below the +canonical tier; the T-1177 cliff representation (`elevation` + `channel_depth` ++ `cliff_edge`); cache composition (server global tier + client `FileAccess` +dir, compose not compete). Ruled: step ladder deferred to round 2's concrete +tables; map time axis = current-state-via-TTL-split with the staleness-vs- +storage eviction-axis distinction; cliffs sparse + Phase-4 scope; round 2 GO +with measurement ⑥ run in parallel. + +### Interview 2 (post round 2) — two rulings that reworked the round-2 package + +1. **Ladder bottom narrowed from tile/voxel (1 m) to chunk (64 m).** Jeroen, + verbatim: *"the actual tile level rung seems unusable. maybe replace with + 64?"* — reasoning: a full-screen Atlas view at 10 px/tile shows ~192×108 m + of ground, which is in-world viewport content (Phase 5), not map content. + This is a **deliberate narrowing of the D-226(d) floor's interview-1 + opening, not a reversal of it** — block and chunk remain legal Atlas rungs + under the viewport-sized carve-out; tile/voxel re-close, exactly as item + (d) originally required, now scoped to the tier where the opening was + never meant to reach. Dudley benched the never-measured chunk rung in + response (`dudley-interview2-response.md` §1) and built **Option D** — the + adopted ladder. +2. **Seed-chaining ruled the OPPOSITE of Dudley's round-2 default.** Jeroen + answered **NO** to Dudley's round-2 "independent re-derivation, coarser + rung called only as a nested function" ruling — he meant the finer step + genuinely **consumes** the coarser step's *resolved* output. Reconciled + without weakening D-227 via the **cache-accelerated pure function** model + (§2 below; `tyre-round2.md` §(a.10)). + +Both rulings triggered new measurement work (the chunk-64m bench, the S2 +courses-density bench at chunk/block, the S4 TTL-cadence formula) rather than +being filed as bare architectural pronouncements — consistent with this +workshop's standing discipline of ruling from real tables, not principles. + +### Post-ratification review (after the as-built briefing) + +Jeroen reviewed [architecture-briefing-final.md](architecture-briefing-final.md) +(the plain-language as-built writeback he requested) and raised two further +items: + +1. **Global-tier byte math was wrong.** Round 2's ~174 MB figure had priced + the canonical always-kept tier as a full District-spacing canvas (8.3M + cells/body). Jeroen's own review caught that the opener is a **variable, + region-grid canvas** (one gridunit per region, not per District cell) — + correcting this produced a **rung-identity split**: rung 0 = **Global** + (the variable, always-kept opener) and rung 1 = **Region** (the largest + *fixed*, evictable rung) — previously conflated as one "step 0 = Region" + tier. Recomputed against the real 267-body population: **~8.85 MB + PNG-encoded, all bodies** (not ~174 MB — roughly 20× smaller). +2. **The lakes gap.** Jeroen's direct question: *"when filling basins to find + an overflow, is that body flagged as lake or flooded or something? do we + draw lakes on the map?"* This surfaced a real gap (§4 below) and produced + a genuine message-crossing between Araminta and Dudley, resolved + convergently — documented in full as process history in §4. + +--- + +## 2. Governance record — filing package (final) + +**D-255 claimed** (`architecture` domain). One new record + twelve amendment +texts, all record-ready as drafted in `tyre-round2.md` §(a). + +| # | Record | Type | Source text | +|---|---|---|---| +| 1 | **D-255 — Body-map-viewer stepped render architecture** | NEW | tyre-round2.md §(a.0) | +| 2 | D-166 corollary — per-step repoint (owns between-step magnification) | AMEND | §(a.1) | +| 3 | D-226 T-1143 ruling 2/3 — tagged-envelope + continuous-zoom superseded | AMEND | §(a.2) | +| 4 | `select_rung` / rung model — replaced by the discrete step index | AMEND | §(a.3) | +| 5 | D-243 — gridunit vocabulary entry (additive; Atlas floor = chunk) | AMEND | §(a.4) | +| 6 | D-226 T-1124 §2 windowed-family ceiling — tagged-envelope triggered, re-scoped | AMEND | §(a.5) | +| 7 | D-227 — map-time TTL-split + staleness/storage eviction-axis distinction | AMEND | §(a.6) | +| 8 | river-courses-t1170.md carrier rule + cliff sparse-list | AMEND (note) | §(a.7) | +| 9 | D-226(d) whole-body prohibition — per-request framing + client-cache accumulation cap | AMEND | §(a.8) | +| 10 | D-226 T-1143 ruling 1 — floor narrowed to chunk (tile/voxel re-close) | AMEND | §(a.8b) | +| 11 | D-227/D-192 — persistent client-cache schema/version tag | AMEND | §(a.9) | +| 12 | D-227 — seed-chaining as "cache-accelerated pure function" | AMEND | §(a.10) | +| 13 | D-227 — lakes sourced from settled hydrology; endorheic cue via outflow-course presence | AMEND | §(a.11) | + +**Records confirmed surviving untouched** (stated explicitly per Tyre's list, +so the Clerk audit does not over-rewrite them): D-010 (four principles — the +server relocation *reinforces* principle 4); D-166 cascade + phase gating +(still Phase-4 Atlas); D-169/D-170 (implant UI, unchanged); D-223 +(`atlas_city_names`, unchanged); D-239 (the frozen 17-zone morphology + +vegetation/glaciation vocabulary — the lakes fix explicitly does **not** +touch it, reusing the existing `MorphologyZone::Lake` discriminant and +routing the endorheic cue through `courses` precisely to avoid an 18th zone). + +**D-record text:** the full, final amendment text for every item above is +authoritative in `tyre-round2.md` §(a.0)–(a.11) — this table is an index with +citations, not a duplicate copy, per the project's single-source-of-truth +convention for governance text (`governance/README.md`). Sync into +`governance/decisions/architecture.md` via `pql decisions claim D architecture +"Body-map-viewer stepped render architecture"` (already reserved as D-255) +followed by `pql decisions sync`. + +--- + +## 3. Architecture summary (as-built) + +The full plain-language writeback is [architecture-briefing-final.md](architecture-briefing-final.md) +— Jeroen's own outline, rewritten in the same style and brevity to reflect +what actually shipped. Condensed here: + +- **Three locked premises, unchanged since the brief:** server-side content + determination (per-gridunit data canvases per zoom step), client as a pure + map-art function (colorize/style/annotate, never invent geometry), stepped + zoom (discrete rungs, cursor-anchored, no continuous zoom-scaled canvas). +- **The Atlas step ladder — six levels, final (Option D):** + + | Rung | Level | Spacing | Canvas | Cost (parallel, measured) | + |---:|---|---:|---|---:| + | 0 | **Global** (opener, canonical, always-kept) | 1 gridunit/region (variable, ~205 km) | body's own region grid, ~18K cells avg | ~16–21 ms/body; ~8.85 MB PNG all 267 bodies | + | 1 | Region | 204.8 km | 3840×2160 fixed | 1,827 ms | + | 2 | District | 2,048 m | 3840×2160 fixed | 1,827 ms | + | 3 | Quarter | 512 m | 3840×2160 fixed | ~1,827 ms (same-band) | + | 4 | Block | 128 m | 3840×2160 fixed | ~1,827 ms (measured) | + | 5 (deepest) | **Chunk** | 64 m | 3840×2160 fixed | 1,724–1,733 ms (measured) | + + Bottom-out rule: **1 screen px per 64 m gridunit, no magnification margin** + (the old 10×-per-tile legibility margin does not apply — a chunk-scale + feature is already legible at 1×1). Tile/voxel (1 m) is Phase-5 in-world + content, never Atlas-mapped. Every fixed row is directly measured; rung 0 + is measured too (Dudley's `bmv_global_tier_bench.rs`). +- **Wire:** tagged-envelope carrier (`StepCanvasRequest`/`StepCanvasResponse`, + a sixth discriminated `Inbound` variant extending an already-five-times- + proven pattern in `server/src/bridge/mod.rs`). 8 dense fields (6 static + 2 + sim-state) PNG-per-field, 2 sparse lists (`courses`, `cliffs`) + MessagePack-native. No `water` field — lakes ride the existing + `MorphologyZone::Lake` (§4). +- **Cache:** three tiers — client in-memory LRU → client disk `FileAccess` + (two independent sweep axes: staleness for sim-state, storage-budget for + everything else) → server (Tier 1 rung-0 resident resource, ~8.85 MB all + bodies, keep-always; Tier 2 sub-global geometry, storage-evicted on + time-since-last-visit). Sim-state TTL bound to 1× the field's own fastest + driving sim-clock bucket (tidal-or-seasonal for flooded, seasonal for + glaciation) — a formula, not a fixed number, confirmed cost-affordable at + 1× with no multiplier needed. +- **Client render:** RTT terrain layer, CPU `Image.set_pixel` colorize + (confirmed cheap at realistic canvas sizes by measurement ⑥, ~78 ns/cell + flat) + unscaled screen-space annotation layer, replacing `_canvas.scale` + and the entire `_zs()`/`_zs_stroke()`/`_zs_ring_radius()` compensation + family by construction. +- **Seed-chaining:** cache-accelerated pure function — a finer rung's + derivation may read a resident coarser rung's continuous baseline as an + *optimization* (falling back to fresh derivation when not resident); the + ①②③ benched costs remain valid as the cache-cold worst-case ceiling. A + mandatory cache-hit == cache-miss byte-identical determinism test is the + correctness gate. +- **Lakes:** sourced from `HydrologyResult`'s settled equilibrium, riding the + existing `MorphologyZone::Lake` — zero new wire fields, zero vocabulary + change (§4). + +--- + +## 4. Process history — the lakes arc (documented in full, not condensed away) + +This is recorded at length deliberately: it is a clean example of the +workshop's own discipline working correctly under a genuine, in-flight +message-crossing, and the coordinator specifically asked that it be +preserved as process history rather than silently resolved into a single +final answer. + +**The catch.** After reviewing the as-built briefing, Jeroen asked directly +whether basin-filling from settled hydrology gets flagged as a lake on the +map at all. Checked against the real code (not assumed): the gap was more +specific than a missing field. `MorphologyZone::Lake` (discriminant 1) +**already exists** in the frozen 17-zone vocabulary — the gap was a missing +**data source**. Today's `Lake` emission comes from a crude heightmap +threshold (`ocean_fraction_q >= 60`, a bilinear sample of the raw +below-sea-level mask) with **zero connection** to `HydrologyResult`'s settled +basins — a gap the code's own comment already flagged as known, just never +connected to the solver this workshop's own measurement ① is about. + +**The crossing.** Dudley answered the coordinator's routed lake-schema +question with a proposal for a **new `water: Vec` dense field** +(`{None, Lake, Sea}`), reasoning from first principles about wire cost and +vocabulary freezing. Independently, in flight, Araminta specced her own +`water: Vec` field with the same shape and posed three questions back to +Dudley (endorheic-carrier sizing, pipeline slot, struct reconciliation). +Dudley answered those three questions fully (visible in his document as §1–3 +under "LAKES CONVERGENCE," including a full crossover-cost bench comparing a +sparse `Vec` list against the dense `water` field across the real +267-body basin-size distribution). + +**The resolution.** Later the same day, both Dudley and Araminta +**independently discovered the better answer and converged on it without +either conceding to the other**: fold lake/sea classification into the +**existing** `MorphologyZone::Lake` discriminant instead of adding any new +field at all. Araminta reached this by re-verifying Dudley's own code-read +directly against source (`district_profile.rs:556-565`, `generator.rs:1198- +1223`, `features.rs:104-105`) and recognizing her own original objection (that +folding into morphology would require widening a frozen vocabulary) was +built on a false premise — the vocabulary entry already existed; only the +data source was missing. Dudley reached the same place from his own +post-ratification code investigation. Both documents mark the earlier +`water`-field material explicitly as **SUPERSEDED-BY-CROSSING** rather than +silently deleting it — Dudley's document states plainly: *"it isn't one of +us conceding to the other, it's convergent verification."* + +**What survived the crossing unmodified:** the pipeline/projection ruling +(sample `HydrologyResult`'s continuous `filled_scaled` field bilinearly per +rung — never project discrete basin-cell membership, which would produce a +blocky, non-refining lake edge, exactly the magnified-coarser-composite +artifact D-166's corollary forbids). This reasoning is agnostic to which +field carries the result, so it applied unchanged to both the abandoned +`water` field and the final morphology-fold. + +**Final, converged answer (D-227 amendment §(a.11)):** +- Lakes ride the **existing** `MorphologyZone::Lake` — zero new wire field, + zero vocabulary change. The fix is a **data-source correction**: + `derive_morphology_zone` sources `Lake` from settled hydrology's continuous + `filled_scaled` field (sampled bilinearly, the same mechanism + `ocean_fraction_q`/`sea_level` already use), falling through to today's + heuristic where no basin exists. +- Lake geometry is **static** (a pure function of seed + terrain, cached + indefinitely-fresh), explicitly distinct from Araminta's sim-state + `flooded` plane — a lake's existence doesn't flicker on the seasonal TTL. +- **Endorheic-vs-overflow cue: outflow-course presence, zero wire bits, zero + new zone.** Dudley's original crossover-cost analysis (dense-bit vs. + sparse-list) was correctly identified by the coordinator as not actually + answering this question — both of those carriers were foreclosed once the + morphology-fold landed. The real choice was reasoned fresh: an overflow + basin's guaranteed non-empty `outlet_path` becomes a visible exit river in + `courses`; an endorheic basin's absence of one is the honest "no drain" + signal. Three reasons this wins over a dense vocabulary bit: proportionality + (endorheic is 4.63% of real basins — too rare to justify a permanent 18th + `MorphologyZone` arm every consumer inherits forever), the outlet-wiring + work is required regardless (an overflow lake must show its exit river to + be hydrologically honest — the cue is a free side effect, not a feature + built to carry it), and the inference is definitionally sound with no + misfire case (every `Overflow` basin has a guaranteed outlet path, every + `Endorheic` basin has none — tested directly: + `overflowing_basin_has_nonempty_outlet_path`). +- **Honest sequencing stated plainly, not glossed over:** until the + basin-outlet→D8 wiring ticket ships (pre-cleared as additive by T-1170 + Ruling 7b's reserved `TERMINAL` sentinel), the map shows lakes but not the + drains-vs-closed distinction. This is not a design gap — it is two + distinct, sequenced deliverables, and the outcomes record states which one + ships first rather than implying the distinction arrives automatically the + day the sourcing fix lands. +- The originally-proposed cliffs-based endorheic cue was independently ruled + out by Araminta after directly re-checking the population survey: zero + carved cells across all 267 real bodies means a cliff-wired cue would never + fire in practice. + +--- + +## 5. Deprecation sweep + +**DQR records** (13 dispositions — the 12 amendments in §2 plus D-192's +cross-reference note; full text in `tyre-round2.md` §(d)): + +| Record | Disposition | +|---|---| +| D-166 corollary (2026-07-21) | AMEND — repointed per-step, not deleted | +| D-226 T-1124 §2 ceiling | AMEND — re-scoped to the legacy `district_window` carrier only; re-scope is **final**, the conditional supersede branch does not fire | +| D-226 T-1143 ruling 2 | SUPERSEDE — tagged-envelope migration now triggered | +| D-226 T-1143 ruling 3 | SUPERSEDE for transport (stepped); cursor-centering/edge-scroll/full-reset survive | +| D-226 T-1143 §6 `select_rung`/`MAX_COVERAGE_M` | SUPERSEDE — replaced by the discrete step index | +| D-226 T-1143 ruling 1 (item-(d) floor opened) | AMEND/NARROW — opening now lands at chunk (64 m); tile/voxel re-close | +| D-243 item (5) vocabulary | AMEND (additive) — `gridunit` role name; Atlas floor bottoms at chunk | +| D-225 (tagged-envelope deferral) | SATISFY (note) — constraint discharged by this workshop's envelope | +| D-226 item (d) whole-body prohibition | AMEND — per-request/per-derivation framing + client-cache accumulation cap + the chunk-floor narrowing | +| D-227 (derive-don't-store) | AMEND (additive ×4) — TTL-split/eviction axes; persistent-cache schema tag; seed-chaining cache-accelerated model; lakes sourcing fix | +| D-192 (no version handshake) | CROSS-REF note — the co-ship guarantee does not extend to the persistent disk cache | +| river-courses-t1170.md (Ruling 1c) | AMEND (note) — terminology repoint + `cliffs` as rule (iii)'s second vector member | + +**Surviving untouched:** D-010, D-166 cascade/phase gating, D-169/D-170, +D-223, D-239 (see §2 for the explicit no-touch statement on each). + +**Tickets** (10 dispositions): + +| Ticket | Disposition | +|---|---| +| T-1176 | CLOSE as delivered — this workshop is its output | +| T-1158 | CANCEL with supersession note — the canonical-frame cluster it wanted to extract is exactly what the stepped model deletes | +| T-1175 | RE-SCOPE, unblocked — the c1 blocker (measurement ⑥) is discharged; tapering lands on the new screen-space annotation layer | +| T-1157 | RE-SCOPE as the stepped mechanism's capture harness — the dead-goldens problem it targets is a direct consequence of the split the stepped model collapses | +| T-1174 | KEEP unchanged — a derivation-correctness bug orthogonal to the render pivot, priority raised (more derive paths now share the same position math) | +| T-1153 | stays `done`; shipped code (`_view_zoom`, continuous-zoom transport) on the `_canvas.scale` retirement path | +| T-1152 | stays `done`; `compute_tile_grid()` mosaic superseded by the rung-0 Global canvas | +| T-1153/T-1152 test suites | retired with their code; replaced by re-scoped T-1157 | +| **NEW — lake morphology sourcing** | CREATE — source `MorphologyZone::Lake` from `HydrologyResult`'s `filled_scaled`, self-contained, no wire change | +| **NEW — hydrology basin-outlet → D8 wiring** | CREATE — thread overflow basin outlets into `RiverNetwork`/`courses`; pre-cleared additive by T-1170 Ruling 7b; this is the ticket the endorheic cue ships on | + +**`_canvas.scale` retirement path** (explicit list for the implementation +ticket, full text `tyre-round2.md` §(d)): `_canvas.scale`/`_canvas.position`/ +`_apply_transform()`; the `_zs()`/`_zs_stroke()`/`_zs_ring_radius()` +compensation family; `select_rung()`/`MAX_COVERAGE_M`/`compute_tile_grid()`; +the `AtlasViewer`/orbital-mosaic-vs-window split (collapses to "the Global +opener is rung 0, one path"); `_view_zoom`/`_canonical_fit_zoom()` (replaced +by the discrete rung index + the rung-0 canonical frame). + +**Clerk consistency audit:** the known-conflicts starting set above (13 DQR + +10 ticket dispositions) is the complete, agent-produced sweep. A systematic +audit pass beyond this named set is the standing pre-dismissal check per the +brief's Expected Output 2 — run against the filed `governance/decisions/ +architecture.md` text once D-255 and the twelve amendments are synced, to +catch any conflict the delta did not explicitly name. + +--- + +## 6. Measurement summary + +| # | Ticket | Headline | Doc | +|---|---|---|---| +| ① | T-1177 | Hydrology viable; cliff repr. = elevation+channel_depth+cliff_edge; population survey: 0/267 real bodies carve | measurements/t1177-hydrology.md | +| ② | T-1178 | Parallel throughput flat 190–220 ns/cell, 330K–8.3M cells | measurements/t1178-t1154-derive-bench.md | +| ③ | T-1154 | Block GO; original deep-step (voxel) bench superseded by chunk bench below | measurements/t1178-t1154-derive-bench.md | +| ④ | T-1179 | PNG-per-field wins; 21×–563× over 30KB cap → tagged envelope triggered | measurements/t1179-wire-table.md | +| ⑤ | T-1180 | Texture upload non-issue, 0.03–4.6ms | measurements/t1180-imagetexture.md | +| ⑥ | (round 2) | CPU colorize 77.5 ns/cell flat; beats byte-buffer ~2×; c1 = CPU-first, confirmed | measurements/t-setpixel-c1.md | +| interview-2 | (Dudley) | Chunk (64m) bench: 1,724–1,733 ms parallel full-canvas; Option D adopted | dudley-interview2-response.md §1 | +| S2 | (Dudley, Troblum-requested) | Courses cost at chunk/block: +38%–87% (vs. District's <5%) — real, traced mechanism, still affordable | dudley-interview2-response.md §3, filed as addendum in measurements/t1178-t1154-derive-bench.md | +| S4 | (Dudley + Araminta) | Sim-state TTL formula: 1× fastest driving sim-clock bucket, confirmed cost-affordable at 1× | dudley-interview2-response.md §4 | +| global-tier recheck | (Dudley, post-ratification) | Rung-0 corrected: ~8.85 MB PNG all 267 bodies (not ~174 MB) — Global/Region rung split | dudley-interview2-response.md POST-RATIFICATION ADDENDUM §6 | +| lake basin distribution | (Dudley, post-ratification) | Real population: 22,270 basins, dense wins 100% vs. sparse for lake carrying | dudley-interview2-response.md LAKES CONVERGENCE §1 | + +--- + +## 7. Adversarial pass disposition (Troblum) + +Final scorecard (`troblum-round2.md` FINAL UPDATE): of seven original +findings (2 BLOCKING, 5 SERIOUS), **all seven are resolved** by the time of +filing: + +| Finding | Resolution | +|---|---| +| B1 (population-scale cliff survey never run) | RESOLVED — 267/267 real bodies surveyed, 0 carve | +| B2 (seed-chaining fork unresolved) | RESOLVED — interview-2 ruling + Dudley's source-verified cache-accelerated model | +| S1 (evict-then-revisit costing) | RESOLVED (mechanism) — both cache specs converge on dropping `distance_decay`; end-to-end revisit number correctly deferred as tuning | +| S2 (courses-density coverage gap) | RESOLVED — Dudley's chunk/block courses bench, real finding, filed | +| S3 (D-226(d) letter vs. purpose) | RESOLVED — per-request framing + client-cache accumulation cap, §(a.8) | +| S4 (sim-state phase cadence unnamed) | RESOLVED — Dudley+Araminta joint formula, 1× confirmed affordable | +| S5 (disk-cache version tag) | RESOLVED — §(a.9), complete fix | + +N1 (units convention), N2 (informational), N3 (clean bill) — all closed or +never live concerns. No finding required walking back a round-1 or round-2 +position; every fix Troblum checked against primary source held up exactly +as documented. + +--- + +## 8. Diagrams + +Pending — to be created via `/d2-diagram` in `docs/diagrams/architecture/` +following the standard post-filing diagram pass: +- Step-ladder diagram (Global opener → Region → District → Quarter → Block → + Chunk, with the variable/fixed and canonical/evictable seams marked). +- Cache-tier diagram (three tiers, the two eviction axes, the rung-0/rung-1 + seam). +- Wire-envelope diagram (tagged `StepCanvasRequest`/`Response` vs. legacy + `district_window`, the re-scoped ceiling boundary). +- Lakes data-flow diagram (`HydrologyResult` → `filled_scaled` bilinear + sample → `MorphologyZone::Lake` → `courses` outlet presence → endorheic + cue), given the arc's own complexity documented in §4 above. + +**Not yet started** — first post-filing task, per standing practice +(diagrams follow filed decisions, not the reverse).