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>
1232 lines
70 KiB
Markdown
1232 lines
70 KiB
Markdown
---
|
||
title: "Body Map Viewer — Dudley Interview 2 Response"
|
||
description: "Chunk-64m deepest-rung bench + Option D table, seed-chaining reconciliation with Tyre (cache-accelerated pure function), S2 courses-density bench at chunk/block, S4 sim-state TTL cost-side answer with Araminta"
|
||
workshop: body-map-viewer
|
||
round: interview-2
|
||
owner: Dudley
|
||
status: complete
|
||
decision_refs: [D-166, D-225, D-226, D-227, D-243, D-203]
|
||
---
|
||
|
||
# Dudley — Interview 2 Response
|
||
|
||
Four items redirected by Jeroen's interview-2 rulings, per the coordinator's
|
||
task and Tyre's parallel message (both landed with the same four items —
|
||
folded into one response since Tyre's questions are a subset of the
|
||
coordinator's task, asked from the governance side).
|
||
|
||
---
|
||
|
||
## 1. LADDER: chunk (64 m) as the new deepest rung
|
||
|
||
Jeroen, interview 2, verbatim: *"the actual tile level rung seems unusable.
|
||
maybe replace with 64?"* — his reasoning: a full 1920-class screen at
|
||
10 px/tile shows ~192×108 m, which is in-world viewport content (Phase 5),
|
||
not Atlas map content. Tile/voxel dropped from the Atlas ladder; chunk
|
||
(64 m, D-243's "stream/derive unit") is the new floor.
|
||
|
||
### (a) The chunk-64m bench — the never-measured rung, now measured
|
||
|
||
New benches, same file (`server/tests/bmv_gridunit_bench.rs`), same
|
||
discipline as the original T-1154 pass (`derive_at_metres` called directly —
|
||
no wire-facing cutoff band exists below Quarter's 1,024 m `MIN_WL_BANDS_M`
|
||
floor for Chunk either, same situation Block/Tile were in):
|
||
|
||
**Per-cell rate, 4,096-cell sweep (matching the existing Block/Tile
|
||
comparison table exactly):**
|
||
|
||
| Spacing | Cutoff | Total (4,096 cells) | ns/cell |
|
||
|---|---|---:|---:|
|
||
| Chunk (64 m) | 64 m (Nyquist) | 7.53 ms | 1,838.1 |
|
||
| Chunk (64 m) | 0 (uncut) | 7.44–7.57 ms | 1,816.0–1,847.9 |
|
||
| Block (128 m) [reference] | 128 m | 7.49–7.61 ms | 1,827.8–1,856.8 |
|
||
|
||
**MEASURED, run twice for stability, both passes agree within ~1%.** Chunk's
|
||
cutoff=64m vs. uncut delta is within noise (1838.1 vs 1816.0–1847.9 ns/cell)
|
||
— **confirms directly, not by inference from Block, the same "no truncation
|
||
work left" finding** the original T-1154 pass established: `enveloped_fbm`
|
||
only skips octaves strictly finer than the cutoff, and
|
||
`VOXEL_OCTAVE_WAVELENGTHS_M`'s finest entry is 128 m — at 64 m (finer than
|
||
that floor), the cutoff still has nothing to truncate, so chunk pays the
|
||
exact same full per-cell rate as Block. Chunk is not "one step cheaper" than
|
||
Block; it's the same flat ~1.8 µs/cell single-thread rate the whole
|
||
Block-through-chunk band shares.
|
||
|
||
**Realistic deepest-step viewport canvas at chunk spacing (replaces the old
|
||
216×384 m / 1 m-spacing bench, which measured the now-dropped tile rung):**
|
||
|
||
| Path | Wall time | ns/cell | Speedup |
|
||
|---|---:|---:|---:|
|
||
| PARALLEL (16 threads, row-chunked) | 1,724.48–1,732.84 ms | 207.9–208.9 | — |
|
||
| SINGLE-THREAD | 15,051.59–15,145.11 ms | 1,814.7–1,825.9 | — |
|
||
| — | — | — | 8.73–8.74× |
|
||
|
||
**MEASURED, run twice for stability, both passes agree within 0.5%.** Full
|
||
3840×2160 canvas (8,294,400 cells) at 1 gridunit-per-screen-px, 64 m spacing
|
||
— **1.72–1.73 seconds parallel.** This is not the 17 ms the old tile-rung
|
||
deep-step bench reported — chunk's deepest-step canvas costs roughly two
|
||
orders of magnitude more, because it covers a MUCH larger world extent at
|
||
the SAME cell count (245.8 km × 138.2 km vs. tile's 216 m × 384 m — see the
|
||
display-band derivation below for why this is the correct comparison, not a
|
||
regression). 1.7 s is still comfortably inside a step-cross tolerance
|
||
measured in the hundreds-of-ms-to-low-seconds range this workshop has used
|
||
throughout (matches the District/Quarter/Block full-canvas numbers already
|
||
in the recommended ladder table, all in the same 1.7–1.8 s band at full
|
||
8.3M-cell canvas size) — **cost still does not gate this rung.**
|
||
|
||
### The new bottom-out rule, stated precisely (for Tyre's amendment text)
|
||
|
||
At **1×1 px-per-gridunit** (the workshop's own ideal ratio, premise 5), a
|
||
3840×2160 canvas at 64 m spacing covers:
|
||
|
||
```
|
||
world_extent_m = canvas_px × gridunit_spacing_m
|
||
smaller axis: 2160 px × 64 m = 138,240 m ≈ 138.2 km
|
||
larger axis: 3840 px × 64 m = 245,760 m ≈ 245.8 km
|
||
```
|
||
|
||
**The new bottom-out rule is "1 screen px per 64 m gridunit," NOT "10 px per
|
||
chunk."** This is a real, substantive difference from the old tile rung's
|
||
rule, not a cosmetic swap of one spacing value for another — worth stating
|
||
explicitly because getting this wrong would silently reintroduce the
|
||
D-226(d)/legibility problem chunk is supposed to solve:
|
||
|
||
- The old tile rule (10 px/tile) existed because a single 1 m gridunit at
|
||
1×1 is *too small to read* on screen — 10 px of magnification margin was
|
||
there to buy legibility for a ground feature that is otherwise
|
||
imperceptible. That's the "in-world viewport" problem Jeroen's ruling
|
||
correctly identifies: 10×-magnifying a 1 m unit is exactly what an
|
||
in-world character-relative camera does, not what an Atlas map does.
|
||
- **Chunk does not have this problem.** A 64 m gridunit at 1×1 is already a
|
||
reasonably-sized map feature (a city block, a short street segment) — it
|
||
doesn't need a legibility margin the way a 1 m ground tile did. Applying
|
||
the old rule's 10× margin to chunk (i.e. "10 px per chunk") would either
|
||
(a) needlessly shrink the deepest step's world coverage by 10× for no
|
||
legibility gain, or (b) be based on a misunderstanding of why the 10×
|
||
factor existed in the first place. **It should not carry over.**
|
||
- **Concrete rule for the amendment text:** *"The deepest Atlas rung (chunk,
|
||
64 m) samples at 1 gridunit per screen pixel (the workshop's own ideal
|
||
ratio) with no additional magnification margin — the 10×-per-tile rule
|
||
that governed the now-dropped voxel/tile rung does not apply to chunk,
|
||
because chunk-scale features do not have the same-order-of-magnitude
|
||
legibility problem a 1 m ground unit did at screen resolution."*
|
||
|
||
### (b) Option D — five rungs, bottom = chunk, every row measured
|
||
|
||
Rebuilding the round-2 recommended table (previously Option B: Region →
|
||
District → Quarter → Block → voxel) with chunk replacing voxel as the
|
||
bottom rung — same viewport-sized-canvas convention as before (fixed
|
||
3840×2160 px canvas at every step, since chunk — unlike the old voxel rung —
|
||
does NOT need the display-ratio-sized-canvas exception; see below).
|
||
|
||
| Step | D-243 rung | Spacing | Step factor (from prev) | Canvas @ 3840×2160, 1×1 | Cells | World extent | Derive cost (parallel, MEASURED) | Wire cost (PNG-per-field, measured/interpolated) |
|
||
|---|---|---:|---:|---|---:|---|---:|---:|
|
||
| 0 | Region | 204.8 km | — | 3840×2160 | 8,294,400 | whole body (capped-tile mosaic, T-1143 §4) | 1,827 ms | 16.88 MB |
|
||
| 1 | District | 2,048 m | 100× | 3840×2160 | 8,294,400 | 7,864 × 4,424 km | 1,827 ms | 16.88 MB |
|
||
| 2 | Quarter | 512 m | 4× | 3840×2160 | 8,294,400 | 1,966 × 1,106 km | ~1,827 ms (T-1154 same-band) | ~16.88 MB |
|
||
| 3 | Block | 128 m | 4× | 3840×2160 | 8,294,400 | 491 × 276 km | ~1,827 ms (measured directly, T-1154) | ~16.88 MB |
|
||
| 4 (deepest) | **Chunk** | **64 m** | **2×** | 3840×2160 | 8,294,400 | **245.8 × 138.2 km** | **1,724–1,733 ms (measured directly, this pass)** | **~16.88 MB (same field-count/cell-count as every other row)** |
|
||
|
||
**Every row is now directly measured or in the same directly-measured cost
|
||
band — the exact "zero asterisks" property Option B (the old skip-chunk
|
||
recommendation) earned by NOT including chunk. Now that chunk itself is
|
||
measured, that property transfers cleanly to Option D with chunk as the
|
||
bottom: this table has no unmeasured or inferred-only row.**
|
||
|
||
**What changed vs. the old Option B, stated plainly:**
|
||
1. **Step count: still 5** (Region, District, Quarter, Block, + one deepest
|
||
rung) — chunk simply replaces voxel/tile as row 4's identity. The ladder
|
||
shape (which rungs, how many steps) is unchanged; only the deepest rung's
|
||
spacing and canvas-sizing convention changed.
|
||
2. **Canvas-sizing convention: chunk uses the SAME fixed-3840×2160-px-budget
|
||
rule as every other step, no exception.** This is the one place Option D
|
||
is structurally simpler than the old Option B: the old voxel/tile deepest
|
||
step needed a special-cased display-ratio-sized canvas (216×384 m at
|
||
10 px/tile, NOT the fixed-canvas convention every other step used,
|
||
because the two conventions were mutually incompatible at 1 m spacing —
|
||
see round 2 §(c)'s Option A analysis). **Chunk does not have this
|
||
incompatibility** — at 1×1 px/gridunit, a fixed 3840×2160 canvas and the
|
||
"no extra magnification margin" bottom-out rule above are the same rule,
|
||
not two conventions in conflict. Dropping the special case is a genuine
|
||
simplification, not a workaround.
|
||
3. **Deepest-step derive cost went UP by two orders of magnitude in absolute
|
||
terms** (17 ms → 1.7 s) because the deepest-step canvas now covers a
|
||
world area orders of magnitude larger (216 m × 384 m → 245.8 km ×
|
||
138.2 km) at the SAME cell count. This is expected and correct — chunk's
|
||
canvas is now the SAME size (in cells, and therefore in derive cost and
|
||
wire bytes) as every other rung's canvas, which is what "no special case"
|
||
means. **1.7 s parallel is still comfortably affordable** (same band as
|
||
Region/District/Quarter/Block's own full-canvas cost) — the absolute
|
||
number changed, the affordability verdict did not.
|
||
|
||
**Recommendation: adopt Option D as stated.** It inherits everything Option
|
||
B earned (measured-not-inferred rows, monotonic-with-spacing cost story
|
||
below Region) and removes Option B's one piece of remaining awkwardness (the
|
||
deepest-step canvas-sizing special case) for free, as a consequence of
|
||
Jeroen's own ruling rather than a separate design choice.
|
||
|
||
---
|
||
|
||
## 2. SEED-CHAINING RECONCILIATION — with Tyre, "cache-accelerated pure function"
|
||
|
||
Jeroen answered **NO** to my round-2 §(b) ruling (independent re-derivation,
|
||
coarser rung called only as a nested function, never reading cached
|
||
output) — he meant the other reading: the finer step genuinely **consumes**
|
||
the coarser step's *resolved* output, not just calls the same derivation
|
||
code fresh. Tyre relayed the coordinator's candidate reconciliation model
|
||
("cache-accelerated pure function") and asked me to assess it from the
|
||
cost/implementation side. This section is that assessment, worked jointly
|
||
with Tyre per his message.
|
||
|
||
### The model, restated precisely
|
||
|
||
- **Definition stays pure, D-227 intact:** `derive(seed, position)` always
|
||
produces the same bytes, by construction, regardless of implementation
|
||
strategy. This is non-negotiable and nothing below touches it.
|
||
- **Implementation is permitted to read a resident coarser step-canvas as an
|
||
acceleration**, falling back to deriving fresh (my original §(b) model,
|
||
now correctly reframed as the FALLBACK path, not the primary design) when
|
||
the coarser canvas isn't resident.
|
||
- **My round-1/round-2 benched numbers survive as the honest worst-case
|
||
ceiling** — every number in ①②③ was measured via the fresh-derive path
|
||
(nothing in any of my benches reads a cached coarser canvas as an input),
|
||
so they remain valid upper bounds on cost regardless of which
|
||
implementation strategy ships.
|
||
|
||
### Is this D-227-sound? Yes — confirming Tyre's argument from the cost/implementation side
|
||
|
||
Tyre's governance argument (his message): the coarser canvas being read is
|
||
itself evictable derived data — a pure function of the same seed — so
|
||
reading it is an **optimization**, not a **semantic dependency**. A semantic
|
||
dependency would mean "the finer step is WRONG without the coarser cache,"
|
||
which is never true here, because the coarser value is always re-derivable
|
||
to the same bytes. **I agree with this argument completely, and it matches
|
||
exactly how I'd reason about it from the implementation side:** the test
|
||
D-227 already applies ("eviction → recompute, always valid") is precisely
|
||
the test this model needs to pass, and it passes by construction — a
|
||
cache-accelerated read and a fresh derive of the SAME coarser value at the
|
||
SAME `(seed, position)` are required to be byte-identical (they're the same
|
||
pure function), so substituting one for the other can never change the
|
||
finer step's output, only its latency.
|
||
|
||
**The mandatory test this needs, matching the shape of my existing
|
||
window-independence/determinism invariants:** a `cache_hit_path ==
|
||
cache_miss_path` byte-exact assertion — derive the coarser rung's value both
|
||
ways (read from a populated cache entry, and via fresh `derive()` with the
|
||
cache artificially evicted) and assert identical output. This is the same
|
||
style of test `determinism_at_330k_cells` (T-1177) and the courses
|
||
window-independence invariant already use in this codebase — not a new kind
|
||
of test, an extension of a pattern already proven twice. **If this test
|
||
passes — and it must, because both paths are the same pure function by
|
||
definition — the two paths are indistinguishable except in speed, which is
|
||
exactly Tyre's "optimization not dependency" characterization.**
|
||
|
||
### Quantifying what the acceleration actually saves — the honest cost answer
|
||
|
||
This is the part the coordinator specifically asked me to assess honestly,
|
||
including the possibility that acceleration doesn't help. **My answer: it
|
||
depends on WHAT the finer step would read from the coarser cache, and for
|
||
the two most likely candidate mechanisms, the answer is genuinely mixed —
|
||
one clearly helps, one plausibly does not, and I want to be precise about
|
||
which is which rather than assert a blanket "yes, faster."**
|
||
|
||
**Candidate mechanism A — the finer step reads the coarser step's RESOLVED
|
||
CLASSIFICATION as a literal shortcut (skip re-deriving morphology/elevation
|
||
where the coarser answer is "good enough").** This is NOT what I'd
|
||
recommend, and I don't think it's what "serves as seed information" means
|
||
either — it would violate the categorical-field re-derivation rule Araminta
|
||
already established in round 1 (coarser steps must re-derive
|
||
morphology/vegetation/glaciation as a fresh classification decision at their
|
||
own spacing — a dominant-mode pick, never an average or a direct carry from
|
||
a different spacing's classification). Reading a District-spacing
|
||
`morphology_zone` value as a shortcut for what a Chunk-spacing cell's
|
||
`morphology_zone` "should" be would be exactly the same-vocabulary,
|
||
different-meaning-per-scale contradiction Araminta ruled against. **I'm
|
||
naming this candidate to rule it out, not to cost it.**
|
||
|
||
**Candidate mechanism B — the finer step reads the coarser step's underlying
|
||
CONTINUOUS PRIMITIVES (the region-baseline / district-baseline value at a
|
||
given world position) as an input to its OWN fresh classification, instead
|
||
of recomputing that baseline from scratch.** This is what I believe Jeroen's
|
||
"serves as seed information for the deeper cascade" phrase actually
|
||
describes, and it's ALSO the pattern already shipped in this codebase today
|
||
(`invent_primitives`'s district call already reads a region baseline; my
|
||
round-2 §(b) response cited this precedent). **This is the mechanism worth
|
||
costing.**
|
||
|
||
**The honest cost answer for mechanism B: reading the cached coarser
|
||
baseline instead of recomputing it is very likely a real, if modest, win —
|
||
but I do not have a number for it, and I want to say precisely why, rather
|
||
than either assert a saving or claim I've measured one.** `derive_at_metres`
|
||
computes several primitives in one call (coast-warp, detail-scatter octaves,
|
||
region-baseline blend, classification) — none of my benches isolate the
|
||
region-baseline-blend sub-cost from the total per-cell cost, because every
|
||
bench I've run measures the WHOLE `derive_at_metres` call, by design (that's
|
||
the actual served cost, and isolating sub-costs wasn't the question ①②③
|
||
asked). So: **is reading+upsampling a coarse cache cheaper than deriving
|
||
fresh at ~1.65 µs/cell? Almost certainly yes for the specific sub-computation
|
||
mechanism B describes** (a cached bilinear-blend lookup is architecturally
|
||
cheaper than recomputing the same blend from raw region data — this is
|
||
essentially free to assert, since the blend itself is a small fraction of
|
||
the total per-cell cost, most of which is the octave-sum detail-scatter
|
||
work that mechanism B does NOT propose caching) — **but I cannot quantify
|
||
the magnitude without a new, targeted bench that isolates the
|
||
region-baseline sub-cost specifically, which nobody has run.**
|
||
|
||
**Is a re-bench genuinely needed?** For the ARCHITECTURE ruling (adopt
|
||
cache-accelerated pure function, yes/no) — **no**, my existing numbers
|
||
already answer the load-bearing question (worst-case ceiling cost, D-227
|
||
soundness) and the mandatory determinism test above is a correctness gate,
|
||
not a cost measurement. For an IMPLEMENTATION decision about exactly how
|
||
much the acceleration saves (needed before anyone sizes an "expected"
|
||
latency, as opposed to a worst-case one) — **yes, eventually, but not before
|
||
this ruling closes**, and I'd scope it as an implementation-ticket
|
||
measurement (isolate the region-baseline-blend cost specifically, then
|
||
compare cached-read vs. fresh-blend at that isolated cost) rather than
|
||
something this workshop needs to resolve before Jeroen ratifies the model.
|
||
My benched ①②③ ceiling numbers are sufficient to close the architecture
|
||
question today.
|
||
|
||
### Answering Tyre's chain-reaction question (Troblum B2)
|
||
|
||
**Each rung derives fresh independently on a cache miss — it does NOT chain
|
||
backward through evicted coarser rungs.** This is not a new architectural
|
||
choice; it falls directly out of the definition staying pure: `derive(seed,
|
||
position)` for ANY rung is fully self-contained (it takes the seed and a
|
||
world position, nothing else, per D-227's own signature) — so a Chunk-rung
|
||
cache miss's fallback is "call `derive_at_metres` at Chunk spacing," full
|
||
stop, not "first check whether the District-rung cache is warm, and if not,
|
||
re-derive District, and if THAT'S baseline is itself missing something,
|
||
re-derive Region..." There is no chain to walk, because the fresh-derive
|
||
fallback was never built to depend on any OTHER rung's cache state — it's
|
||
the same self-contained function every one of my benches already calls
|
||
directly, at every rung, with zero shared cache dependency between rungs.
|
||
**Troblum's chain-reaction worry dissolves for exactly the reason Tyre
|
||
expected: each rung's fresh-derive path is independent per my original §(b)
|
||
function-composition argument, which survives INSIDE this model as the
|
||
fallback, even though it's no longer the exclusive path.** The
|
||
cache-accelerated read (when a coarser canvas IS resident) is a pure speed
|
||
optimization layered on top of that already-independent fallback — it never
|
||
becomes a requirement the fallback depends on.
|
||
|
||
### Verdict — for Jeroen's ratification
|
||
|
||
**Adopt the cache-accelerated pure function model.** It is D-227-sound (Tyre's
|
||
optimization-not-dependency argument, confirmed from the cost/implementation
|
||
side above), it does honor "serves as seed information for the deeper
|
||
cascade" (mechanism B — reading a coarser baseline as an input to a fresh
|
||
classification — is both what I believe Jeroen meant and what the codebase
|
||
already does today at the district/region boundary), my benched ①②③ numbers
|
||
remain valid as the worst-case ceiling under this model (nothing about
|
||
adding an acceleration path can make the fallback path slower than what I
|
||
measured), no chain-reaction risk exists (each rung's fresh-derive fallback
|
||
is self-contained), and the ONE new obligation this model adds — the
|
||
cache-hit-path == cache-miss-path determinism test — is a correctness gate
|
||
this codebase already knows how to write, not new machinery. **What remains
|
||
unquantified (the magnitude of the acceleration's actual saving) is real but
|
||
not load-bearing for the architecture ruling** — it's an implementation-time
|
||
measurement, not a gate on Jeroen's yes/no here.
|
||
|
||
---
|
||
|
||
## 3. S2 BENCH — deep-step × high-river-density courses cost
|
||
|
||
Ruled before filing: the last zero-data-point cell on the courses-cost
|
||
axis. Every prior courses-inclusive measurement (T-1178's Cross-check 1, 18
|
||
courses/331,776 cells) is at District spacing; nothing measured courses-on
|
||
cost at Chunk or Block. New bench:
|
||
`bench_s2_courses_density_at_chunk_and_block`
|
||
(`server/tests/bmv_gridunit_bench.rs`), using the real GJ1c river network
|
||
(not synthetic — the densest real confluence region found by scanning for
|
||
the river cell with the most other river cells within an 8 px search
|
||
radius), built via the actual PUBLIC invention pipeline
|
||
(`river_course::build_edges` + `river_course::invent_course`, both `pub` —
|
||
unlike `layer_proxy::invent_courses_near_window` itself, which is private to
|
||
that module; this bench replicates its per-edge invention loop using the
|
||
same public primitives, matching this file's existing replica-loop
|
||
discipline).
|
||
|
||
### Results — MEASURED, run twice for stability
|
||
|
||
| Rung | Window | Courses in window | Avg points/course | Courses OFF | Courses ON | Delta |
|
||
|---|---|---:|---:|---:|---:|---:|
|
||
| Chunk (64 m) | 64×64 cells (4,096 m × 4,096 m) | 2 | 1,732.0 | 1,861.9–1,913.3 ns/cell | 3,489.7–3,533.7 ns/cell | **+84.7% to +87.4%** |
|
||
| Block (128 m) | 64×64 cells (8,192 m × 8,192 m) | 2 | 867.0 | 1,849.4–1,855.7 ns/cell | 2,544.1–2,816.8 ns/cell | **+37.6% to +51.8%** |
|
||
|
||
Both runs agree within run-to-run noise typical of this file's other 4,096-
|
||
cell sweeps (~5-10%); the direction and rough magnitude are stable across
|
||
both passes.
|
||
|
||
### The honest finding: this is a real, structural cost — meaningfully larger than District's <5%, with a clear mechanism
|
||
|
||
**This is not the same order of magnitude as the District-rung courses-cost
|
||
figure (+0.09–0.21 ms against a ~5 ms baseline, under 5%) already on
|
||
record. At Chunk and Block spacing, courses cost 38%–87% MORE, not under
|
||
5% more.** I traced the mechanism rather than reporting the number without
|
||
explanation: `near_perennial_water`'s cost is `O(courses × points-per-
|
||
course)` (a per-segment bounding-box + point-to-segment-distance scan over
|
||
every station on every candidate course, per cell). `invent_course`
|
||
resamples each course's control polyline at `station_spacing_m` — the SAME
|
||
spacing value passed as the rung's own cutoff/spacing. **A course spanning a
|
||
fixed chord length gets proportionally MORE points the finer the rung's
|
||
station spacing is** — confirmed directly by the instrumentation this bench
|
||
added: 1,732 points/course at Chunk (64 m stations) vs. 867 at Block (128 m
|
||
stations), almost exactly the 2× ratio matching the 2× spacing ratio between
|
||
the two rungs. District's own courses (2,048 m stations) have roughly 32×
|
||
fewer points per course than Chunk's for the same chord length, which is
|
||
exactly why District's courses-cost figure was small and Chunk's is not —
|
||
**this was always going to happen once a courses-inclusive measurement was
|
||
taken at a spacing this much finer than District; nobody had run it because
|
||
nobody had a courses-inclusive bench below District until now.**
|
||
|
||
**What this means for the wire-cost/ticket-plan picture, stated plainly:**
|
||
courses ARE affordable at Chunk/Block in absolute terms — even the
|
||
worst case (+87% on a ~1.9 µs/cell baseline) lands at roughly 3.5 µs/cell,
|
||
which is still well within the "comfortably interactive" band every other
|
||
number in this workshop has used (a full 8.3M-cell chunk-spacing canvas at
|
||
3.5 µs/cell single-thread-equivalent would be ~29s single-thread / ~3.3s
|
||
parallel at the same ~8.7× speedup this file's other benches measure — a
|
||
real, larger number than the courses-off 1.7s figure in Option D's table
|
||
above, but not a "computer catches fire" case). **The real implication is
|
||
implementation-side, not a cost-affordability gate:** `near_perennial_water`
|
||
resampling courses at the SAME spacing as the rung it's serving is a real
|
||
per-cell cost driver that scales inversely with rung spacing, and whoever
|
||
implements Chunk-rung course rendering should know this going in rather than
|
||
discover it as a surprise regression — a station-spacing cap independent of
|
||
rung spacing (courses don't need MORE points just because the rung asking
|
||
for them is finer, if the goal is "is this cell near a river," not "render
|
||
the river at full rung resolution") is a plausible optimization worth
|
||
flagging for the implementation ticket, but I'm naming it as a finding, not
|
||
ruling on it — that's a river-rendering design call, not a cost-measurement
|
||
one.
|
||
|
||
**Fold-in note:** this section is written to be pasted directly into
|
||
`measurements/t1178-t1154-derive-bench.md` as an addendum (same discipline
|
||
as the T-1177 population-survey addendum) — flagging here rather than
|
||
duplicating the write, since the coordinator's instruction was "fold into
|
||
the derive-bench doc." I have not yet made that edit; see the summary below
|
||
for the exact pending action.
|
||
|
||
---
|
||
|
||
## 4. S4 — sim-state TTL phase-cadence, joint proposal with Araminta
|
||
|
||
Araminta's proposal (relayed by the coordinator): a **formula**, not a fixed
|
||
real-time number — `SIM_STATE_TTL[field] = 1 × the field's own fastest
|
||
driving clock-bucket, per body class`. Concretely: `flooded` = 1 tidal
|
||
bucket on bodies with a moon (D-253's tidal ≈ day-phase bucket), degrading
|
||
to 1 seasonal bucket on moonless bodies; `glaciation` = 1 season-step always
|
||
(seasonal-only driver). Her rationale, in short: the map's staleness
|
||
tolerance should equal the sim's own recompute granularity (can never be
|
||
VISIBLY wrong), the risk is asymmetric (too-short wastes a refetch, harmless;
|
||
too-long shows wrong water-height, the real bug — so bias toward the faster
|
||
term), and binding to the bucket UNIT rather than a hardcoded time value
|
||
survives D-253's own "provisional, tunable" bucket-size calibration.
|
||
|
||
### My half — the serving-side cost check
|
||
|
||
**Question: is re-deriving/re-serving `flooded`/`glaciation` at 1-bucket-
|
||
rollover cadence affordable, or does cost force a >1× multiplier?**
|
||
|
||
**Answer: affordable at 1×, no multiplier needed — and the reason is
|
||
structural, not just "the number is small."** I confirmed this by reading
|
||
`district_profile.rs` directly rather than assuming it: `glaciation_grade`
|
||
and the morphology zone's water/flood classification are **not a separate
|
||
sub-pipeline** — they come out of the SAME `derive_at_metres` call as every
|
||
other field (`DistrictProfile` is one struct, populated by one function
|
||
call; there is no field-level partial-derive path that computes "just
|
||
glaciation" or "just the water classes" more cheaply than a full derive).
|
||
This has one important consequence for the TTL cost question:
|
||
|
||
**There is no cheaper "sim-state-only" re-derive path to cost separately —
|
||
re-serving frozen/flooded at any cadence costs the SAME per-cell rate as a
|
||
full geometry re-derive, because it IS a full geometry re-derive** (the two
|
||
fields just happen to be the ones whose VALUE can change between derives,
|
||
while morphology/elevation/moisture/vegetation are re-derived to the
|
||
identical bytes every time per D-227 purity — but the COST of computing
|
||
them is not separable). This means the TTL cost question reduces to a
|
||
question I've already answered at every rung in this workshop: **is a full
|
||
re-derive of a step canvas, at 1-bucket cadence, affordable?** Given every
|
||
rung's derive cost (Region/District/Quarter/Block/Chunk, Option D above) is
|
||
in the 1.7–1.8 s parallel band for a full 8.3M-cell canvas, and — critically
|
||
— **the actual sim-state re-derive doesn't need a FULL canvas re-derive at
|
||
all, only the currently-cached window(s) a player might reopen**, the real-
|
||
world cost is far below even that ceiling: it's bounded by how many cached
|
||
step-canvas entries exist for a given body at TTL-rollover time (Tier 2 of
|
||
my round-2 cache-tier spec — sub-global geometry entries, evicted on time-
|
||
since-last-visit, which is a SMALL, bounded set per body in normal play, not
|
||
"re-derive the whole planet on every tidal tick").
|
||
|
||
**No multiplier needed — 1× holds.** I'm not widening Araminta's proposed
|
||
cadence, because the cost isn't the constraint her formula's own rationale
|
||
already correctly identified the real constraint (staleness-vs-correctness,
|
||
not compute cost) — the compute-cost check confirms it doesn't ALSO need to
|
||
be a constraint, it just needed confirming rather than assuming.
|
||
|
||
### Joint final proposal
|
||
|
||
**Formula (Araminta's, adopted as stated, 1× multiplier confirmed
|
||
affordable):**
|
||
|
||
```
|
||
SIM_STATE_TTL[flooded] = 1 × tidal_bucket (moon-bearing bodies, D-253)
|
||
= 1 × seasonal_bucket (moonless bodies)
|
||
SIM_STATE_TTL[glaciation] = 1 × seasonal_bucket (always — seasonal-only driver)
|
||
```
|
||
|
||
**Rationale (joint paragraph):** the map's staleness tolerance is bound to
|
||
exactly the sim's own recompute granularity, so a cached sim-state plane is
|
||
never visibly wrong — it can be at most one bucket stale, and the simulation
|
||
itself has no fresher answer to offer during that bucket. Binding to the
|
||
bucket UNIT rather than a fixed real-time value survives D-253's own
|
||
provisional/tunable bucket calibration automatically, and the formula's
|
||
asymmetric-risk argument (too-short TTL wastes a harmless refetch;
|
||
too-long TTL shows objectively wrong water-height, the real bug) already
|
||
justifies biasing toward the faster of the two candidate drivers on
|
||
moon-bearing bodies without needing a cost-side override. The serving-side
|
||
check confirms this formula needs no widening: `flooded`/`glaciation` have
|
||
no cheaper isolated re-derive path than a full per-cell derive (they're
|
||
computed inside the same `derive_at_metres` call as every other field), but
|
||
the actual re-derive workload at rollover time is bounded by the small,
|
||
currently-cached set of step-canvas windows for a given body (per the round-
|
||
2 cache-tier spec's Tier 2), not a whole-planet re-derive — so 1× the
|
||
sim's own bucket cadence is both correctness-necessary (Araminta's argument)
|
||
and cost-affordable (this section's confirmation), with no tension between
|
||
the two requiring a compromise multiplier.
|
||
|
||
---
|
||
|
||
## Summary for filing / next steps
|
||
|
||
1. **Chunk-64m is measured, Option D adopted (5 rungs, bottom = chunk, every
|
||
row measured), the new bottom-out rule is "1×1 px/gridunit, no
|
||
magnification margin"** — stated precisely above for Tyre's amendment
|
||
text. His D-226(d) partial-floor-restore (tile/voxel back to
|
||
never-Atlas-mapped) is consistent with everything measured here.
|
||
2. **Seed-chaining: "cache-accelerated pure function" adopted** — D-227-sound
|
||
(confirmed from the cost/implementation side, matching Tyre's governance
|
||
argument), no chain-reaction risk (each rung's fresh-derive fallback is
|
||
self-contained), my ①②③ benched numbers survive as the worst-case
|
||
ceiling, the acceleration's actual magnitude is plausibly real but
|
||
unquantified (named as a future implementation-time measurement, not a
|
||
gate on this ruling), and one new mandatory test (cache-hit ==
|
||
cache-miss, byte-exact) closes the correctness obligation.
|
||
3. **S2 courses-density at Chunk/Block: measured, and it's a real finding,
|
||
not a formality** — +38% to +87% cost at these finer rungs (vs.
|
||
District's <5%), traced to a concrete mechanism (course point-count scales
|
||
inversely with rung spacing via `station_spacing_m`). Still affordable in
|
||
absolute terms; flagged as an implementation consideration (a possible
|
||
station-spacing cap independent of rung spacing) for whoever builds
|
||
Chunk-rung course rendering. **Folded into
|
||
`measurements/t1178-t1154-derive-bench.md` as a dated addendum**, matching
|
||
the T-1177 population-survey addendum's format (done in this same pass —
|
||
confirmed with Tyre, whose §(d) Troblum-disposition note originally
|
||
carried stale "pending" phrasing and has since been corrected to cite
|
||
both addenda as filed).
|
||
4. **S4: joint formula with Araminta, 1× multiplier confirmed, no widening
|
||
needed** — `flooded`/`glaciation` TTL bound to 1× their driving sim
|
||
bucket (tidal-or-seasonal, seasonal-only respectively), cost-affordable
|
||
because sim-state re-derive is bounded by the small cached-window set per
|
||
body, not a whole-planet re-derive.
|
||
|
||
**Code changes:** `server/tests/bmv_gridunit_bench.rs` — three new `#[ignore]`d
|
||
release benches (`bench_chunk_spacing_4096_cells`,
|
||
`bench_chunk_deep_step_realistic_canvas`,
|
||
`bench_s2_courses_density_at_chunk_and_block`) plus one new helper
|
||
(`build_gj1c_courses_near_window`). No `server/src/` file modified. Full
|
||
bench-file test list (11 tests) confirmed intact; full crate `cargo build
|
||
--release` clean.
|
||
|
||
---
|
||
|
||
## POST-RATIFICATION ADDENDUM (2026-07-23): lake schema gap + global-tier math recheck
|
||
|
||
Two items from Jeroen's post-ratification review, redirected by the
|
||
coordinator after interview 2 closed.
|
||
|
||
### 5. LAKES ON THE MAP — the schema gap, and where it actually sits
|
||
|
||
Jeroen's direct question: *"when filling basins to find an overflow, is that
|
||
body flagged as lake or flooded or something? do we draw lakes on the map?"*
|
||
Answer, checked against the real code rather than assumed: **currently, no —
|
||
and the gap is more specific than "the field doesn't exist." It's that the
|
||
field DOES exist (`MorphologyZone::Lake`, discriminant 1, in the 17-zone
|
||
vocabulary this whole workshop's payload schema already carries), but
|
||
nothing in the derivation pipeline ever sources it from settled hydrology.**
|
||
|
||
**What actually happens today, confirmed by direct read
|
||
(`district_profile.rs:556-565`):**
|
||
|
||
```rust
|
||
if ocean_fraction_q >= 80 {
|
||
// "No body-scale salinity signal at district level yet; treat all as
|
||
// OpenOcean. Lake differentiation lives at ChunkContext."
|
||
return MorphologyZone::OpenOcean;
|
||
}
|
||
if ocean_fraction_q >= 60 {
|
||
return MorphologyZone::Lake;
|
||
}
|
||
```
|
||
|
||
`ocean_fraction_q` is a bilinear sample of `TerrainAnalysis.ocean_mask`
|
||
(`district_profile.rs:1870`), and `ocean_mask` itself is nothing more than
|
||
`elev[i] < sea_level` (`features.rs:104`) — a raw per-cell below-sea-level
|
||
threshold from the ORIGINAL heightmap, with **zero connection to
|
||
`HydrologyResult`'s settled-equilibrium output** (`filled_scaled`, basin
|
||
membership, `BasinOutcome::Endorheic`/`Overflow`). The `Lake` vs `OpenOcean`
|
||
split that exists today is a crude density heuristic (how much of the
|
||
bilinear sample window is below sea level), not a real inland-vs-connected
|
||
distinction, and it was never wired to the hydrology solver this whole
|
||
workshop's ①/T-1177 measurement is about — the solver computes real basin
|
||
geometry and nobody downstream reads it.
|
||
|
||
**One more piece of the picture, also confirmed by direct read: a BETTER
|
||
building block than `ocean_fraction_q` already exists and is ALSO unused for
|
||
this purpose.** `TerrainAnalysis.lake_mask` (`features.rs:105`,
|
||
`compute_lake_mask`) is a proper flood-fill connected-component test —
|
||
below-sea-level cells connected to a grid edge are `ocean_mask`-true and
|
||
`lake_mask`-false (the sea); below-sea-level cells NOT connected to an edge
|
||
are `lake_mask`-true (landlocked water). This is consumed today by
|
||
`road_graph.rs` and `features.rs` for road-planning purposes
|
||
(`ta.ocean_mask[i] || ta.lake_mask[i]` water-avoidance checks) — **but is
|
||
never read by `derive_morphology_zone` or anywhere in the Atlas
|
||
classification/wire path.** So there are actually two disconnected gaps
|
||
stacked on each other: (a) the wire schema's water classification doesn't
|
||
read hydrology's settled-equilibrium basins at all, and (b) even the
|
||
CRUDER connectivity-based `lake_mask` that already exists and would be a
|
||
strict improvement over the current `ocean_fraction_q` heuristic is also not
|
||
wired into the classification path. Both are real; (a) is the one Jeroen's
|
||
question is really about, since `lake_mask` still can't distinguish
|
||
overflow lakes from endorheic ones or know about a lake basin that only
|
||
exists because of settled hydrology's overflow logic (a filled basin can
|
||
have interior cells that were never below the ORIGINAL heightmap's sea
|
||
level — that's the entire point of "filling basins to find an overflow").
|
||
|
||
**The fix, specced with Araminta (relayed via coordinator; her framing
|
||
adopted, my derive-pipeline slotting added):**
|
||
|
||
**New static water-classification field, sourced from `HydrologyResult` at
|
||
canvas-derive time, distinct from Araminta's `flooded` sim-state plane.**
|
||
|
||
- **Not a new dense field — fold into `morphology`, reusing the existing
|
||
`Lake`/`OpenOcean` discriminants (1/0) the vocabulary already carries.**
|
||
This is a zero-cost wire change: no new array, no new byte, no new
|
||
encoding question. The 17-zone `MorphologyZone` enum already has the
|
||
right vocabulary entry; the only thing missing is the DATA SOURCE that
|
||
decides when to emit it.
|
||
- **Source at derive time:** for a gridunit whose position falls inside a
|
||
`HydrologyResult` basin's `cells` (row-major cell indices at the 512×256
|
||
working-grid resolution the solver runs at — see the projection question
|
||
below), emit `MorphologyZone::Lake` instead of falling through to the
|
||
`ocean_fraction_q`-threshold heuristic. The heuristic remains the fallback
|
||
for cells NOT covered by any basin (i.e., it still decides open-ocean vs.
|
||
land at the coarse working-grid resolution the way it does today) — this
|
||
is additive, not a replacement of the existing sea-detection logic.
|
||
- **This is STATIC equilibrium geometry, correctly homed separately from
|
||
Araminta's `flooded` sim-state plane.** A lake basin's existence and
|
||
footprint is exactly as static as `morphology`/`elev_q` themselves —
|
||
`HydrologyResult` is a pure function of `(elevation, sea_level, climate)`,
|
||
computed once, byte-identical forever (T-1177's own determinism proof).
|
||
`flooded` (Araminta's plane, TTL-bound to the S4 sim-state formula above)
|
||
is about CURRENT sim state changing over game time — a river cresting its
|
||
banks this season, not a basin's settled existence. Conflating the two
|
||
would have meant a lake's presence-on-the-map flickers on the same TTL as
|
||
seasonal flood state, which is wrong for exactly the reason Jeroen's
|
||
question implies: a lake is there or it isn't, at generation time, and
|
||
that fact doesn't need re-deriving on a sim clock.
|
||
|
||
**Endorheic vs. overflow lakes — same water class, no client-relevant
|
||
distinction at the map-art level.** `BasinOutcome::Endorheic` vs.
|
||
`BasinOutcome::Overflow` is a real, meaningful distinction to the SOLVER
|
||
(it decides whether an outlet path gets carved at all — see the cliff/
|
||
`channel_depth` field's own dependency on a basin having an `Overflow`
|
||
outcome), but nothing about how the client should COLOR or RENDER a lake
|
||
gridunit depends on which outcome produced it — both are `MorphologyZone::
|
||
Lake` on the wire, no additional bit needed. The one place the distinction
|
||
DOES matter to the client is exactly the place a channel already carries it:
|
||
an `Overflow` basin's outlet is what populates the sparse `cliffs:
|
||
Vec<CliffSegment>` list (round 2 §(a)) at cells along `outlet_path` where
|
||
`channel_depth_scaled > 0` — so the endorheic/overflow distinction is
|
||
already client-visible, just via the EXISTING cliff mechanism, not a new
|
||
field on the lake classification itself. No double-encoding needed.
|
||
|
||
**Byte cost: near-zero, confirmed by the same reasoning T-1177's cliff
|
||
finding already established.** Lake regions are, by construction,
|
||
CONTIGUOUS (a basin's `cells` are one flood-filled component) — exactly the
|
||
shape PNG's DEFLATE compresses best (T-1179's own per-field RLE table:
|
||
`morphology`/`vegetation`, the two genuinely piecewise-constant
|
||
classification fields, compress to a handful of runs at 330K cells). Since
|
||
this change reuses the EXISTING `morphology` field rather than adding a new
|
||
one, there is no new byte cost to price at all — the only change is which
|
||
DISCRIMINANT value gets written into cells that were already being
|
||
classified, and `Lake`'s discriminant (1) compresses exactly as well as
|
||
every other `morphology` value already does.
|
||
|
||
**Where the hydrology solve slots into the derive pipeline — the projection
|
||
question, answered precisely:**
|
||
|
||
`HydrologyResult` is computed ONCE per body, at the 512×256 working-grid
|
||
resolution (T-1177's own scope — "solve() is called once per body... same
|
||
way `drainage::analyze` already runs once per body today"), held in the
|
||
D-203-shaped global-tier cache extension my round-2 cache spec already
|
||
proposed. A finer-rung canvas derive (District through Chunk) needs to know,
|
||
for each gridunit's world position, whether that position falls inside a
|
||
basin — **this is exactly the same projection problem `sea_level` already
|
||
solves today**, and should use the identical mechanism: `TerrainAnalysis`
|
||
(the working-grid analysis struct `derive_at_metres` already reads for
|
||
`ocean_fraction_q` via bilinear sampling) gains a basin-membership field
|
||
(a per-working-grid-cell basin id or a simple bool, populated from
|
||
`HydrologyResult.basins[*].cells` once when hydrology solves) sampled the
|
||
SAME way `ocean_mask` is sampled today — bilinear/nearest lookup at the
|
||
gridunit's world position against the 512×256 working grid, not a
|
||
re-solve at the finer rung's own spacing. This is the SAME "coarse baseline,
|
||
finer classification decision" pattern round 2 §(b)'s seed-chaining
|
||
reconciliation already ruled correct (mechanism B: read a coarser rung's
|
||
CONTINUOUS PRIMITIVE as an input to a fresh classification, never re-derive
|
||
the coarser thing itself) — the hydrology basin membership is exactly
|
||
mechanism-B shaped: computed once at 512×256, sampled (not re-solved) at
|
||
every finer rung, feeding straight into `derive_morphology_zone`'s existing
|
||
water-classification tier alongside `ocean_fraction_q`.
|
||
|
||
**No new bench needed to confirm this is affordable.** Sampling one more
|
||
bilinear field (basin membership) alongside the `ocean_fraction_q` sample
|
||
`derive_at_metres` already performs every cell, every rung, is the same
|
||
shape of work as the sample it's replacing/augmenting — my own T-1178/T-1154
|
||
per-cell rate numbers (~190–220 ns/cell parallel, flat across every rung
|
||
this workshop measured) already include equivalent-cost sampling operations
|
||
in that per-cell budget. This is a derivation-pipeline WIRING change (new
|
||
field on `TerrainAnalysis`, one more gate in `derive_morphology_zone`), not
|
||
a new cost category.
|
||
|
||
### 6. GLOBAL-TIER MATH RECHECK — corrected number
|
||
|
||
Jeroen's correction to the ladder top: **GLOBAL is rung 0**, the body-surface
|
||
opener, with a **VARIABLE canvas = the body's own region grid** — one
|
||
gridunit PER REGION (`regions_per_equator(R) × regions_per_equator(R)/2`,
|
||
D-243's elastic seam, `scale::regions_per_equator`), not a fixed
|
||
3840×2160-cell District-spacing canvas the way my round-2 cache-tier spec's
|
||
~174 MB figure assumed. **REGION is rung 1**, the largest FIXED-size rung —
|
||
viewport-sized and evictable like every other sub-global rung, not part of
|
||
the always-keep tier.
|
||
|
||
**My round-2 174 MB figure was computed against the wrong canvas shape** —
|
||
I'd sized the "global tier" as if it were a full District-spacing
|
||
step-0 canvas (8.3M cells/body, the same size as every other rung's canvas),
|
||
which is exactly the assumption Jeroen's correction replaces. Recomputed
|
||
against the correct rung-0 shape (one gridunit per region), using REAL
|
||
per-body radii from `systems.db` (`BodyParamsReader`, the project's
|
||
established read-only accessor — never raw `sqlite3`), across the real
|
||
267-body committed population (same set T-1177's population survey used):
|
||
|
||
**New bench:** `server/tests/bmv_global_tier_bench.rs`
|
||
(`bench_global_tier_bytes_real_population`). Discovers every committed
|
||
body_id (same walk as the T-1177 population survey), reads each body's real
|
||
`body_radius_km` from `systems.db`, computes
|
||
`regions_per_equator(R) × regions_per_equator(R)/2` cells per body, sums
|
||
across the population, and prices the total at both the raw 6 B/cell rate
|
||
(`DistrictWindowLayer`'s own documented figure) and T-1179's measured
|
||
PNG-per-field rate (638,382 bytes / 331,776 cells = 1.924 B/cell).
|
||
|
||
```bash
|
||
cd server
|
||
cargo test --release --test bmv_global_tier_bench -- --ignored --nocapture
|
||
```
|
||
|
||
**Results — MEASURED (deterministic: real radii + a pure formula, byte-
|
||
identical on re-run, confirmed):**
|
||
|
||
| | Value |
|
||
|---|---:|
|
||
| Bodies (real radius found) | 267 / 267 |
|
||
| Total rung-0 cells across population | 4,825,615 |
|
||
| Average cells/body | 18,073 |
|
||
| **Total bytes, raw 6 B/cell** | **27.61 MB** |
|
||
| **Total bytes, PNG-per-field (1.924 B/cell)** | **8.85 MB** |
|
||
|
||
**The corrected always-keep global-tier figure is ~8.85 MB PNG-encoded
|
||
across the real population (~27.6 MB raw) — roughly 20× smaller than my
|
||
original 174 MB estimate.** This is the number for Tyre's bracket. The
|
||
correction is exactly the direction Jeroen's ruling implied ("dramatically
|
||
smaller") and for exactly the reason his correction identifies: a
|
||
region-grid canvas (one gridunit per ~205 km region) is a MUCH coarser
|
||
sample than a District-spacing canvas (one gridunit per 2,048 m) — roughly
|
||
100× coarser per axis, ~10,000× fewer cells per body at the same world
|
||
extent, which is the entire point of region being a real, distinct,
|
||
coarser rung rather than a relabeling of what District already does at
|
||
canvas resolution.
|
||
|
||
**Range across the real population, for context:** largest rung-0 canvas is
|
||
GJ325Ac at 25,200 cells (47.4 KB PNG-encoded) — a body with radius
|
||
7,317.6 km, close to the largest bodies in the committed set; smallest is
|
||
GJ784c-m1 (a moon, radius 733.9 km) at just 253 cells (0.5 KB). The
|
||
Earth-class reference point (R=6371 km) lands at 195×97 = 18,915 cells,
|
||
matching the brief's own "~195×98 ≈ 19K" framing almost exactly (97 vs. 98
|
||
rows — the one-off is `cols/2` integer division vs. the brief's own rounding
|
||
of a half-circumference figure; not a discrepancy worth chasing further,
|
||
both land at the same order of magnitude the brief already cited).
|
||
|
||
**What does NOT change:** the eviction POLICY (Tier 1 = keep-always, never
|
||
evicted by time-since-last-visit; Tier 2 = sub-global geometry, storage-
|
||
evicted on time-since-last-visit) is unaffected by this correction — only
|
||
the BYTE BUDGET the keep-always policy commits to shrinks. The D-203-shaped
|
||
resource extension proposal (`orbital_canvas` field on `BodyWorldState` or a
|
||
sibling resource) also stands unchanged — it was never sized to the wrong
|
||
174 MB figure in a way that required different code, only a different
|
||
comment about how big the resident data actually is. At ~8.85 MB across the
|
||
ENTIRE real population, this budget is trivially affordable as permanent
|
||
process-resident memory (not just disk-safe, which was already true at
|
||
174 MB) — worth noting since it may simplify Stig's disk-vs-memory framing
|
||
for this ONE tier specifically (Tier 1 only; Tier 2/sub-global geometry
|
||
still wants the disk-backed, storage-evicted treatment his and my specs
|
||
already agree on).
|
||
|
||
**Code changes:** `server/tests/bmv_global_tier_bench.rs` — new file, one
|
||
`#[ignore]`d release bench. Reads `server/data/systems.db` read-only via the
|
||
existing `BodyParamsReader` accessor (asset-pipeline golden rule respected —
|
||
no raw `sqlite3`, no write path touched). No `server/src/` file modified.
|
||
|
||
### Rung-0 derive cost — MEASURED, not extrapolated (for Tyre's §(c) Option D table)
|
||
|
||
Tyre also asked for a derive-cost figure for rung-0's own row in the ladder
|
||
table. The existing `bench_derive_orbital_at_metres_region_spacing`
|
||
(`zoom_ladder_bench.rs`) measures `derive_orbital_at_metres`'s per-cell rate
|
||
at a fixed 4,096-cell (64×64) sweep (**884.0 ns/cell single-thread**,
|
||
directly measured, ~2.13× cheaper than a full `derive_at_metres`
|
||
classification call — no `invent_primitives`, bilinear region-baseline blend
|
||
only) but its own full-canvas row is explicitly labelled "EXTRAPOLATED from
|
||
the measured per-cell rate," not independently measured at real canvas
|
||
shape. Per this workshop's own anti-extrapolation discipline (the T-1143
|
||
planetary-rung post-mortem this whole workshop exists to avoid repeating), I
|
||
ran the REAL per-body canvas shape end-to-end instead of scaling the
|
||
4,096-cell number up.
|
||
|
||
**New bench:** `server/tests/bmv_global_tier_bench.rs::bench_rung0_derive_cost_real_canvas_shapes`.
|
||
Calls `derive_orbital_at_metres` directly over the actual `cols × rows`
|
||
extent three representative real bodies would use (smallest, Earth-class,
|
||
largest — the same population `bench_global_tier_bytes_real_population`
|
||
surveys).
|
||
|
||
| Body | Radius | Canvas (cols×rows) | Cells | Wall time | ns/cell |
|
||
|---|---:|---|---:|---:|---:|
|
||
| GJ784c-m1 (smallest, moon) | 733.9 km | 23×11 | 253 | 0.22–0.23 ms | 851–912 |
|
||
| Earth-class reference | 6,371.0 km | 195×97 | 18,915 | 15.90–16.19 ms | 835–856 |
|
||
| GJ325Ac (largest) | 7,317.6 km | 225×112 | 25,200 | 20.94–21.34 ms | 831–847 |
|
||
|
||
**MEASURED, run twice for stability, both passes agree within ~5-8%** (the
|
||
same run-to-run noise band this workshop's other 4,096-cell-scale benches
|
||
show). Per-cell rate is flat across all three body sizes (~830–910 ns/cell),
|
||
consistent with `bench_derive_orbital_at_metres_region_spacing`'s own
|
||
884.0 ns/cell figure — no size-dependent degradation.
|
||
|
||
**Direct answer to Tyre's two requests:**
|
||
|
||
1. **Single-body rung-0 derive cost: ~16–21 ms single-thread**, depending on
|
||
body radius (larger bodies have proportionally more regions per equator,
|
||
hence more cells) — trivially interactive, one order of magnitude below
|
||
any step-cross tolerance this workshop has used.
|
||
2. **All-267-bodies-summed single-thread total (the worst-case ceiling, using
|
||
the real total cell count 4,825,615 from the byte-cost bench above):
|
||
~4.0 s.** Stated precisely as a ceiling, not a real production cost:
|
||
rung-0 is populated lazily, once per body, on that body's first
|
||
Atlas-open (same D-206 background-queue population path every other
|
||
cached layer already uses) — no production path solves all 267 bodies'
|
||
rung-0 canvases synchronously in one batch. The per-body row is the
|
||
number that matters for "how long until a fresh Atlas-open feels
|
||
snappy" (~16–21 ms, imperceptible); the summed total is only useful as
|
||
an upper bound on total server-side compute if every body were opened
|
||
once, back to back — which is a stress ceiling, not a real request
|
||
pattern (mirrors exactly how T-1177's 273-body hydrology figure works:
|
||
the meaningful number is per-body cost, the summed total is a sanity
|
||
ceiling).
|
||
|
||
Both numbers are real and can go directly into Tyre's bracket without
|
||
qualification — no extrapolation anywhere in this section.
|
||
|
||
---
|
||
|
||
## LAKES CONVERGENCE (2026-07-23): Araminta's three routed questions — §1 SUPERSEDED-BY-CROSSING, see the correction block immediately below
|
||
|
||
**SUPERSEDED-BY-CROSSING (2026-07-23, later same day).** Everything under
|
||
§1 and the struct in §3 below answers a framework Araminta has since
|
||
**withdrawn**: a new `water: Vec<u8>` dense field. Her final position,
|
||
reached independently by verifying my own code-read at source, **adopts
|
||
the morphology-fold I proposed in the POST-RATIFICATION ADDENDUM above**
|
||
(`MorphologyZone::Lake` sourced from `HydrologyResult`, zero new fields) —
|
||
not the `water` field this section was built against. Both of us landed
|
||
on the morphology-fold independently, which is worth stating plainly: it
|
||
isn't one of us conceding to the other, it's convergent verification. **§2
|
||
(the pipeline/projection ruling — filled-surface continuous sampling)
|
||
stands and is ADOPTED UNCHANGED** — that reasoning is agnostic to which
|
||
field carries the result (morphology-fold or a dedicated `water` field
|
||
both need the same continuous-sampling mechanism to avoid the blocky-edge
|
||
problem), so nothing about the correction touches it. **§3's struct is
|
||
WRONG and superseded** — the real struct is 6 static + 2 sim-state dense
|
||
(8 total) + 2 sparse (`courses`, `cliffs`), Araminta's final shape,
|
||
corrected further down this section. §1's crossover data (dense-vs-sparse
|
||
for CARRYING a bit) is **repurposed, not discarded** — see the endorheic-cue
|
||
ruling below, which uses this data for a different question than the one
|
||
§1 originally answered.
|
||
|
||
*(§1/§3 text below is left in place, unedited, as the historical record of
|
||
what was superseded — do not cite it as current. The corrected content
|
||
starts at "### Endorheic cue under morphology-fold" after §3.)*
|
||
|
||
---
|
||
|
||
Araminta's `araminta-round2.md` §(e) specs `water: Vec<u8>` (`{None=0,
|
||
Lake=1, Sea=2}`, static plane, new dense field) — I agree with her
|
||
reasoning outright (frozen `morphology` vocabulary stays frozen, bit-packing
|
||
already loses on T-1179's own DEFLATE finding, a dedicated field costs
|
||
almost nothing given lake regions are contiguous). Her three questions,
|
||
answered below.
|
||
|
||
### 1. Endorheic carrier sizing — sparse `lakes: Vec<LakeBasin>` vs. `water`'s 4th value
|
||
|
||
**Ruling: `water` gains a 4th value (`LakeEndorheic=3`). Do NOT add a
|
||
sparse `lakes: Vec<LakeBasin>` list.** This is a clean, decisive result,
|
||
not a close call — I built the actual crossover analysis rather than
|
||
reasoning from the single 412,700-cell synthetic data point T-1177's
|
||
original appendix carried (that number is from a SYNTHETIC ridged-terrain
|
||
grid at 8.3M cells, a canvas size no production path derives hydrology at
|
||
synchronously — not a real-population statistic, and not what the
|
||
crossover call should be argued from).
|
||
|
||
**New bench:** `server/tests/hydrology_equilibrium_bench.rs::bench_per_basin_size_distribution_real_population`.
|
||
Solves every real committed body at the real 512×256 production working
|
||
grid (same 267-body population, same solver, same grid size as the
|
||
existing T-1177 population survey) and records every INDIVIDUAL basin's
|
||
cell count — not just the aggregate total the original survey reported.
|
||
|
||
**Results — MEASURED, run twice for stability, byte-identical both times
|
||
(deterministic: real terrain, no randomness), cross-checked against the
|
||
population survey's own total (2,694,012 lake cells — exact match):**
|
||
|
||
| | Value |
|
||
|---|---:|
|
||
| Total basins | 22,270 |
|
||
| Min basin size | 1 cell |
|
||
| Max basin size | 18,782 cells |
|
||
| Mean | 121.0 cells |
|
||
| Median (p50) | 18 cells |
|
||
| p90 / p95 / p99 / p99.9 | 246 / 486 / 1,703 / 7,128 cells |
|
||
|
||
**Histogram:**
|
||
|
||
| Bucket | Basins | % of basins | Cells | % of lake cells |
|
||
|---|---:|---:|---:|---:|
|
||
| 1–10 | 8,894 | 39.94% | 33,399 | 1.24% |
|
||
| 11–50 | 6,299 | 28.28% | 160,553 | 5.96% |
|
||
| 51–200 | 4,402 | 19.77% | 451,537 | 16.76% |
|
||
| 201–1,000 | 2,236 | 10.04% | 951,614 | 35.32% |
|
||
| 1,001–5,000 | 401 | 1.80% | 783,355 | 29.08% |
|
||
| 5,001–20,000 | 38 | 0.17% | 313,554 | 11.64% |
|
||
| 20,001+ | 0 | 0.00% | 0 | 0.00% |
|
||
|
||
**The crossover analysis, priced at both carriers' real byte rates
|
||
(sparse `Vec<(u16,u16)>` = 4 raw bytes/cell, a pessimistic/conservative
|
||
sparse estimate since it doesn't apply MessagePack's own compact framing
|
||
discount; dense = T-1179's measured PNG-per-field rate, 1.924 bytes/cell):**
|
||
|
||
**Dense wins for 100% of basins, at every size measured, including the
|
||
largest (18,782 cells).** Zero basins in the real population cross over to
|
||
where sparse would be cheaper. This isn't close: even a single-cell basin
|
||
costs 4 bytes sparse vs. ~1.9 bytes dense-if-isolated, and the moment a
|
||
basin has ANY neighboring cells (which every real basin does, by
|
||
construction — `label_lake_basins`' BFS only produces contiguous regions),
|
||
DEFLATE's run-length win on the dense field only widens the gap. **The
|
||
naive per-cell byte-rate comparison alone (4 vs. 1.924) already means
|
||
dense wins point-for-point before any compression benefit is even
|
||
counted** — sparse's `Vec<(u16,u16)>` shape has no structural advantage
|
||
over dense here the way it does for genuinely sparse, spatially-isolated
|
||
features (settlements, cliff edges) precisely because lake cells are
|
||
NEVER isolated — a basin's very definition is a contiguous flood-filled
|
||
region, which is the exact shape a raster field compresses best and a
|
||
per-cell coordinate list compresses worst (a coordinate list gets zero
|
||
benefit from spatial contiguity; a raster field gets maximum benefit from
|
||
it).
|
||
|
||
**Why this differs from the cliff/`CliffSegment` sparse-list decision
|
||
(round 1/2, still correct, not being revisited):** cliffs are RARE (zero
|
||
carved cells across the entire real population, T-1177's population
|
||
survey addendum) — a sparse list of a feature that's empty on 267/267
|
||
real bodies costs nothing because it's usually zero-length. Lakes are the
|
||
opposite: EVERY body in the population has multiple lake basins (22,270
|
||
basins across 267 bodies, ~83 basins/body average), and every basin is a
|
||
contiguous blob, not a rare point event. The two features look superficially
|
||
similar (both are "extra water-related hydrology output") but sit on
|
||
opposite sides of the sparse-vs-dense argument for structural reasons, not
|
||
because one measurement contradicts the other.
|
||
|
||
**Concrete consequence for Araminta's schema:** drop `lakes:
|
||
Vec<LakeBasin>` from `EncodedStepCanvas` entirely — it was always marked
|
||
as her fallback-pending-my-numbers, and the numbers say the fallback is
|
||
the answer. `water: Vec<u8>` becomes 4-state: `{None=0, Lake=1, Sea=2,
|
||
LakeEndorheic=3}`. This also SIMPLIFIES the schema relative to her
|
||
provisional draft (one dense field instead of one dense field plus a
|
||
sparse list), which is a strictly better outcome than either of us
|
||
expected going in — the crossover question had a clean, one-sided answer
|
||
rather than needing a size-dependent hybrid rule.
|
||
|
||
**One consequence worth flagging for Stig's map-art rule (her own
|
||
constraint, restated against the concrete field values):** the
|
||
endorheic/overflow visual distinction she specified ("secondary visual
|
||
cue... never competing with sea-vs-lake") maps directly onto `Lake` vs.
|
||
`LakeEndorheic` as two `water` discriminants that share styling family —
|
||
the client reads `water == 1 || water == 3` for "this is a lake, color it
|
||
lake-blue," and additionally checks `water == 3` for the secondary
|
||
endorheic cue (the outline/texture/saturation treatment she described).
|
||
This is a natural fit for the discriminant-value shape and needs no
|
||
special-casing beyond what a 4-state enum already gives a colorizer.
|
||
|
||
### 2. Pipeline slot — where `water` gets populated, and the projection answer
|
||
|
||
Confirming and sharpening what I already worked out for the coordinator's
|
||
earlier lake-schema question (same underlying mechanism, now answered
|
||
against Araminta's specific field shape):
|
||
|
||
**`HydrologyResult` solves once per body at 512×256** (T-1177's own scope,
|
||
unchanged), held in the Tier 1 keep-always cache alongside rung-0's own
|
||
data. A finer-rung canvas derive needs, for each gridunit's world
|
||
position, the `water` classification at that position.
|
||
|
||
**Direct answer to the specific question posed: does `water` derive
|
||
per-gridunit from the FILLED SURFACE (elevation < filled level = water),
|
||
or does it project BASIN-CELL MEMBERSHIP?**
|
||
|
||
**Rule: from the filled surface, sampled continuously — NOT basin-cell
|
||
membership projected as a discrete lookup. This is the right answer under
|
||
D-227's invention discipline, and here's the precise reasoning, not just
|
||
the conclusion:**
|
||
|
||
- **Basin-cell membership is a WORKING-GRID-RESOLUTION fact** — `Basin.cells`
|
||
is a `Vec<usize>` of row-major indices into the 512×256 grid. Projecting
|
||
membership directly (nearest-cell or bilinear-bool lookup against a
|
||
512×256 boolean mask, exactly how `ocean_mask`/`ocean_fraction_q` work
|
||
today) would make a lake's EDGE only as precise as the 512×256 working
|
||
grid's own cell size — at District spacing (2,048 m) that's coarser than
|
||
the 512×256 grid itself in the wrong direction is fine (the grid is
|
||
finer), but at Chunk spacing (64 m) a bilinear-bool sample of a
|
||
512×256-resolution mask produces a BLOCKY lake edge that doesn't refine
|
||
as the rung gets finer — exactly the "magnified interpolation of a
|
||
coarser composite" D-166's corollary forbids, the same error class this
|
||
entire workshop exists to eliminate.
|
||
- **The filled surface (`filled_scaled: Vec<i64>`, also 512×256-resolution)
|
||
has the SAME resolution problem if sampled the same crude way** — but it
|
||
doesn't have to be, because `filled_scaled` is a CONTINUOUS quantity
|
||
(an elevation-like field, water-surface height), not a boolean mask.
|
||
Comparing a bilinearly-INTERPOLATED filled-surface sample against a
|
||
bilinearly-interpolated ORIGINAL elevation sample at the gridunit's exact
|
||
world position (both sampled the same way `elev_q` already is — this is
|
||
literally the same sampling mechanism `ocean_fraction_q` uses today for
|
||
sea, just against `filled_scaled` instead of the raw heightmap) produces
|
||
a lake edge that's a smooth function of position, refining naturally as
|
||
the rung gets finer — exactly the same way coastlines already refine
|
||
today via `coast_invention`'s domain-warp on the SAME kind of continuous
|
||
primitive.
|
||
- **Concretely: `water_at_position = if bilinear(filled_scaled, pos) >
|
||
bilinear(original_elevation, pos) { Lake-or-Endorheic } else if
|
||
bilinear(original_elevation, pos) < sea_level { Sea } else { None }`** —
|
||
three continuous comparisons at the gridunit's own world position, the
|
||
same shape as the sea-level check `derive_morphology_zone` already
|
||
performs via `ocean_fraction_q`, just reading `HydrologyResult`'s
|
||
`filled_scaled` field instead of (or alongside) the raw elevation. This
|
||
is mechanism-B-shaped from the seed-chaining reconciliation (round 2 §(b)
|
||
and my earlier lake-schema note): a coarser rung's CONTINUOUS PRIMITIVE
|
||
(the filled-surface field, computed once at 512×256) is sampled fresh at
|
||
every finer rung's own position, never re-solved, never read as a
|
||
discrete cached lookup.
|
||
- **`LakeEndorheic` vs. `Lake`:** since `Basin.outcome` is a per-BASIN
|
||
property (constant across all of a basin's cells, not itself a
|
||
continuous field), it needs one more piece: which basin (if any) a
|
||
position falls inside, to know which `outcome` applies. This IS a
|
||
discrete lookup (basin id per working-grid cell, nearest-sample not
|
||
bilinear — you can't blend "half endorheic, half overflow"), but it only
|
||
matters at the water/land BOUNDARY resolution question is already
|
||
resolved by the continuous `filled_scaled` comparison above — the
|
||
endorheic/overflow classification is uniform across the interior of a
|
||
water region, it only needs a coarse per-basin lookup (populate a
|
||
512×256 basin-id grid once when hydrology solves, nearest-sample it),
|
||
never a fine-grained boundary computation of its own.
|
||
|
||
**No new bench needed to confirm this is affordable** — sampling one more
|
||
bilinear field (`filled_scaled`) plus one nearest-sample lookup (basin id,
|
||
only evaluated where the continuous check already says "this is water") is
|
||
the same shape of work `ocean_fraction_q`'s existing sample already
|
||
performs every cell, every rung. My own T-1178/T-1154 per-cell rate numbers
|
||
(~190–220 ns/cell parallel, flat across every rung) already include
|
||
equivalent-cost sampling operations in that per-cell budget — this is a
|
||
derivation-pipeline WIRING change, not a new cost category, exactly as I
|
||
told the coordinator for the original lake-schema question.
|
||
|
||
### 3. Struct field count — reconciled
|
||
|
||
Araminta's `EncodedStepCanvas` in her round-2 draft lists 10 dense fields
|
||
(counting `flooded` and the now-superseded `water`+`lakes` pair) plus 2
|
||
sparse lists. Per question 1's ruling above (`lakes` dropped, `water`
|
||
becomes 4-state), the reconciled field count against my `StepCanvasResponse`/
|
||
`EncodedStepCanvas` design (round 2 §(a)) is:
|
||
|
||
```rust
|
||
pub struct EncodedStepCanvas {
|
||
pub width: u32,
|
||
pub height: u32,
|
||
// STATIC GEOMETRY PLANE (7 dense fields)
|
||
pub morphology: EncodedField,
|
||
pub elev_q: EncodedField,
|
||
pub temp_dc: EncodedField,
|
||
pub moisture_q: EncodedField,
|
||
pub vegetation: EncodedField,
|
||
pub settlement_id: EncodedField,
|
||
pub water: EncodedField, // {None,Lake,Sea,LakeEndorheic} — 4-state, NEW
|
||
// SIM-STATE PLANE (2 dense fields)
|
||
pub glaciation: EncodedField,
|
||
pub flooded: EncodedField,
|
||
// SPARSE LISTS (2 — courses existing, cliffs ratified; NO lakes list)
|
||
pub courses: Vec<RiverCourse>,
|
||
pub cliffs: Vec<CliffSegment>,
|
||
}
|
||
```
|
||
|
||
**9 dense fields total (7 static + 2 sim-state), 2 sparse lists — one
|
||
fewer field than Araminta's provisional 10-dense-plus-lakes count, because
|
||
`water`/`lakes` collapse into ONE field (`water`, 4-state) rather than
|
||
staying two separate schema elements.** This is the single authoritative
|
||
field list per her own (d) note ("keeps the struct's field list as one
|
||
authoritative list rather than two documents each claiming to be
|
||
current") — confirming here so both documents converge on the same struct
|
||
rather than each claiming a slightly different one.
|
||
|
||
**Envelope-framing consequence: none.** Exactly as both of us already
|
||
argued for `flooded` and the original `water` addition — one more/fewer
|
||
field derived in the same row-chunked pass (or, for `water`, in a pass
|
||
keyed on the same `HydrologyResult` `cliffs` already draws from) doesn't
|
||
reopen the flat-envelope ruling. This is field-set growth on the existing
|
||
flat `StepCanvasResponse`, not a new response type or a dense/sparse
|
||
split — my round-2 §(a) reasoning against splitting the envelope applies
|
||
unchanged to a 9-field struct exactly as it did to a 6-field one.
|
||
|
||
*(§3 above is superseded by the crossing — the real struct has no `water`
|
||
field at all. Corrected below.)*
|
||
|
||
---
|
||
|
||
## CORRECTED (2026-07-23, post-crossing): struct, final; endorheic cue, ruled
|
||
|
||
### Struct, Araminta's final shape — confirmed, no dissent
|
||
|
||
`MorphologyZone::Lake` (discriminant 1, already in the frozen 17-zone
|
||
vocabulary) is sourced from `HydrologyResult` per the POST-RATIFICATION
|
||
ADDENDUM's morphology-fold above — zero new fields for the lake/sea
|
||
distinction itself. The corrected, authoritative struct:
|
||
|
||
```rust
|
||
pub struct EncodedStepCanvas {
|
||
pub width: u32,
|
||
pub height: u32,
|
||
// STATIC GEOMETRY PLANE (6 dense fields)
|
||
pub morphology: EncodedField, // Lake/OpenOcean now sourced from HydrologyResult
|
||
pub elev_q: EncodedField,
|
||
pub temp_dc: EncodedField,
|
||
pub moisture_q: EncodedField,
|
||
pub vegetation: EncodedField,
|
||
pub settlement_id: EncodedField,
|
||
// SIM-STATE PLANE (2 dense fields)
|
||
pub glaciation: EncodedField,
|
||
pub flooded: EncodedField,
|
||
// SPARSE LISTS (2)
|
||
pub courses: Vec<RiverCourse>,
|
||
pub cliffs: Vec<CliffSegment>,
|
||
}
|
||
```
|
||
|
||
**8 dense fields (6 static + 2 sim-state), 2 sparse lists.** No `water`
|
||
field anywhere. This is Araminta's final shape; I have no dissent — it's
|
||
strictly simpler than either of our provisional drafts (mine at 9 fields,
|
||
hers at 10), because the morphology-fold makes the lake/sea distinction
|
||
free to carry rather than requiring its own slot.
|
||
|
||
### Endorheic cue under morphology-fold — the actual open question
|
||
|
||
The coordinator is right that my §1 crossover analysis doesn't settle
|
||
this: that analysis compared dense-field-bit vs. sparse-coordinate-list
|
||
for carrying an endorheic signal, and **both of those options are now
|
||
foreclosed** — a dense bit under morphology-fold means either widening
|
||
the frozen 17-zone vocabulary to 18 (option ii below) or resurrecting a
|
||
new field (which contradicts the entire point of the fold), and a sparse
|
||
per-basin list was never Araminta's proposal for the cue specifically, it
|
||
was for the abandoned `lakes: Vec<LakeBasin>` carrier. The real question
|
||
— no wire signal at all vs. a vocabulary change vs. a deferred mechanism
|
||
— is a different decision than the one my crossover data answered, and I
|
||
want to be precise that I'm not stretching that data to cover a question
|
||
it wasn't built for.
|
||
|
||
**My pick: (i), Araminta's outflow-course-presence inference. No wire bit
|
||
at all for endorheic-vs-overflow; the cue arrives as a side effect of
|
||
wiring basin outlets into the courses/river-network machinery, which is
|
||
work T-1170 Ruling 7b already reserved room for and which the map needs
|
||
done anyway.**
|
||
|
||
**Rationale, grounded in the actual basin-outcome data:**
|
||
|
||
- **Proportionality — 1,030 of 22,270 basins (4.63%) are endorheic.** This
|
||
is the number that should drive the vocabulary-change call, and it
|
||
argues against option (ii) specifically. An 18th `MorphologyZone` entry
|
||
is a permanent widening of a D-239 frozen vocabulary — every consumer
|
||
of that enum (client colorizer, any future classification logic, the
|
||
one-colorizer-family guarantee Araminta's own round-1 §3 argues for)
|
||
inherits the new arm forever, to carry a distinction that's true for
|
||
fewer than 1 in 20 lake basins. A frozen vocabulary earns its frozen
|
||
status by being conservative about additions; 4.63% is not the kind of
|
||
frequency that should be the bar-clearing case for reopening it,
|
||
especially when a zero-cost alternative exists that reaches the same
|
||
end state.
|
||
- **Option (i) has zero marginal wire cost and zero marginal vocabulary
|
||
cost, and the work is required regardless of this decision.** An
|
||
overflow lake's exit river is real hydrology output — `outlet_path` is
|
||
already a non-empty, guaranteed field on every `BasinOutcome::Overflow`
|
||
(confirmed by direct read: `hydrology_equilibrium.rs`'s own
|
||
`overflowing_basin_has_nonempty_outlet_path` test asserts exactly this).
|
||
Wiring that path into the river-course/`RiverNetwork` machinery so an
|
||
overflow lake visibly shows its exit river on the map is not a
|
||
cue-specific feature — it's the map correctly showing hydrology that
|
||
already exists, which the workshop needs for overflow lakes to read as
|
||
hydrologically complete regardless of whether anyone cares about the
|
||
endorheic distinction. The cue is a **free side effect** of doing that
|
||
work, not an additional feature built to carry it. This is exactly the
|
||
shape T-1177's own `RIVER_DOWNSTREAM_TERMINAL` sentinel reservation
|
||
anticipated: I flagged in the original T-1177 measurement doc that
|
||
`river_course::build_edges` already treats that sentinel as a safe
|
||
no-op, confirming the endorheic case was already designed to be
|
||
additive when this moment came — it has come, and it's additive.
|
||
- **The inference is structurally sound, not a fragile heuristic.** Every
|
||
`Overflow` basin has a real, non-empty `outlet_path`; every `Endorheic`
|
||
basin has none, by definition (that's what "endorheic" means — no
|
||
outflow). There's no edge case where the presence-of-exit-river signal
|
||
could misfire or need a tie-break rule the way, say, a quantized
|
||
threshold might. The client reads "does this lake have a river flowing
|
||
out of it" and that question has an unambiguous, always-correct answer
|
||
under the solver's own definitions — no cue-specific correctness burden
|
||
at all.
|
||
- **Option (iii) (defer with no mechanism recorded) is worse than (i) for
|
||
a reason specific to this workshop's own discipline, not a general
|
||
preference for shipping features.** The outlet-wiring work is going to
|
||
happen regardless (overflow lakes need to show SOME exit-river
|
||
behavior to be honest about their own hydrology — the current schema
|
||
can't even represent "this basin has settled but I'm choosing not to
|
||
draw where its water goes"). Recording "no mechanism, unscoped" would
|
||
either silently under-scope that ticket later or force a second design
|
||
pass to rediscover exactly what (i) already worked out. Since the
|
||
mechanism costs nothing extra to name now, naming it is strictly better
|
||
than deferring the naming.
|
||
|
||
**Verdict: (i).** Endorheic-vs-overflow ships with zero wire bits, zero
|
||
vocabulary change, as a client-side inference from whether a lake's
|
||
`morphology`-classified region has a river course exiting it — which is
|
||
answerable directly from the existing `courses: Vec<RiverCourse>` sparse
|
||
list once basin outlets are wired into it (a ticket-level follow-up,
|
||
pre-cleared by T-1170 Ruling 7b, not a wire-schema change). If Jeroen
|
||
wants the distinction visually stronger than "no exit river present" can
|
||
convey on its own (a genuinely different aesthetic call, not a data
|
||
question), that's a Stig-side styling decision layered on the same
|
||
zero-bit signal, not a reason to revisit the carrier.
|