docs(meta): body-map-viewer workshop — rounds, measurements, outcomes, as-built briefing
The complete workshop record: four round-1 positions, five round-2 syntheses (incl. Troblum's adversarial pass with addendum + final scorecard — all seven findings resolved), both lead interviews, Qatux's round notes and the 8-section workshop-outcomes.md (the lakes message-crossing documented as process history), measurement ⑥ (set_pixel/c1) + the population-survey and chunk/S2 addenda in the measurement docs, the brief's appendix updated through ⑥, and architecture-briefing-final.md — Jeroen's outline written back as-built (six-level ladder, lakes, ~9MB resident global tier). README row: Complete. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)) |
|
||||
|
||||
@@ -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<RiverCourse>`,
|
||||
outside the windowed-family ceiling, per the same echo key as the six dense
|
||||
arrays). I have nothing to add to the *geometry* carrier — Tyre's ruling is
|
||||
correct and I'm not reopening it. My scope is the **naming** half, which
|
||||
Ruling 1c's `RiverCourse` struct doesn't currently carry at all (`edge_id`,
|
||||
`class`, `points`, `terminus` — no name field).
|
||||
|
||||
**River names: same ids-with-lookup shape as settlements, one level indirect.**
|
||||
`edge_id` is already the stable identity (`river_course::pack_cell_id`,
|
||||
stable across every window/rung shipping that edge, per the doc). Do not
|
||||
inline "Kelvin's Run" into every `RiverCourse` polyline on every window that
|
||||
happens to cross it — a trunk river crosses dozens of windows over its
|
||||
length, and the string would repeat dozens of times for zero new
|
||||
information (the same argument PNG encoding wins for the dense fields:
|
||||
don't pay bytes for something the client can dereference). Instead: name
|
||||
assignment is a whole-body, rung-independent property of the **edge-id
|
||||
graph** (rivers don't get renamed between gridunits), so it belongs in a
|
||||
names-only whole-body lookup exactly like `atlas_city_names` —
|
||||
`river_names: BTreeMap<u32 /* edge_id or trunk-root id */, String>` fetched
|
||||
once per body, joined client-side against the `edge_id` already on every
|
||||
`RiverCourse`. This is additive to the existing whole-body atlas-names
|
||||
request path, not a new mechanism — same shape, second table.
|
||||
|
||||
One nuance rivers have that settlements don't: a single named river usually
|
||||
spans many `edge_id`s (a trunk plus its class-2 tributaries share a name;
|
||||
"unnamed" streams don't get an entry at all). That's a **naming-assignment
|
||||
generator concern** (which edges share a name), not a wire-encoding concern
|
||||
— the wire only needs "this edge_id resolves to this name-table key or
|
||||
none," so I'm not blocking on it; flagging it so Dudley's naming generator
|
||||
(whenever that lands) knows the wire already expects a many-edges-to-one-name
|
||||
join, not a strict 1:1.
|
||||
|
||||
### Roads → not in this workshop's scope, but the encoding falls out for free
|
||||
|
||||
Roads aren't measured or built yet (outline: "later, ... roads, railroads,
|
||||
etc will also be in scope"), but worth stating now since it's free: roads
|
||||
are graph-like exactly like rivers, so they inherit rule (iii) + the same
|
||||
ids-with-lookup naming shape without a new design pass when they land. Not
|
||||
claiming this as decided — just noting the pattern generalizes and nobody
|
||||
needs to re-litigate named-feature encoding a third time.
|
||||
|
||||
### Why not dense classification rasters for any of these
|
||||
|
||||
The brief posed "dense classification rasters + sparse feature list vs
|
||||
inline identity vs ids-with-lookup" as the open question. A dense raster
|
||||
(e.g. a `settlement_id: Vec<u32>` per-gridunit array, one entry per cell)
|
||||
is the wrong shape for anything that is **sparse and discrete** — settlements
|
||||
and named rivers occupy a small fraction of gridunits in any canvas. T-1179's
|
||||
own per-field RLE numbers make this concrete: `morphology`/`vegetation`
|
||||
(genuinely dense, every cell has a value) still only manage 6 runs at 330K
|
||||
cells because they're spatially coherent — a settlement-id raster would be
|
||||
mostly a single "no settlement" sentinel with a handful of small filled
|
||||
regions, which is exactly what a sparse feature list (a handful of
|
||||
`{id, footprint}` entries) encodes in a few hundred bytes instead of
|
||||
330K–8.3M raster cells that are 99%+ one repeated sentinel. **Dense rasters
|
||||
are for continuous fields (rule ii territory — biome, elevation, moisture);
|
||||
sparse feature lists + ids-with-lookup are for discrete named things (rule
|
||||
i/iii territory). Don't blend them — the moment "sparse feature" bleeds into
|
||||
"raster," we're back to paying for information nobody asked for.**
|
||||
|
||||
---
|
||||
|
||||
## 2. The per-gridunit payload schema
|
||||
|
||||
**Baseline: the six fields `DistrictWindowLayer` ships today are exactly
|
||||
right and I'm not adding classification vocabulary — I'm adding the fields
|
||||
the outline's own example promised and the cliff proposal requires.**
|
||||
|
||||
Jeroen's outline literally specified the payload: *"biome: grassland,
|
||||
settlement: null/'name here', river: null/'name here/unnamed', frozen:
|
||||
true/false, flooded: true/false, sea: true/false."* Reconciling that
|
||||
against what's measured and what's shipped:
|
||||
|
||||
| Outline's ask | Wire field today | My call |
|
||||
|---|---|---|
|
||||
| biome | `morphology: Vec<u8>` (17-zone D-239 §6) | Carries it. "Biome" in the outline reads as morphology zone — keep the existing vocabulary, don't add a second one. |
|
||||
| — | `elev_q: Vec<u8>` | Keep (height, per outline's separate ask). |
|
||||
| frozen | `glaciation: Vec<u8>` (5-grade) | Carries it — a boolean collapse of glaciation is a client-side *derivation of the same data*, not a new field. Client reads `glaciation > 0` if it wants a bool; server shouldn't duplicate the same fact two ways on the wire. |
|
||||
| flooded, sea | `morphology` (has water zone classes) + hydrology's settled state | See "map time axis" note below — this is where the outline's "sea"/"flooded" distinction needs a decision the workshop, not me, should rule on (Q1 to Jeroen in the brief). I'm not duplicating a boolean the morphology vocabulary can already express; if the workshop wants a fast client-side "is this wet" check without decoding the full morphology enum, that's a **cheap additive derived bit**, not new server-computed information — flag it, don't block on it. |
|
||||
| settlement: null/name | **new**, see below | Presence + id, not inline name (§1). |
|
||||
| river: null/name | `courses: Vec<RiverCourse>` + **new** name lookup | Already ships geometry; name resolves via the new whole-body table (§1). |
|
||||
| — | `moisture_q`, `vegetation` | Keep — not in the outline's own example list but already shipped and load-bearing (vegetation climate law, D-239 §8). |
|
||||
|
||||
**New field: `settlement_id: Vec<u32>` (0 = none), per-gridunit dense array,
|
||||
same shape as `morphology`.** This is the one genuinely new dense field my
|
||||
schema adds. It's dense (not sparse-list) because "which settlement, if any,
|
||||
covers this gridunit" is a per-cell lookup a renderer needs at paint time —
|
||||
exactly the same shape argument as `morphology` itself (a classification
|
||||
raster, not a feature list, because every cell needs an answer, most of them
|
||||
"none"). This is different from the settlement *name* (§1's ids-with-lookup)
|
||||
— the id here is cheap (u32, near-constant runs, same RLE profile as
|
||||
`morphology`/`vegetation` in T-1179's per-field table — settlements are
|
||||
rare and spatially coherent, so this field costs almost nothing extra under
|
||||
PNG encoding). The **name** join happens client-side against the whole-body
|
||||
lookup; the wire never repeats the string.
|
||||
|
||||
**Cliff/vertical structure: adopt T-1177's proposal as the payload
|
||||
shape, gated on frequency, not on principle.** Dudley's measurement is
|
||||
unambiguous: gorge carving is *rare* (zero carved cells across all three
|
||||
production-scale benches; genuine carving needs a narrow two-basin-saddle
|
||||
geometry that's uncommon at continental working-grid resolution). That
|
||||
changes my answer from "add three dense fields" to "add them as
|
||||
sparse, not dense":
|
||||
|
||||
- `channel_depth: Vec<u16>` and `cliff_edge: Vec<bool>` **as dense per-gridunit
|
||||
arrays would be wasteful** given near-zero occupancy — RLE would crush them
|
||||
to a handful of runs, but PNG already wins on the honestly-dense fields, so
|
||||
paying for two more mostly-zero dense arrays is not free even if cheap.
|
||||
- **My call: fold the cliff case into the same sparse-feature-list shape as
|
||||
settlements/rivers, not a seventh/eighth dense array.** A
|
||||
`cliffs: Vec<CliffSegment>` field (`{ cell: (u16,u16), channel_depth: u16 }`,
|
||||
`cliff_edge` implied by list membership) parallel to `courses` — present
|
||||
only where it's true, empty in the overwhelming majority of windows T-1177
|
||||
measured. This keeps the common case (no gorge in this window) at zero
|
||||
bytes and matches the carrier three-way rule's own logic: this is
|
||||
rung-indexed invented detail (rule iii — hydrology solves once per body but
|
||||
the *carved representation* is a windowed-rung concern the same way course
|
||||
geometry is), so it belongs on the windowed payload as a sparse list, not
|
||||
baked into the dense sentinel arrays.
|
||||
- Elevation stays single-valued (`elev_q`) for every gridunit, including
|
||||
carved ones — it's the rim/dominant height per T-1177's own reasoning. The
|
||||
channel floor is a derived value (`elev_q - channel_depth`) the client
|
||||
computes only for gridunits that appear in the sparse `cliffs` list. No
|
||||
change to the existing single-height contract for the 99%+ common case.
|
||||
|
||||
**What the client is allowed to do with these fields — restated plainly,
|
||||
because "map art function, not data function" needs a hard line:**
|
||||
|
||||
- Color/style pixels from the classification fields (morphology, glaciation,
|
||||
vegetation, moisture) — yes, that's the entire job.
|
||||
- Interpolate/smooth/tween the color *presentation* between adjacent
|
||||
gridunits or between step-crosses (cosmetic) — yes, per the brief's premise
|
||||
3 investigation item.
|
||||
- Invent geometry, decide where a river bends, decide whether a cell is
|
||||
flooded, decide whether a cliff exists — **no, never.** Every one of those
|
||||
is a server-derived fact already on the wire (courses' points, morphology's
|
||||
water classes, the new `cliffs` list). If the client ever needs to guess
|
||||
at content the schema doesn't carry, that's a missing field, not a license
|
||||
to invent — send it back to this schema, don't let it leak into client-side
|
||||
derivation logic. This is the same discipline D-227 already enforces
|
||||
server-side; it just needs restating as a *client* rule now that the
|
||||
client receives finished-content data instead of raw geometry it used to
|
||||
interpolate itself.
|
||||
- Resolve names via the whole-body lookup tables (§1) — yes, a join, not a
|
||||
derivation.
|
||||
|
||||
---
|
||||
|
||||
## 3. Encoding continuity across steps
|
||||
|
||||
**One colorizer family, confirmed — and the wire schema I'm proposing is
|
||||
exactly what makes that cheap to guarantee, because every step ships the
|
||||
same six-plus-two field shape at a different spacing, never a different
|
||||
vocabulary.**
|
||||
|
||||
T-1143 §6's rule (one colorizer spans every rung, cited already for
|
||||
temperature in D-226 T-1124 §2 — *"deliberately not a separate district-tier
|
||||
quantization... one temperature colorizer spans both zoom levels"*) already
|
||||
set the precedent I'm extending to the whole schema: **every step's payload
|
||||
uses the identical field set, identical enum vocabularies
|
||||
(`MorphologyZone`'s 17 zones, `VegetationClass`'s 7 including `Marine`,
|
||||
`GlaciationGrade`'s 5 grades), at every step from global down to the deepest
|
||||
gridunit spacing.** The only thing that changes between steps is *sampling
|
||||
density*, never *meaning*. This is what makes the between-step magnification
|
||||
red flag (Tyre's #1) tolerable — a magnified hold of step-N's canvas for one
|
||||
step interval is showing the *same* classification vocabulary at coarser
|
||||
spacing, not a different color language snapping in, so the discontinuity is
|
||||
a resolution jump, never a palette jump.
|
||||
|
||||
**"Average-back across step boundaries" — confirm the mechanism, name the
|
||||
limit.** For continuous quantized fields (`elev_q`, `temp_dc`, `moisture_q`)
|
||||
average-back is safe and already implicit in how a coarser step's cell is
|
||||
itself computed (it's a spatial mean over finer terrain, same as today's
|
||||
Region/District relationship). For **categorical** fields
|
||||
(`morphology`, `vegetation`, `glaciation`), averaging is meaningless — you
|
||||
cannot average "grassland" and "forest" into a third zone. R3 in the
|
||||
delivered ladder design doc already flags this as an open risk
|
||||
("average-back unverified for categorical morphology gates") and T-1179's
|
||||
own per-field RLE table gives the concrete reason it matters: `morphology`
|
||||
and `vegetation` are the two fields that compress to near-nothing (6 runs
|
||||
at 330K cells) *because* they're genuinely piecewise-constant zone
|
||||
classifications, not smoothly-varying scalars — averaging them at a step
|
||||
boundary would destroy exactly the property that makes them cheap and
|
||||
legible. **My call: coarser steps must derive morphology/vegetation/
|
||||
glaciation as a fresh classification decision at that step's own spacing
|
||||
(the dominant-mode rule D-226's `dominant_district_type`/`dominant_zoning`
|
||||
precedent already uses for settlement aggregates — pick the plurality
|
||||
class, don't blend), never as a numeric average of the finer step's discrete
|
||||
values.** This is consistent with the whole "each step is a derivation
|
||||
sampled at gridunit resolution" framing (Tyre's D-166 corollary repoint) —
|
||||
a coarser step doesn't downsample a finer step's raster, it re-derives at
|
||||
its own spacing, and for categorical fields that re-derivation is a mode/
|
||||
plurality pick, not an arithmetic mean.
|
||||
|
||||
**"Different classes of content at lower zoom" (forest → clearings/ponds) —
|
||||
this is new vocabulary, not more density, and it needs to be scoped now so
|
||||
it doesn't sneak in unbounded.** The outline names this directly: *"at low
|
||||
enough zoom, a green forest biome may start showing clearings and ponds or
|
||||
such — to be determined and tinkered with."* My read: this is **sub-zone
|
||||
detail-scatter within an existing classification**, the same category as
|
||||
`voxel_relief`/`voxel_mosaic`'s invented detail-scatter (D-227, T-1154's
|
||||
octave-cutoff family) — not a new top-level vocabulary entry on
|
||||
`morphology`. A "forest with a clearing" is still `Forest` morphology at
|
||||
the district level; the clearing is deeper-step invented texture *within*
|
||||
that classification, resolved the same way finer steps already resolve
|
||||
everything else (fresh derivation at that step's `min_wavelength_m` cutoff).
|
||||
**Concretely: this does not need a new wire field or a vocabulary change at
|
||||
all** — it falls directly out of `min_wl_m` already being echoed per window
|
||||
(the octave cutoff field `DistrictWindowLayer` carries today) and the
|
||||
deeper step simply deriving at a smaller cutoff, which — per T-1154's
|
||||
measurement — is trivially affordable (17 ms parallel at the deepest
|
||||
realistic 83K-cell canvas). The one thing I'd ask the synthesis round to be
|
||||
disciplined about: keep this as "existing classification, finer octave
|
||||
detail," not "a growing zoo of sub-biome enum values" — that way it never
|
||||
threatens the one-colorizer-family guarantee, it just adds spatial texture
|
||||
underneath a class that was already decided at the coarser step. If a
|
||||
future pass wants clearings/ponds to carry *distinct* semantic meaning
|
||||
(e.g. a pond is walkable water, not decoration), that's a scope question
|
||||
for whoever owns block/tile classification next, not a step-continuity
|
||||
question — flagging the boundary, not answering past it.
|
||||
|
||||
---
|
||||
|
||||
## Summary for the wire contract / tagged-envelope synthesis (feeding Q2/Tyre)
|
||||
|
||||
For whoever writes the synthesis-round wire contract:
|
||||
|
||||
- **Per-step payload = the existing six dense fields (`morphology`,
|
||||
`elev_q`, `temp_dc`, `moisture_q`, `vegetation`, `glaciation`) + one new
|
||||
dense field (`settlement_id: Vec<u32>`) + two sparse feature lists
|
||||
(`courses` — already shipped — and new `cliffs`) + the existing `courses`
|
||||
name resolution now needs a whole-body `river_names` lookup alongside the
|
||||
existing `atlas_city_names` settlement lookup.**
|
||||
- **Encoding: PNG-per-field for every dense array** (T-1179's unambiguous
|
||||
winner — smallest and fastest at every measured size, no per-field
|
||||
hand-tuning needed since DEFLATE already captures the compressibility
|
||||
spread the per-field RLE table exposed). Sparse lists (`courses`,
|
||||
`cliffs`) stay MessagePack-native — they're already small (~1–2 KB
|
||||
typical per T-1170) and don't benefit from raster encoding.
|
||||
- **This payload is categorically larger than the 30 KB windowed-query cap
|
||||
at every measured size (21×–563×, T-1179)** — confirms the brief's own
|
||||
read that the tagged-envelope migration is not avoidable by a smarter
|
||||
encoding choice. I have nothing to add to that call beyond confirming the
|
||||
numbers hold for the schema I'm proposing (it's the same six fields T-1179
|
||||
measured, plus one more near-free dense field and two near-free sparse
|
||||
lists — doesn't change the order-of-magnitude verdict).
|
||||
- **Vocabulary stays frozen across every step.** No per-step schema
|
||||
variants, no new enum arms below the district-level vocabulary already
|
||||
governed by D-239. Deeper detail is resolution and octave cutoff, never a
|
||||
new field.
|
||||
@@ -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<u8>` | Classification zone — a pure function of terrain derivation, D-239 §6 vocabulary, no sim-time input. **§(e) note:** the existing `Lake`/`OpenOcean` discriminants gain a hydrology-basin-membership data source (post-ratification fix, CLOSED) — no wire change, same field, better-sourced values. |
|
||||
| `elev_q` | `Vec<u8>` | Height — geology, not weather |
|
||||
| `moisture_q` | `Vec<u8>` | Climate baseline (region-computed-once-inherited per D-243 §3), not a live sim tick value |
|
||||
| `vegetation` | `Vec<u8>` | Derived from climate baseline + morphology, same determinism class |
|
||||
| `settlement_id` | `Vec<u32>` | Settlement placement is a generation-time fact, not sim state |
|
||||
| `courses` | `Vec<RiverCourse>` | River geometry is invented deterministically (T-1170), never moves. **§(e) note:** an overflow lake basin's outlet edge extends into this list once the D8 sourcing fix (§(e)) lands — the same field, no new carrier. |
|
||||
| `cliffs` | `Vec<CliffSegment>` | Hydrology settled equilibrium (T-1177) — a pure function of seed, computed once per body, not re-solved on a clock |
|
||||
|
||||
**Sim-state plane (short-TTL, re-requested as sim time advances — the
|
||||
concrete instantiation of D-226's clock-bound region water-height model,
|
||||
`governance/decisions/architecture.md` §"`Water` is a dynamic depth
|
||||
state... a region property computed once per phase"):**
|
||||
|
||||
| Field | Type | Why sim-state |
|
||||
|---|---|---|
|
||||
| `glaciation` | `Vec<u8>` | 5-grade freeze state — this is the "frozen" half of the outline's frozen/flooded pair. Glaciation as *permanent ice geology* (a glacier that doesn't melt) is static; glaciation as *seasonal frost* is sim-state. Per D-226's clock model, both are the same field sampled at different clock buckets — the wire doesn't need two fields, it needs this one field re-requested on the seasonal clock-bucket rollover. |
|
||||
| `flooded` | **new**, `Vec<bool>` or folded bit — see below | The outline's explicit ask (`flooded: true/false`) that today's `morphology` water-zone classes don't cleanly separate from permanent sea/lake morphology. This is the region water-height-vs-elevation comparison D-226 already specifies as the mechanism — a derived boolean per gridunit, computed at request time from the current region water-height clock state, not stored geometry. |
|
||||
|
||||
**`temp_dc` is the genuinely ambiguous one, and I'm ruling it static, not
|
||||
sim-state, for a specific reason worth stating.** Temperature has a
|
||||
diurnal/seasonal clock term per D-226's model too, so it looks like a
|
||||
sim-state candidate at first read. But the Atlas map (this workshop's whole
|
||||
scope) is not a live weather display — it's a navigational reference the
|
||||
player consults at any moment, and the *baseline* climate classification
|
||||
(what makes a zone "arctic" vs "temperate" for morphology/vegetation
|
||||
purposes) is the region-computed-once-inherited value D-243 §3 promises,
|
||||
not the live diurnal offset. Treating `temp_dc` as static keeps it
|
||||
consistent with `moisture_q`/`vegetation`, which are already climate
|
||||
*baseline* values, not live samples — and it avoids a wire/cache
|
||||
inconsistency where morphology (static, climate-baseline-derived) and
|
||||
`temp_dc` (if sim-state) disagree about which clock reading produced the
|
||||
zone you're looking at. **If a future pass wants a live "current
|
||||
temperature" readout as a distinct overlay** (a genuinely different
|
||||
feature — a weather-report reading, not the map's base classification),
|
||||
that's an additive sim-state field on top of this, not a reclassification
|
||||
of the existing one. Flagging this reasoning explicitly since it's the one
|
||||
plane assignment that isn't obvious from the field name alone — Jeroen's
|
||||
"map time axis" ruling names frozen/flooded specifically, and I'm reading
|
||||
that specificity as intentional (temperature wasn't named alongside them),
|
||||
not an oversight.
|
||||
|
||||
### The staleness axis, stated per field — why each side, explicitly
|
||||
|
||||
The coordinator flagged this precisely: the plane split isn't "things that
|
||||
sound weather-related vs. things that don't," it's a real distinction each
|
||||
field needs its own stated reason for, because two fields with
|
||||
superficially similar names (`glaciation`, `temp_dc`) land on opposite
|
||||
sides and a reader shouldn't have to infer why.
|
||||
|
||||
- **`glaciation` → SIM-STATE, because "frozen" is seasonal, not
|
||||
geological.** The field's own doc comment already frames it as a
|
||||
"5-grade" classification, and D-226's clock model treats freeze/thaw as
|
||||
exactly the kind of clock-bucket-rollover term (diurnal/seasonal
|
||||
phase-stepped, `RegionPhase` memoized per bucket) the whole
|
||||
"current sim state" framing in Jeroen's ruling is about. A gridunit that
|
||||
reads `glaciation: 4` (heaviest grade) in winter and a lower grade in
|
||||
summer is not lying about the world's geology — it's answering "what
|
||||
does this look like right now," which is precisely what a short-TTL,
|
||||
re-requested-on-clock-rollover field is for. **Permanent ice sheets are
|
||||
not a separate wire concept** — a body-wide-permanently-glaciated
|
||||
region still reads a high grade at every clock sample, so the sim-state
|
||||
framing costs nothing for the "always frozen" case; it only pays off
|
||||
(shorter TTL, re-fetch on season change) for the seasonal case, which is
|
||||
exactly where the map needs to be honest about "right now" rather than
|
||||
"always."
|
||||
- **`flooded` → SIM-STATE, for the identical reason on the water axis.**
|
||||
D-226's own water-height model states this almost verbatim: *"Depth is
|
||||
time-varying via a deliberately cheap, deterministic, clock-bound
|
||||
water-height... floodplain / tidal-flat / seasonal-river emerge rather
|
||||
than being placed."* `flooded` is a derived comparison
|
||||
(region-water-height-at-current-clock-phase vs. local elevation), not
|
||||
stored geometry — it cannot be static by construction, because the same
|
||||
gridunit legitimately answers `true` at high water and `false` at low
|
||||
water within one static-geometry payload's entire cached lifetime.
|
||||
- **`temp_dc` → STATIC, because the *map's* temperature reading is a
|
||||
climate-baseline classification, not a live weather sample.** This is
|
||||
the one that needs the explicit contrast: `temp_dc` *could* have a
|
||||
diurnal/seasonal clock term exactly like `glaciation`/`flooded` do (the
|
||||
same D-226 clock model covers temperature too — "one clock model drives
|
||||
temperature, flooding, tides, snow..."), so the distinction here isn't
|
||||
"does this field have a clock term at all" (glaciation, flooded, AND
|
||||
temp_dc all do, in the underlying sim). The distinction is **what the
|
||||
Atlas map is using the field for.** `temp_dc` on this payload feeds the
|
||||
*zone classification* — it's read alongside `morphology`/`vegetation`
|
||||
to answer "what kind of place is this," a determinism-class question
|
||||
(D-227: the same seed always classifies this gridunit as arctic
|
||||
tundra), not a "what's the weather doing this afternoon" question. If
|
||||
`temp_dc` rode the sim-state plane, a player who opens the Atlas in
|
||||
winter vs. summer would see the *classification itself* flicker between
|
||||
TTL refreshes even though the underlying terrain and biome haven't
|
||||
changed — that's a worse experience than the actual live-diurnal-shift
|
||||
case (frozen/flooded), where the flicker *is* the honest answer to
|
||||
"what does this look like right now." **`glaciation`/`flooded` sim-state
|
||||
because their live value IS the map-relevant fact; `temp_dc` static
|
||||
because its live value is not what the map is answering with it** — same
|
||||
underlying clock-bound sim mechanism, different consumer question, hence
|
||||
opposite plane assignment. This is the reasoning that makes the split
|
||||
non-arbitrary rather than a guess from the field name.
|
||||
|
||||
### Separate planes vs. bitfields — the byte-cost comparison Jeroen asked for
|
||||
|
||||
Two candidate shapes for the sim-state plane, priced against T-1179's
|
||||
measured rates (6.00 bytes/cell raw MessagePack, ~0.32× that with PNG
|
||||
per-field ≈ 1.9 bytes/cell effective):
|
||||
|
||||
**Option 1 — separate L8 planes, one byte per field (`glaciation:
|
||||
Vec<u8>` already ships this way; `flooded: Vec<u8>` as 0/1 or reusing the
|
||||
existing byte width).** Cost: two single-channel PNG-encoded planes.
|
||||
Stig's measurement ⑤ already names the win here directly — *"L8 is 4–9×
|
||||
cheaper at every size... a free win if any wire field... can ship
|
||||
single-channel before the client colorizes it"* — and `glaciation`
|
||||
already is single-channel. At 330K gridunits, PNG-per-field measured
|
||||
638,382 bytes total across all **six** existing fields (T-1179); a rough
|
||||
per-field share (not uniform, since fields compress differently, but
|
||||
`glaciation`'s own RLE run-count — 28.7% of dense at 330K, "moderately
|
||||
compressible" — sits mid-table) puts one sim-state plane at roughly
|
||||
tens of KB PNG-encoded, not hundreds. Two separate planes (glaciation +
|
||||
flooded) roughly doubles that share but each stays independently
|
||||
requestable — the client can re-fetch just the sim-state pair without
|
||||
touching the static-geometry payload at all, which is the entire point of
|
||||
splitting them.
|
||||
|
||||
**Option 2 — packed bitfield, both sim-state facts folded into fewer
|
||||
bytes (e.g. glaciation's 3 real bits + a flooded bit packed into one byte,
|
||||
per T-1179's bit-packing measurement).** Cost: T-1179's bit-packing
|
||||
number is unambiguous and directly on point — bit-packing alone buys
|
||||
"~17–18% off raw... real but modest," and critically: **"packing bits
|
||||
first actually hurts DEFLATE's job on the low-entropy fields... by
|
||||
destroying their byte-aligned run structure; DEFLATE prefers finding runs
|
||||
of identical raw bytes over finding runs of identical bit-groups spread
|
||||
across byte boundaries."** `glaciation` and a hypothetical `flooded`
|
||||
boolean are exactly the low-entropy, spatially-coherent case (glaciation
|
||||
already showed moderate-to-good compressibility; a flooded boolean over a
|
||||
coherent water body is likely similar or better) that Option 2's own
|
||||
measured finding says bit-packing actively hurts.
|
||||
|
||||
**My call: Option 1, separate L8/single-byte planes, PNG-per-field
|
||||
encoded — not bitfields.** Three independent reasons converge:
|
||||
|
||||
1. **T-1179's own measured finding rules bit-packing out on compression
|
||||
grounds** for exactly this kind of field (low-entropy, spatially
|
||||
coherent) — PNG-per-field already beats bit-packed-then-PNG'd at every
|
||||
size in the table (0.32×/0.30×/0.32× vs 0.53×/0.56×/0.54× across
|
||||
330K/2.07M/8.3M).
|
||||
2. **Stig's measurement ⑤ prices the upload-side win of single-channel
|
||||
planes directly** — L8 is 4–9× cheaper than RGBA8, and that win is
|
||||
available *because* the planes are separate single-channel textures,
|
||||
not because they're bit-packed. Packing bits into one byte doesn't
|
||||
change the texture channel count on the client side; it just makes the
|
||||
CPU-side unpack step more expensive for a compression loss.
|
||||
3. **The TTL-split's whole point is independent re-fetchability.** A
|
||||
packed byte containing both a static bit (if one were ever folded in)
|
||||
and a sim-state bit would force the *entire byte* to inherit the
|
||||
shorter TTL — you can't selectively re-request half a byte. Keeping
|
||||
`glaciation` and `flooded` as separate planes (both sim-state, so this
|
||||
specific tension doesn't bite today) is what keeps the TTL boundary
|
||||
*aligned with the field boundary*, which is the property this whole
|
||||
sub-task is asking for. Folding sim-state and static fields into the
|
||||
same byte would be the one packing move that actually breaks something
|
||||
structural, not just costs a few percent — good thing neither
|
||||
candidate here needs that (both `glaciation` and `flooded` are
|
||||
sim-state, so packing them together, if ever done, wouldn't cross the
|
||||
TTL boundary — but I'm not recommending it anyway, given point 1).
|
||||
|
||||
**Concrete byte cost, stated plainly:** at the PNG-per-field encoding
|
||||
already adopted for the rest of the schema, the sim-state plane (two L8
|
||||
fields, `glaciation` + `flooded`) costs on the order of the same
|
||||
per-field share any other moderately-compressible classification field
|
||||
costs in T-1179's table — a small fraction of the total step-canvas
|
||||
payload, not a meaningfully separate budget line. The TTL split is a
|
||||
**freshness/re-request-cadence decision, not a bytes-saved decision** —
|
||||
splitting the planes doesn't shrink the payload, it lets the client avoid
|
||||
re-fetching the (much larger) static-geometry majority of the payload
|
||||
when only the sim-state pair has gone stale. That's the actual value:
|
||||
bandwidth saved on **re-fetch frequency**, not on first-fetch size.
|
||||
|
||||
### Schema, final form
|
||||
|
||||
```
|
||||
StepCanvas {
|
||||
// envelope/echo fields (existing pattern, carried forward)
|
||||
center, step_index, gridunit_spacing_m, min_wl_m, ...
|
||||
|
||||
// STATIC GEOMETRY PLANE — cached indefinitely-fresh, D-227 determinism
|
||||
morphology: Vec<u8>, // dense, PNG-per-field — Lake/OpenOcean discriminants now
|
||||
// basin-sourced from HydrologyResult (§(e), CLOSED), no wire change
|
||||
elev_q: Vec<u8>, // dense, PNG-per-field
|
||||
temp_dc: Vec<i16>, // dense, PNG-per-field — climate baseline, not live reading
|
||||
moisture_q: Vec<u8>, // dense, PNG-per-field
|
||||
vegetation: Vec<u8>, // dense, PNG-per-field
|
||||
settlement_id: Vec<u32>, // dense, PNG-per-field (new)
|
||||
courses: Vec<RiverCourse>, // sparse, MessagePack (existing, T-1170) — overflow-basin outlet
|
||||
// edges extend into this list once the D8 sourcing fix (§(e)) lands
|
||||
cliffs: Vec<CliffSegment>, // sparse, MessagePack (new, ratified lead-interview-1)
|
||||
|
||||
// SIM-STATE PLANE — short-TTL, re-requested on clock-bucket rollover
|
||||
glaciation: Vec<u8>, // dense, PNG-per-field, L8
|
||||
flooded: Vec<u8>, // dense, PNG-per-field, L8 (new)
|
||||
}
|
||||
```
|
||||
|
||||
**Note on the lakes fix (§(e)):** no new field appears in this struct.
|
||||
The originally-proposed `water: Vec<u8>` and `lakes: Vec<LakeBasin>`
|
||||
candidates are both withdrawn — Dudley's post-ratification finding
|
||||
(`MorphologyZone::Lake` already exists, just poorly sourced) means the
|
||||
fix is a **derive-pipeline data-source change**, not a wire-schema
|
||||
addition. `morphology` and `courses` carry annotated notes above marking
|
||||
where the fix lands; no field count changes.
|
||||
|
||||
**`CliffSegment` shape** (unchanged from round 1, restated for
|
||||
completeness): `{ cell: (u16, u16), channel_depth: u16, /* cliff_edge
|
||||
implied by list membership */ }` — direct, non-lossy carry of
|
||||
`HydrologyResult`'s `channel_depth_scaled`/`cliff_edge` fields (Dudley,
|
||||
T-1177), sparse list parallel to `courses`, `#[serde(default)]` empty on
|
||||
non-carving windows.
|
||||
|
||||
**Client rule, restated once more since the TTL split makes it sharper:**
|
||||
the client may cache the static-geometry plane forever and treat the
|
||||
sim-state plane as a separately-expiring sub-fetch — but it may never
|
||||
*infer* flooded/frozen state from the static plane (e.g. inferring
|
||||
"flooded" from `morphology`'s water-zone classes as a substitute for the
|
||||
real `flooded` field). The two planes answer different questions
|
||||
(permanent water body vs. current water-height-vs-elevation state) and
|
||||
conflating them client-side would silently violate the very TTL split this
|
||||
section defines.
|
||||
|
||||
---
|
||||
|
||||
## (b) Region-as-step-0 — CORRECTED post-ratification (Jeroen's review; Tyre doing the parallel correction in his own amendment texts)
|
||||
|
||||
**Status update, stated plainly before the corrected text: my three-way
|
||||
"CONFIRMED" closure below was wrong on rung *count*, right on
|
||||
*spacing intuition*.** Jeroen's post-ratification review caught the gap:
|
||||
I collapsed two distinct rungs into one. The corrected model, per the
|
||||
coordinator's relay:
|
||||
|
||||
> **GLOBAL is rung 0** — the body-surface opener, a **VARIABLE-extent**
|
||||
> canvas (the body's entire region grid, one gridunit per region — this
|
||||
> is the D-243 elastic seam **made visible on the wire**, not hidden above
|
||||
> the ladder as I originally wrote). This is the **canonical, always-kept
|
||||
> tier**.
|
||||
> **REGION is rung 1** — the largest **FIXED**-size rung: viewport-sized,
|
||||
> evictable, exactly like every other step below it.
|
||||
|
||||
My original text conflated these because I reasoned from *spacing* alone
|
||||
("step 0 needs a D-243 rung; Region is the coarsest one; therefore step 0
|
||||
IS Region") and missed that the opener's defining property isn't its
|
||||
gridunit spacing at all — it's that its **canvas extent is per-body
|
||||
floating** (`round(2πR / 204.8 km)` regions around the equator, D-243's
|
||||
own elastic-seam formula), which is categorically different from every
|
||||
other step's fixed-px-budget canvas (my own (a)/(d) sections, Dudley's
|
||||
3840×2160 convention). A rung whose *extent* varies per body cannot be
|
||||
"the Region rung" in the same sense Region-at-rung-1 is, even though both
|
||||
sample at region-sized gridunits. **Spacing and extent are two different
|
||||
axes, and I only checked one.**
|
||||
|
||||
### The corrected reasoning, restated on the right axis
|
||||
|
||||
1. **Gridunit spacing at the opener is still region-sized — that part of
|
||||
my original argument holds unchanged.** Nothing about the correction
|
||||
invalidates why region-spacing is the right sampling density for a
|
||||
whole-body view (T-1179's cell-count-scales-with-area argument,
|
||||
D-226(d)'s whole-body-planetary-layer boundary at anything finer). The
|
||||
opener's gridunits ARE region-sized. What I got wrong is naming that
|
||||
fact "step 0 = the Region rung" instead of "step 0 samples at Region
|
||||
spacing, but is its own rung above Region because its extent is the
|
||||
variable whole-body grid, not a fixed viewport."
|
||||
2. **The canonical/always-keep property moves with the correction, and
|
||||
this is the load-bearing consequence for my (a) TTL-split work.**
|
||||
Dudley's round-1 global-tier costing (~174 MB PNG-encoded across all
|
||||
273 bodies) and my own (a) section's "static plane cached
|
||||
indefinitely-fresh... only the global tier keep-always" language were
|
||||
written assuming Region *was* step 0 — they need to be read as
|
||||
describing **rung 0 (the opener)**, not rung 1. Region (rung 1) is
|
||||
now correctly a **storage-evictable** sub-global tier, same as every
|
||||
rung below it — it does NOT inherit "canonical" just because it's the
|
||||
largest fixed rung. This is consistent with, not a reversal of,
|
||||
Jeroen's lead-interview-1 ruling ("canonical survives only as the
|
||||
global tier") — the correction is *which rung is "the global tier,"*
|
||||
not whether the keep-always/evictable split itself holds.
|
||||
3. **The elastic seam is not "above the ladder, never derived at" as I
|
||||
originally wrote — it's rung 0's own canvas-extent parameter.** My
|
||||
original bullet 3 said "the elastic seam sits *above* [Region], at
|
||||
the per-body planetary count, which is not itself a spacing value the
|
||||
Atlas derives at." That's the specific sentence Jeroen's correction
|
||||
overturns: the elastic seam IS what the Atlas derives at rung 0 —
|
||||
`round(2πR / 204.8 km)` regions isn't a hidden background fact, it's
|
||||
the literal gridunit count of the opener's canvas, visible to the
|
||||
player as "how big does this body's region-grid look." This is a
|
||||
genuine strengthening of D-166's "seamless zoom ladder" mandate, not
|
||||
a complication: the elastic seam was always going to be visible
|
||||
*somewhere* the first time a player opens two differently-sized
|
||||
bodies' Atlas views side by side, and rung 0 is the correct, honest
|
||||
place for that to surface, rather than being smoothed over.
|
||||
|
||||
### What this changes in my payload schema (a) and wire schema (d) — corrected, not reworked
|
||||
|
||||
**No field-set change.** Rung 0's `StepCanvas` still uses the identical
|
||||
field set from (a) — same six-static-plus-two-sim-state dense fields,
|
||||
same two sparse lists, same whole-body name lookups. The correction is
|
||||
entirely about **which rung is canonical and what "extent" means at the
|
||||
top**, not about the payload contents. Concretely:
|
||||
|
||||
- `EncodedStepCanvas.width`/`.height` (Dudley's struct, §(d)) are **fixed
|
||||
constants at every rung except rung 0**, where they are instead
|
||||
`derive_region_count(body_radius_km)` — the same per-body function
|
||||
D-243's elastic seam already defines, not a new derivation. This is
|
||||
the one place in the whole ladder where `width`/`height` are a
|
||||
body-dependent value rather than the shared fixed-px-budget convention
|
||||
— worth a doc-comment on the struct so an implementer doesn't assume
|
||||
every response has the same canvas dimensions.
|
||||
- The **cache-tier "keep-always" retention floor (Stig's round-1 §4,
|
||||
Dudley's round-2 §(d) global tier) now pins specifically on rung 0's
|
||||
entries, keyed by `(body_id, rung=0)`** — not on `(body_id, rung=1
|
||||
/*Region*/)` as my prior text implied. Region (rung 1) entries flow
|
||||
through the same storage-TTL/time-since-last-visit eviction every
|
||||
other sub-global rung uses.
|
||||
- **Step index numbering shifts by one relative to my prior text**
|
||||
throughout this document: everywhere I previously wrote "step 0" I
|
||||
should now write "rung 0 (the opener)," and every reference to
|
||||
"Region as the top of the sub-global ladder" now correctly reads as
|
||||
"Region, rung 1, the first fixed-size step below the opener." I'm not
|
||||
renumbering every instance below for this correction pass — flagging
|
||||
the mapping once, here, as the authoritative key: **rung 0 = opener
|
||||
(variable, region-spaced gridunits, canonical) → rung 1 = Region
|
||||
(fixed, viewport-sized, evictable) → rung 2 = District → ... → deepest
|
||||
rung = whatever Jeroen's chunk(64m)-bottom ruling lands on.**
|
||||
|
||||
### Convergence status
|
||||
|
||||
Tyre is producing the equivalent correction directly in his amendment
|
||||
texts (D-243 gridunit entry, the step-ladder table) per the coordinator's
|
||||
note — I'm not routing this one for a fresh cross-check the way I did the
|
||||
original (b) confirmation, since Jeroen's own review is the authority
|
||||
here and both of us are independently correcting against the same
|
||||
relayed ruling, not proposing competing readings. If Tyre's amendment
|
||||
text names the rungs differently (e.g. a different rung-0 label than
|
||||
"opener/GLOBAL"), I'll conform my terminology to his filed text rather
|
||||
than contest it — this section's job is getting the *architecture* right
|
||||
for my schema's sake, not owning the naming.
|
||||
|
||||
---
|
||||
|
||||
## (c) Label de-dup at step seams — the concrete rule
|
||||
|
||||
Tyre's gray case (round-1 §3, bucket C): *"is client-side de-duplication...
|
||||
legal presentation logic, or does it risk client and server disagreeing
|
||||
about which gridunit owns this feature's label?"* His own read: *"legal,
|
||||
IF the server always ships feature identity (a stable id) rather than the
|
||||
client inferring identity from proximity."* My round-1 payload schema
|
||||
already ships exactly that precondition (`settlement_id` per gridunit,
|
||||
`edge_id` on every `RiverCourse`) — so the precondition is met, and the
|
||||
rule below is the concrete mechanism that makes the "legal" branch actual,
|
||||
not just theoretical.
|
||||
|
||||
### The rule
|
||||
|
||||
**Deterministic anchor choice, keyed on feature identity, never on which
|
||||
canvas/step happened to fetch first.**
|
||||
|
||||
**For settlements (`settlement_id` occupying a contiguous run of
|
||||
gridunits within one step's canvas, and potentially spanning a step or
|
||||
canvas-tile seam at low zoom):**
|
||||
|
||||
> The label anchor is the **arithmetic-mean gridunit position of every
|
||||
> cell carrying that `settlement_id` within the current step's canvas**,
|
||||
> rounded to the nearest gridunit via a fixed tie-break rule (round half
|
||||
> toward the lower `(x,y)` in row-major order — an arbitrary but
|
||||
> *stable* tie-break, the same discipline `label_lake_basins`' row-major
|
||||
> discovery order already uses for determinism, T-1177). This is a pure
|
||||
> function of `(step, canvas_bounds, settlement_id, the cells carrying
|
||||
> it)` — same inputs, same anchor, always, on both the server-echo side
|
||||
> and any two clients computing it independently.
|
||||
|
||||
**Why this handles the seam case without special-casing it:** a
|
||||
settlement whose footprint is split across two adjacent canvas tiles at
|
||||
the same step (the mosaic case, not the step-cross case) has each tile
|
||||
compute its own mean over *only the cells that tile actually received* —
|
||||
which means the two tiles will independently compute two different
|
||||
anchor points (each tile's own partial-footprint centroid), and **both
|
||||
draw a label**. This sounds like the bug the gray case worried about, but
|
||||
it isn't, once you name the actual failure mode precisely: the risk was
|
||||
never "two labels for one settlement," it's "two labels disagreeing about
|
||||
*which* settlement, or a label a player can't correlate with the
|
||||
feature." A settlement large enough to span a canvas-tile seam at a given
|
||||
step is, by construction, large enough that *both* halves are visibly the
|
||||
same feature to the player (its footprint fill color and `settlement_id`
|
||||
match on both tiles) — showing the name near each visible fragment is
|
||||
correct map behavior, not a duplication bug, the same way a real
|
||||
paper-map atlas repeats a country's name across each visible page-tile
|
||||
its territory spans. **The actual dedup requirement is narrower than "one
|
||||
label globally": it's "never show two different names for the same id,
|
||||
and never let two different ids merge into one label."** Both are
|
||||
structurally impossible under this rule, since the anchor computation and
|
||||
the name lookup are both pure functions of `settlement_id` — there is no
|
||||
code path where the id resolves to different text on two tiles.
|
||||
|
||||
**Where genuine single-label dedup DOES matter: the step-cross case, not
|
||||
the tile-seam case.** When the player crosses a step boundary (not a
|
||||
tile boundary within one step), the *previous* step's held canvas and the
|
||||
*newly arrived* step's canvas briefly coexist during hold-fetch-swap
|
||||
(Stig's round-1 §2). Both canvases might carry labels for the same
|
||||
settlement during that transition window. **Rule: the annotation layer
|
||||
only ever draws labels from the currently-authoritative canvas — the
|
||||
held (stale, about-to-be-replaced) canvas's annotation layer is not drawn
|
||||
during the hold interval, only its terrain texture is (magnified, per the
|
||||
between-step-magnification red flag).** This isn't a new mechanism — it
|
||||
falls directly out of Stig's own architecture: the annotation layer is
|
||||
described as an *unscaled sibling* that "re-run[s] the annotation layer's
|
||||
world→screen transform against the new step's bounds" on arrival, which
|
||||
already implies the annotation layer belongs to one step's data at a
|
||||
time, not both simultaneously. I'm stating the corollary explicitly
|
||||
because "which canvas's annotation layer is currently drawn" wasn't
|
||||
spelled out as a rule anywhere in round 1's four documents, and it's
|
||||
exactly the kind of implicit assumption that produces the doubled-label
|
||||
bug if an implementer doesn't carry it forward.
|
||||
|
||||
**For river name labels (a `RiverCourse` polyline, potentially cropped
|
||||
differently by two adjacent windows per Ruling 1e's window-independence
|
||||
invariant):** the same identity-first principle, adapted to a line
|
||||
feature rather than a point/area feature:
|
||||
|
||||
> The label anchor for a named river is the **midpoint, by arc-length, of
|
||||
> the *cropped* `points` array the current canvas received for that
|
||||
> `edge_id`** — not a global midpoint of the river's full extent (which
|
||||
> the current canvas may not have visibility into at all, per Ruling 1e:
|
||||
> the geometry is a pure function of `(seed, body, edge, rung)`, cropped
|
||||
> per-window, never re-parametrized). Each canvas places its own river
|
||||
> label at its own cropped-midpoint, independently. Same non-issue as
|
||||
> settlements: a long trunk river visibly spans multiple canvas tiles, and
|
||||
> a label per visible tile is correct cartography, not a duplication bug,
|
||||
> as long as every tile's label resolves to the *same* name via the
|
||||
> `river_names` lookup (§(d) below) keyed on the same `edge_id` — which it
|
||||
structurally does, by construction, same argument as settlements.
|
||||
|
||||
**One explicit non-goal, stated so a future implementer doesn't
|
||||
over-engineer this:** I am **not** proposing any cross-tile or
|
||||
cross-canvas coordination protocol (no "who owns this label" arbitration,
|
||||
no server-side label-placement pass). The rule above needs zero new wire
|
||||
data and zero new server logic — it's entirely a client-side
|
||||
presentation policy operating on data the schema in (a) already ships
|
||||
(`settlement_id`, `edge_id`, the cropped geometry). This is deliberately
|
||||
inside Tyre's bucket-A "legal interpolation" territory: the client is
|
||||
*placing* a label for an already-identified thing, never *deciding* that
|
||||
a thing exists or what it's called.
|
||||
|
||||
---
|
||||
|
||||
## (d) Final wire schema — CLOSED (envelope framing confirmed by Dudley)
|
||||
|
||||
**Envelope-framing question resolved: shape (a), one flat tagged
|
||||
message.** Routed to Dudley via the coordinator — asked whether the
|
||||
tagged envelope frames the full `StepCanvas` as one flat message (my
|
||||
default assumption, matching today's `DistrictWindowLayer` shape scaled
|
||||
up) or splits dense/sparse into separately-tagged sub-messages for
|
||||
progressive delivery. **Dudley confirmed (a) without qualification**
|
||||
(`dudley-round2.md` §(a)): every field comes off the same row-chunked
|
||||
derive pass in one traversal, so a dense/sparse split saves no server
|
||||
compute and only adds round-trips and partial-state handling — the
|
||||
identical argument D-225 already used against per-layer whole-body
|
||||
requests. The progressive-paint UX case (terrain visible before
|
||||
courses/cliffs finish decoding) is real but solved client-side for free:
|
||||
courses/cliffs are a tiny fraction of total decode time (a few KB against
|
||||
a multi-hundred-KB-to-tens-of-MB dense payload), so the client paints the
|
||||
terrain RTT layer immediately and defers the annotation layer's draw
|
||||
calls by a frame — a rendering-order choice, not a wire-protocol one. He'd
|
||||
revisit only if a future profiling pass found the dense and sparse derive
|
||||
passes on genuinely different cost timelines; nothing measured this
|
||||
workshop shows that (courses cost +0.09–0.21 ms against a ~5 ms baseline
|
||||
— negligible, not staggered).
|
||||
|
||||
**Concrete carrier, per Dudley's `StepCanvasResponse` design** (matching
|
||||
my field list exactly, field-for-field, including the resolved
|
||||
`cliffs: Vec<CliffSegment>` sparse shape):
|
||||
|
||||
```rust
|
||||
pub struct StepCanvasResponse {
|
||||
pub body_id: String,
|
||||
pub step_index: u32,
|
||||
pub center: (i64, i64), // echoed, same staleness-guard pattern as district_window
|
||||
pub status: StepCanvasStatus, // Ready | Pending | Error(String)
|
||||
pub canvas: Option<EncodedStepCanvas>,
|
||||
}
|
||||
|
||||
pub struct EncodedStepCanvas {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub morphology: EncodedField, // PNG-per-field — Lake/OpenOcean now basin-sourced (§(e), CLOSED)
|
||||
pub elev_q: EncodedField, // PNG-per-field
|
||||
pub temp_dc: EncodedField, // PNG-per-field
|
||||
pub moisture_q: EncodedField, // PNG-per-field
|
||||
pub vegetation: EncodedField, // PNG-per-field
|
||||
pub settlement_id: EncodedField, // PNG-per-field (my new dense field)
|
||||
pub glaciation: EncodedField, // PNG-per-field, L8 — sim-state plane
|
||||
pub flooded: EncodedField, // PNG-per-field, L8 — sim-state plane (my new field, needs adding to his struct)
|
||||
pub courses: Vec<RiverCourse>, // sparse, MessagePack-native — overflow-basin outlets extend this (§(e))
|
||||
pub cliffs: Vec<CliffSegment>, // sparse, MessagePack-native
|
||||
}
|
||||
```
|
||||
|
||||
**One reconciliation note against Dudley's struct as literally drafted:**
|
||||
his `dudley-round2.md` §(a) code sample predates my TTL-split work in this
|
||||
document and lists `glaciation` alongside the other five dense fields
|
||||
without a `flooded` field or a plane distinction — that's sequencing, not
|
||||
disagreement (his draft cites "Araminta's new dense field
|
||||
`settlement_id`" by name, so he was already building directly off my
|
||||
round-1 schema, just before the sim-state-plane work landed in this
|
||||
round-2 pass). `EncodedStepCanvas` needs `flooded` added as a ninth
|
||||
field. **No further field is needed for lakes** — §(e)'s final answer
|
||||
(post-ratification, superseding an earlier draft of this section that
|
||||
proposed `water`/`lakes` as new fields) reuses `morphology` and
|
||||
`courses` unchanged; the lakes fix is a derive-pipeline data-source
|
||||
change (Dudley's basin-membership sampling into
|
||||
`derive_morphology_zone`, plus a follow-up D8-wiring task for the
|
||||
outflow-course extension), never a wire-schema addition. Dudley's
|
||||
whole-payload-together envelope argument still applies to `flooded` —
|
||||
one more field derived in the same row-chunked pass doesn't reopen the
|
||||
flat-vs-split question — but there is nothing further to reconcile on
|
||||
the lakes item specifically; it resolved to zero new fields.
|
||||
|
||||
### Encoding, confirmed unchanged from round 1
|
||||
|
||||
**PNG-per-field for every dense array field, MessagePack-native for every
|
||||
sparse list field.** Nothing in round 1's cross-agent convergence or
|
||||
Jeroen's lead-interview-1 rulings changes this — it's the unambiguous
|
||||
T-1179 winner (smallest *and* fastest at every measured size,
|
||||
21×–563× better than the alternatives against the reference cap), and no
|
||||
other participant's round-1 position argued for a different encoding.
|
||||
Restating the two encoding rules precisely, now that the field list is
|
||||
final:
|
||||
|
||||
- **Dense fields** (`morphology`, `elev_q`, `temp_dc`, `moisture_q`,
|
||||
`vegetation`, `settlement_id`, `glaciation`, `flooded`): each field is
|
||||
its own PNG-encoded plane. `glaciation` and `flooded` specifically as
|
||||
**L8** (single-channel, per Stig's measurement ⑤ finding that L8 is
|
||||
4–9× cheaper than RGBA8 at every size) — both are already single-byte
|
||||
values with no need for a wider channel format.
|
||||
- **Sparse fields** (`courses`, `cliffs`): MessagePack-native
|
||||
`Vec<Struct>`, unencoded beyond the envelope's own framing — both are
|
||||
already small relative to the dense planes (T-1170's ~1–2 KB typical
|
||||
for courses; cliffs smaller still given the rarity finding), and
|
||||
MessagePack's compact `bin`/array framing is already efficient at these
|
||||
sizes per T-1179's own measured 6.00 bytes/cell raw rate finding (no
|
||||
wasted per-element type tags).
|
||||
|
||||
### The full field list, final
|
||||
|
||||
Restating (a)'s schema in wire-contract form, the complete answer to "the
|
||||
per-gridunit payload schema + named-feature encoding + envelope framing"
|
||||
(brief Expected Output 4):
|
||||
|
||||
**Per-step-canvas payload** (rides the tagged envelope, §1d's amendment
|
||||
text — the new carrier, not `district_window`):
|
||||
- 6 static + 2 sim-state dense fields (listed in (a) — field count
|
||||
**unchanged** from the original round-2 pass; §(e)'s lakes fix reuses
|
||||
`morphology`, adding no field)
|
||||
- 2 sparse feature lists (`courses`, `cliffs`) — `courses` gains
|
||||
overflow-basin outlet edges once §(e)'s D8-wiring follow-up lands, same
|
||||
field, no new carrier
|
||||
- Envelope/echo fields: `center`, `step_index` (replacing `granularity`/
|
||||
`granularity_v2` per Tyre's §1c `select_rung`-replacement ruling),
|
||||
`min_wl_m` (unchanged purpose — octave cutoff echo, still needed for
|
||||
cache-key correctness per the existing doc comment's reasoning). At
|
||||
rung 0 (the opener), `width`/`height` are body-dependent
|
||||
(`derive_region_count(body_radius_km)`) rather than the fixed
|
||||
3840×2160-class budget every other rung uses — per (b)'s correction.
|
||||
|
||||
**Whole-body lookups** (unchanged carrier — the existing names-only
|
||||
request path, D-223's shape, fetched once per body, never inside the
|
||||
step-canvas envelope):
|
||||
- `atlas_city_names` (existing)
|
||||
- `river_names: BTreeMap<u32 /* edge_id or trunk-root id */, String>`
|
||||
(new, my round-1 proposal, unchanged in round 2 — nothing in round 1's
|
||||
cross-agent discussion touched river naming, so I'm carrying it forward
|
||||
as-is)
|
||||
|
||||
### What round 1 already confirmed and I'm not re-arguing
|
||||
|
||||
- The tagged-envelope migration itself: triggered, not merely likely
|
||||
(Tyre §1d, unanimous across Dudley/Araminta/Tyre in round 1).
|
||||
- Viewport-sized canvases at every rung below the opener (Dudley + Tyre,
|
||||
independently argued convergence) — **corrected per (b)'s post-
|
||||
ratification fix:** rung 0 (the opener, variable per-body extent) is
|
||||
the **one** exception; Region is now rung 1, a **fixed**, viewport-
|
||||
sized, evictable rung like every rung below it, not the exception
|
||||
rung. Every rung from Region (rung 1) down uses the viewport-sized
|
||||
policy — the "whole-body canvas" property belongs to rung 0 alone.
|
||||
- One colorizer family, same field set at every step (my round-1 §3,
|
||||
reconfirmed here as applying to the final field list including the new
|
||||
cliff/sim-state fields — nothing about the TTL split or the cliff
|
||||
ratification changes the "same vocabulary at every step" guarantee,
|
||||
since neither is a new *classification*, just a new *field*).
|
||||
|
||||
### Byte-size implication of the final schema (not a new measurement — reasoning from T-1179's numbers)
|
||||
|
||||
The final schema adds two new dense fields (`settlement_id`,
|
||||
`flooded` — `glaciation` already existed) and one new sparse field
|
||||
(`cliffs`) to T-1179's originally-measured six-field set. None of these
|
||||
change the order-of-magnitude verdict: T-1179's own six-field payload was
|
||||
already 21×–563× the 30 KB reference cap at the three measured sizes; two
|
||||
more near-free sparse/single-channel-dense fields (settlements are rare
|
||||
and spatially coherent per the RLE precedent; cliffs are measured as
|
||||
near-zero-occupancy) don't meaningfully change that ratio. **The
|
||||
tagged-envelope call remains settled by round 1's numbers — I'm not
|
||||
requesting a re-run of T-1179 for the final field list, since the
|
||||
schema's byte-cost character (dominated by the same
|
||||
`morphology`/`vegetation`-vs-`elev_q`/`temp_dc` compressibility split
|
||||
T-1179 already characterized) hasn't changed, only grown by a small,
|
||||
well-understood margin.**
|
||||
|
||||
---
|
||||
|
||||
## Summary for the lead interview
|
||||
|
||||
1. **(a) Payload schema, final:** 8 dense fields (6 static-geometry + 2
|
||||
sim-state: `glaciation`, new `flooded`) + 2 sparse lists (`courses`,
|
||||
new `cliffs`, the latter CLOSED per (b) below) + 2 whole-body name
|
||||
lookups (`atlas_city_names`, new `river_names`). TTL split is physical
|
||||
field separation, not a post-hoc policy — static plane cached
|
||||
indefinitely-fresh (with storage-budget eviction per Jeroen's
|
||||
amendment, distinct from staleness), sim-state plane short-TTL and
|
||||
independently re-fetchable. Byte cost: separate L8 planes win over
|
||||
bitfields on both compression (T-1179) and correctness (independent
|
||||
re-fetchability) grounds — ruled against packing. Staleness-axis
|
||||
reasoning stated per field, not inferred from the name: `glaciation`/
|
||||
`flooded` are sim-state because their *live* value is the map-relevant
|
||||
fact (seasonal frost, current water-height-vs-elevation); `temp_dc`
|
||||
stays static because the map consumes it as a climate-baseline
|
||||
classification input, not a live weather reading — same underlying
|
||||
D-226 clock mechanism, different consumer question.
|
||||
2. **(b) Rung 0 (opener) vs. rung 1 (Region): CORRECTED post-
|
||||
ratification, not the three-way "Region = step 0" closure this
|
||||
summary previously stated.** Jeroen's own review caught the gap: the
|
||||
opener is its **own rung** (rung 0, variable per-body extent —
|
||||
`round(2πR / 204.8 km)` regions, D-243's elastic seam made visible on
|
||||
the wire, canonical/always-kept) sampled at region-sized gridunits;
|
||||
Region is **rung 1**, the largest *fixed*, viewport-sized, evictable
|
||||
rung — not the canonical tier. My spacing intuition (region-sized
|
||||
gridunits at the top) was right; my rung *count* was wrong (I
|
||||
collapsed two rungs into one). Tyre is producing the equivalent
|
||||
correction in his own amendment texts in parallel; I'm conforming to
|
||||
his filed terminology rather than contesting it if it differs from
|
||||
"opener/GLOBAL." Also closed, unaffected by this correction: the
|
||||
cliff wire shape (`cliffs: Vec<CliffSegment>`, sparse list,
|
||||
zero-length when nothing carved, `#[serde(default)]`, parallel to
|
||||
`courses`) — Tyre confirmed his round-1 "new arrays over
|
||||
steal-a-bit" language was against bit-stealing an existing byte, not
|
||||
for dense per-gridunit arrays.
|
||||
3. **(c) Label de-dup: a deterministic, identity-keyed anchor rule**
|
||||
(mean-position/tie-break for point features, cropped arc-length
|
||||
midpoint for line features), computed independently per canvas/tile —
|
||||
explicitly *not* a cross-tile coordination protocol. The real seam
|
||||
risk was never "two labels," it was "two labels disagreeing" — both
|
||||
structurally prevented by the schema already carrying stable ids.
|
||||
Added the missing step-cross corollary (only the authoritative
|
||||
canvas's annotation layer draws during hold-fetch-swap) since no
|
||||
round-1 document stated it explicitly.
|
||||
4. **(d) Final wire schema: CLOSED.** PNG-per-field (dense) +
|
||||
MessagePack-native (sparse), unchanged encoding verdict from round 1,
|
||||
field list finalized per (a) with the cliff shape closed per (b).
|
||||
Envelope framing confirmed by Dudley (`dudley-round2.md` §(a)): one
|
||||
flat tagged `StepCanvasResponse`/`EncodedStepCanvas`, my default shape
|
||||
(a) held without qualification — every field derives in the same
|
||||
row-chunked pass, so splitting the wire buys no compute savings and
|
||||
only adds round-trip/partial-state cost; the progressive-paint UX case
|
||||
is solved client-side (deferred annotation-layer draw calls) for free.
|
||||
One implementation note carried forward: `EncodedStepCanvas` as
|
||||
Dudley drafted it needs `flooded` added as a ninth field (sequencing,
|
||||
not disagreement — his draft predates this round's TTL-split work) —
|
||||
the addition doesn't reopen the flat-envelope ruling, since the plane
|
||||
split is a client-side re-fetch-cadence concept, not a wire-framing one.
|
||||
|
||||
**Status at this point in the document: (a), (c), (d) fully closed, no
|
||||
open cross-checks. (b) carries a post-ratification correction**
|
||||
(rung 0/opener vs. rung 1/Region, per Jeroen's own review) rather than
|
||||
standing as originally filed — the correction is architectural (which
|
||||
rung is canonical, what "extent" means at the top), not a reopening of
|
||||
the field-set or wire-schema work in (a)/(d), both of which are
|
||||
unaffected. **One further item follows this summary, §(e) below (lakes)
|
||||
— read past this point before treating the document as final; the true
|
||||
closing status is stated at the end of §(e).**
|
||||
|
||||
---
|
||||
|
||||
## (e) CLOSED — lakes: morphology-fold, sourced from settled hydrology; endorheic cue via outflow-course presence
|
||||
|
||||
**Final answer, superseding my original "new `water` field" proposal
|
||||
below.** Dudley's post-ratification addendum (`dudley-interview2-response.md`
|
||||
§5) corrects a factual premise my original rejection of folding into
|
||||
`morphology` was built on — I verified his code-read directly
|
||||
(`district_profile.rs:556-565`, `generator.rs:1198-1223`,
|
||||
`features.rs:104-105`) before accepting it, not on his say-so. The
|
||||
reconciliation below is mine to make since I own encoding; his
|
||||
derive-pipeline slotting is adopted unchanged.
|
||||
|
||||
### What I got wrong originally, and what actually resolves it
|
||||
|
||||
**My original §(e) rejected folding into `morphology` on the premise
|
||||
that lake-vs-not would "require... widening an already-frozen
|
||||
vocabulary."** That premise is false. `MorphologyZone::Lake`
|
||||
(discriminant 1) **already exists** in the frozen 17-zone enum
|
||||
(`generator.rs:1202`, doc-commented *"Lake body — hub-and-spoke;
|
||||
perimeter access toward water"*) — confirmed by direct read, not
|
||||
assumed. The gap was never a missing vocabulary entry; it's a missing
|
||||
**data source**. Today, `derive_morphology_zone` emits `Lake` from a
|
||||
crude threshold (`ocean_fraction_q >= 60`, `district_profile.rs:563-565`)
|
||||
where `ocean_fraction_q` is a bilinear sample of `TerrainAnalysis
|
||||
.ocean_mask`, itself nothing more than `elev[i] < sea_level` on the raw
|
||||
heightmap (`features.rs:104`) — **zero connection to
|
||||
`HydrologyResult`'s settled-equilibrium basins.** The code's own comment
|
||||
at line 560 admits this ("Lake differentiation lives at ChunkContext") —
|
||||
it was already a known, named gap before this workshop, just never
|
||||
connected to the solver this workshop's ① measurement is about.
|
||||
|
||||
**My second objection — that per-cell basin membership is finer-grained
|
||||
than morphology's region-level classification — is answered by Dudley's
|
||||
sourcing fix, not by a new field.** The fix doesn't ask `morphology` to
|
||||
carry a coarser aggregate than the solver computes; it asks
|
||||
`derive_morphology_zone` to gain a **third input** (basin membership,
|
||||
sampled the identical bilinear way `ocean_fraction_q` already is) and
|
||||
emit the *existing* `Lake` discriminant when that membership test hits,
|
||||
falling through to today's heuristic otherwise. This is per-gridunit
|
||||
resolution at the SAME granularity every other `morphology` gate
|
||||
already operates at (`derive_morphology_zone` is called per-gridunit
|
||||
today, gated on `slope_q`/`elev_q`/`ocean_fraction_q` — all already
|
||||
per-cell bilinear samples of coarser working-grid fields). There was
|
||||
never an actual grain mismatch; I mis-stated the objection by treating
|
||||
"basin membership" as if it were categorically different from every
|
||||
other per-cell classification input `derive_morphology_zone` already
|
||||
consumes, when it's the identical shape.
|
||||
|
||||
**Verdict: fold into `morphology`. No new dense field. Zero new wire
|
||||
bytes.** `water: Vec<u8>` from my original proposal is withdrawn — the
|
||||
byte-cost argument I made for it (near-zero, PNG compresses contiguous
|
||||
classification blobs well) transfers unchanged to reusing `morphology`,
|
||||
except the cost is now not just near-zero but **exactly zero**, since no
|
||||
new array exists at all. This is a strictly better outcome than my
|
||||
original proposal on every axis I originally argued from (byte cost,
|
||||
frozen-vocabulary discipline, one-colorizer-family consistency) — the
|
||||
correction makes the schema simpler, not just different.
|
||||
|
||||
**Sea vs. Lake stays exactly as today** — `OpenOcean`/`Lake` are both
|
||||
already-existing discriminants; nothing about this fix touches the
|
||||
`ocean_fraction_q >= 80` open-ocean tier. Only the `Lake` emission site
|
||||
gains a second, more-authoritative trigger (basin membership) ahead of
|
||||
the existing heuristic fallback.
|
||||
|
||||
### Endorheic-vs-overflow — corrected mechanism, honest wiring caveat stated
|
||||
|
||||
**The coordinator's flagged weak point is real, and I'm ruling against
|
||||
my own original proposal's premise, not defending it.** My original
|
||||
"distinguishes via the sparse `cliffs` list" reasoning assumed carved
|
||||
`cliff_edge` cells would exist often enough to be a usable signal.
|
||||
T-1177's own POPULATION SURVEY closes that door completely: **zero
|
||||
carved cells, zero carved-outlet basins, across all 267 real committed
|
||||
bodies (100.00%)** — not "rare," observed *nowhere*. A visual cue wired
|
||||
through `cliffs` would never fire in practice. I checked this number
|
||||
directly in `measurements/t1177-hydrology.md` before accepting the
|
||||
correction, not on the coordinator's relay alone.
|
||||
|
||||
**Chosen mechanism: option (a), outflow-course presence — zero new
|
||||
data, semantically true, matching option (b)/(c)'s crossover concern
|
||||
without needing either.** An overflow basin, by `BasinOutcome`
|
||||
definition, has a resolved outlet path to the sea/another basin/open
|
||||
ground; an endorheic basin has none. A river course (`courses:
|
||||
Vec<RiverCourse>`, T-1170) leaving a lake gridunit IS the honest visual
|
||||
signal for "this lake drains" — no course leaving is the honest signal
|
||||
for "this lake doesn't." This needs no new field, no per-basin outcome
|
||||
byte, and sidesteps the crossover-size question I raised for option
|
||||
(b)'s `Vec<LakeBasin>` entirely (that question is now moot — there is no
|
||||
`lakes` list to size).
|
||||
|
||||
**Honest wiring caveat, stated precisely because it's the one place this
|
||||
isn't yet "free" in the way the coordinator's framing implied:** T-1177's
|
||||
own document is explicit in its "does NOT do" scope —
|
||||
*"`HydrologyResult`/`Basin`/`BasinOutcome` are prototype-only types, not
|
||||
wired into `RiverNetwork`... or any `AtlasLayerResponse` payload."* I
|
||||
checked this directly (`t1177-hydrology.md` line ~362) rather than
|
||||
assuming the outflow-course mechanism already connects end-to-end. It
|
||||
does not, today. `courses`/`river_downstream` are D8-network-derived
|
||||
(T-1170's mechanism, keyed on `fdir[i]`/river cells extracted by
|
||||
`drainage.rs`'s existing flow analysis); a hydrology basin's resolved
|
||||
`outlet_path` (Dijkstra overflow routing, a *different* computation) is
|
||||
not currently threaded into that D8 river network at all. **This is not
|
||||
a reason to reject option (a)** — Ruling 7b of the T-1170 design
|
||||
document already anticipated exactly this extension and pre-cleared it
|
||||
as additive (*"the `TERMINAL` downstream sentinel is reserved now...an
|
||||
interior sink becomes an additive drainage-extraction change, not a wire
|
||||
migration; and the termination mechanism... generalizes unchanged to a
|
||||
terminal lake shoreline. When you want endorheic basins, the work is in
|
||||
D8 sink retention and lake morphology, and every piece this batch builds
|
||||
consumes them without modification"*) — but it IS a real, small,
|
||||
implementation-scoped wiring task, not something the wire schema gets
|
||||
"for free" the moment `water=Lake` ships. **Stating the honest scope:**
|
||||
an overflow basin's spill point needs to register as a D8 downstream
|
||||
continuation (feeding `river_downstream`) rather than terminating at the
|
||||
lake, so `courses` picks up the outlet edge naturally; an endorheic
|
||||
basin's absence of such an edge is what already reads as "no course"
|
||||
under the existing mechanism, requiring no change at all. This is
|
||||
follow-up implementation work for whoever wires basin membership into
|
||||
`derive_morphology_zone` (Dudley's pipeline-slot answer, below) —
|
||||
flagging it explicitly rather than letting the schema doc imply the
|
||||
visual distinction ships automatically the day `water=Lake` lands.
|
||||
|
||||
### Visual treatment — unchanged from my original proposal, still holds
|
||||
|
||||
Lakes draw as water at every rung, same colorizer family as sea/ocean
|
||||
morphology — no new palette, per my round-1 §3 one-colorizer-family
|
||||
guarantee (this was never contingent on the field being `water` vs.
|
||||
`morphology` — a `Lake` discriminant colors identically either way).
|
||||
Endorheic-vs-overflow reads via course presence at the lake's outlet,
|
||||
not a distinct fill color or texture on the lake body itself — simpler
|
||||
than my original "secondary visual cue" proposal (texture/saturation/
|
||||
outline treatment), since a present-or-absent outflow course *is* the
|
||||
visual difference, no additional styling rule needed from Stig beyond
|
||||
drawing `courses` the way (c)'s label-anchor work already assumes he
|
||||
does.
|
||||
|
||||
### Byte cost — better than originally argued, not merely confirmed
|
||||
|
||||
**Exactly zero new wire bytes**, stronger than my original "near-zero"
|
||||
claim. `morphology`'s existing per-field cost (already priced across
|
||||
every canvas size in T-1179's table) is unchanged in shape — only the
|
||||
*source* of a subset of already-shipping `Lake`/`OpenOcean` discriminant
|
||||
writes moves from a heightmap threshold to a hydrology-basin test. No
|
||||
new array, no new encoding question, no new crossover-size analysis
|
||||
(the `Vec<LakeBasin>` sizing question is withdrawn along with the field
|
||||
it would have carried).
|
||||
|
||||
### Pipeline slot — Dudley's answer, adopted unchanged
|
||||
|
||||
`HydrologyResult` computed once per body at 512×256 (held in the
|
||||
D-203-shaped global-tier cache, T-1177's own scope); a finer-rung
|
||||
canvas derive needs basin membership at a gridunit's world position —
|
||||
Dudley's proposal: `TerrainAnalysis` gains a basin-membership field
|
||||
(basin id or bool, populated once when hydrology solves), sampled the
|
||||
SAME bilinear way `ocean_mask`/`ocean_fraction_q` already are, feeding
|
||||
`derive_morphology_zone`'s existing water tier as a new gate ahead of
|
||||
the `ocean_fraction_q` fallback. This is round 2 §(b)'s seed-chaining
|
||||
mechanism B (sample a coarser rung's continuous primitive, never
|
||||
re-solve) applied to hydrology exactly as it's already applied to
|
||||
`sea_level` — no new architecture, no new bench needed (his own
|
||||
per-cell rate numbers already include equivalent-cost sampling
|
||||
operations in the measured budget). Adopted without modification —
|
||||
this is implementation-pipeline territory, not schema/encoding, and his
|
||||
reasoning is sound on direct inspection.
|
||||
|
||||
### Convergence status: CLOSED, no open sub-questions remain
|
||||
|
||||
Unlike the original §(e), there is no remaining sizing call routed to
|
||||
Dudley — the `lakes: Vec<LakeBasin>` vs. `water` 4th-value crossover
|
||||
question is moot (neither ships; the fold-into-`morphology` answer
|
||||
needs neither). The one caveat carried forward is the outflow-course
|
||||
wiring gap named above, which is implementation scope for whoever lands
|
||||
the basin-membership sourcing fix, not an open design question for this
|
||||
document.
|
||||
|
||||
---
|
||||
|
||||
## True closing status (supersedes the "Summary for the lead interview" section above) — DOCUMENT FULLY CLOSED
|
||||
|
||||
- **(a) Payload schema:** closed. Field count is **unchanged** from the
|
||||
original round-2 pass (six static + two sim-state dense fields, two
|
||||
sparse lists) — §(e)'s lakes fix reuses `morphology`/`courses`, adding
|
||||
nothing.
|
||||
- **(b) Rung 0/1 split:** corrected in this pass (opener = rung 0,
|
||||
variable extent, canonical; Region = rung 1, fixed, evictable) —
|
||||
architectural correction, terminology pending final conformance to
|
||||
Tyre's parallel amendment text.
|
||||
- **(c) Label de-dup:** unaffected by both post-ratification items,
|
||||
closed as before.
|
||||
- **(d) Wire schema:** closed. No field-count change from §(e) — the
|
||||
`EncodedStepCanvas` struct is annotated where the lakes fix lands
|
||||
(`morphology`, `courses`) but gains no new field.
|
||||
- **(e) Lakes: CLOSED, no open sub-questions.** Reversed my own original
|
||||
proposal after verifying Dudley's code-read directly
|
||||
(`district_profile.rs`, `generator.rs`, `features.rs`): `morphology`
|
||||
already carries a `Lake` discriminant, mis-sourced from a heightmap
|
||||
threshold instead of settled hydrology — the fix is a data-source
|
||||
correction (Dudley's basin-membership sampling, adopted unchanged), not
|
||||
a wire addition. Endorheic-vs-overflow resolved via outflow-course
|
||||
presence (option (a) per the coordinator's framing), after directly
|
||||
confirming in `t1177-hydrology.md` that the originally-proposed
|
||||
`cliffs`-based cue would never fire (zero carved cells, 267/267 real
|
||||
bodies) — ruled against my own earlier reasoning once the population
|
||||
survey data made it untenable. One honest implementation caveat carried
|
||||
forward (not a design question): `HydrologyResult`'s basin outlets
|
||||
aren't yet wired into the D8 river network `courses` draws from —
|
||||
pre-cleared as additive by T-1170 Ruling 7b, follow-up work for
|
||||
whoever lands the sourcing fix, not a wire-schema gap.
|
||||
|
||||
**Whole document status: fully closed, no open cross-checks, no
|
||||
pending sub-questions routed to any other participant.** Ready for
|
||||
filing.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<EncodedCanvas>`
|
||||
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.
|
||||
@@ -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<IgnoredAny>` 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<EncodedStepCanvas>,
|
||||
}
|
||||
|
||||
/// One dense field, PNG-per-field encoded (T-1179's measured winner).
|
||||
pub struct EncodedField {
|
||||
pub png_bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
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<RiverCourse>, // sparse, MessagePack-native (unchanged shape)
|
||||
pub cliffs: Vec<CliffSegment>, // 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<CliffSegment>`
|
||||
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<CliffSegment>` 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<EncodedStepCanvas>` 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.
|
||||
@@ -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).
|
||||
@@ -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
|
||||
```
|
||||
@@ -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<CliffSegment>` 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<edge_id, String>` 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<u32>`; the cliff fields are
|
||||
**sparse, not dense** — a `cliffs: Vec<CliffSegment>` 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<CliffSegment>` 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.
|
||||
@@ -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<CliffSegment>` 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<CliffSegment>`, 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.
|
||||
@@ -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/<body_id>/`, 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.
|
||||
<body_id>.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.
|
||||
@@ -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/<body_id>/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:
|
||||
# "<body_id>:<step>:<center>:<n>:<granularity_v2>"
|
||||
file_path: String # user://atlas_cache/<body_id>/<key_hash>.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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<u8>` dense field**
|
||||
(`{None, Lake, Sea}`), reasoning from first principles about wire cost and
|
||||
vocabulary freezing. Independently, in flight, Araminta specced her own
|
||||
`water: Vec<u8>` 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<LakeBasin>` 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).
|
||||
Reference in New Issue
Block a user