diff --git a/governance/decisions/architecture.md b/governance/decisions/architecture.md index 898794a2a..74205a403 100644 --- a/governance/decisions/architecture.md +++ b/governance/decisions/architecture.md @@ -1615,10 +1615,14 @@ Technical foundation decisions that constrain implementation: engine, client-ser pub window_n: u32, } ``` - This is the identical pattern `StartupMessage.role` already uses (`bridge/types.rs`, D-254 §2) — an old client sending only `{body_id, up_to}` still decodes cleanly, `window_center` defaults to `None`, no protocol version bump, no new demux branch. `up_to` is unaffected and keeps gating which whole-body layers run; a window request can ride alongside any `up_to` value (the window path calls `derive_district` directly — it does not depend on which whole-body layers the cascade has cached, only on `TerrainAnalysis` + `BodyParams` being resolvable for the body, the same precondition `--render` already has). + This is the identical pattern `StartupMessage.role` already uses (`bridge/types.rs`, D-254 §2) — an old client sending only `{body_id, up_to}` still decodes cleanly, `window_center` defaults to `None`, no protocol version bump, no new demux branch. `up_to` is unaffected and keeps gating which whole-body layers run; a window request rides alongside any `up_to` value — the window derivation depends only on `TerrainAnalysis` + `BodyParams` being resolvable for the body (the same precondition `aliveness_probe --render` has), not on which whole-body layers the cascade has cached. + + **Serving model — the window derives on the Rayon background queue, NOT inline on the tick thread (binding).** `serve_atlas_requests` runs in `TickPhase::PreInput` and drains *all* queued atlas requests synchronously in one loop (`plugin.rs:99-128`), sharing that tick's response flush with the star-map/city-names/browse serve systems. Every expensive path in that system today goes to the Rayon background queue and returns `Pending` — `drain_generation_completions`' own doc is explicit that this drain is "a cheap channel drain + cache insert, **never the ~45 ms cascade itself**" (`plugin.rs:207`). A window derive at n=32/64 is ~7–29 ms (§4); running it inline would blow that "cheap" contract, and a **pan-burst would stack several inline derives in one drain loop**, delaying the entire tick's response flush (star-map, browse, everything). Client debounce (§5) is *courtesy* — the server cannot enforce it and must not depend on it. So a window request follows the **exact same background-queue pattern as a whole-body cache miss**: `handle_atlas_request` submits a window-derive work item to the `GenerationQueue` (a new `GenWorkItem` variant carrying `body_id` + `(center, n)` + the resolved terrain/params) rather than deriving inline; a later tick's `drain_generation_completions` receives the finished `DistrictWindowLayer` and caches it (keyed by `(body, center, n)`, alongside `BodyWorldState` or in a sibling window cache — a wiring-ticket call). Because `district_window` is an `Option`, an as-yet-underived window is simply served as `None` — the *same* "layer hasn't produced yet → `None`" signal every whole-body layer already uses, independent of the body-level `AtlasLayerStatus` (a body whose whole-body layers are cached still answers `Ready` with `district_window: None` until the window job completes; the client re-requests via the existing D-225 poll loop and gets the window on a later response once cached). This avoids overloading the body-level status with window-readiness — the `Option` carries it. §5's border-fade already covers this multi-tick wait as UX. **Recommended (not mandated here):** per-connection window-request coalescing — a newly-queued window request for the same body/connection supersedes an unserved older one, so a pan-burst collapses to one derive server-side even if the client's debounce let several through. The precise `GenWorkItem` shape, the coalescing key, and the completion-routing wiring are a follow-up-ticket concern (T-1137); this record fixes only that window derivation is background-queued like every other expensive atlas path, never inline on the PreInput drain. - **(2) Response carrier — a distinct payload, not a sixth/seventh dense-layer `Option` field (RULING, binding).** The growth-ceiling note on `AtlasLayerResponse` (this record's base text, re-affirmed by T-1112 §4's "last two candidates" framing) governs one specific family: **dense, whole-body, cache-keyed-on-body-alone layers** — `district_grid`, `road_graph`, `settlements`, `region_grid`, and `quarter_footprints` (T-1112/T-1119, landing concurrently with this design). A windowed district payload is a different kind of traffic by construction: its content is keyed on **`(body, center, n)`**, it is re-requested on every pan (not cached once per body and reused), and a stale response must be **detectable and discardable** by the client rather than silently rendered — none of which is true of the five/six-member family the ceiling was written for. Retrofitting it into that family as a bare `district_window: Option` sitting next to `region_grid` would misrepresent its semantics (implying the same "cached snapshot, always current" contract its neighbours have) even before the slot-count argument. The ceiling's subject is therefore explicitly re-scoped here to **the dense whole-body layer family** — it does not gate this field, and this field does not count against it. This is option (c) from the T-1124 refinement's three choices, chosen over (a) (a bare seventh/eighth `Option` peer — technically fits the struct, dishonestly fits the family) and (b) (a second response message — real complexity, a new framing concept, for a problem the existing struct already solves once the semantics are named correctly). + **Windowed-family ceiling (binding, replaces the migration trigger the re-scoping removed).** Re-scoping the whole-body ceiling to exclude windowed queries must not leave the windowed family *uncapped* — that would let a second windowed field (a windowed chunk-preview, a second simultaneous viewport) land frictionless as `district_window_2`, exactly the drift the whole-body cap exists to prevent. So the windowed family gets its own hard rule, mirroring the request side's "five HARD, a sixth migrates" discipline: **there is exactly ONE windowed-query field on `AtlasLayerResponse` (`district_window`), and a second windowed query is choice (b) — a dedicated response message — by rule, not by case-by-case judgment.** The rationale is symmetric with (b)'s rejection here: one windowed payload fits the existing response struct honestly (the client asked for a window, got a window); two concurrent windowed payloads riding one `AtlasLayerResponse` would need per-field request-correlation (which echo matches which in-flight request?) that the single-field echo-key design deliberately avoids — that correlation machinery *is* the tagged/multiplexed framing a dedicated message provides, so a second windowed consumer is the trigger to build it, not a reason to bolt a second `Option` on. So: `district_window` is the windowed family's five-HARD equivalent at one, and the next windowed field is a migration, full stop. + `AtlasLayerResponse` gains exactly one new field along these lines: ```rust pub struct AtlasLayerResponse { @@ -1631,7 +1635,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser pub district_window: Option, } ``` - `DistrictWindowLayer` **echoes `center`/`n` back on the response** — this is the client's race-condition guard, not a convenience field. Because window derivation is pure and deterministic (D-227: `derive_district` is a function of `(seed, body_id, body_params, terrain, district_pos)` only — no hidden request-order dependence), the same `(center, n)` query always yields the same payload, so the echoed tuple *is* the cache/staleness key: the client compares it against whichever window it most recently asked for and discards any response whose echo doesn't match (superseded by a later pan). No sequence number or request-id is needed — D-227's purity is what makes the echo sufficient. + `DistrictWindowLayer` **echoes `center`/`n` back on the response** — this is the client's race-condition guard, not a convenience field. Because window derivation is pure and deterministic (D-227: `derive_district` is a function of `(seed, body_id, body_params, terrain, district_pos)` only — no hidden request-order dependence), the same `(center, n)` query always yields the same payload, so the echoed tuple *is* the cache/staleness key: the client compares it against whichever window it most recently asked for and discards any response whose echo doesn't match (superseded by a later pan). No sequence number or request-id is needed — D-227's purity is what makes the echo sufficient. **`body_id` is not part of the echo tuple because it does not need to be:** the echo rides *inside* `AtlasLayerResponse`, whose existing `body_id` field already scopes the whole response to one body (the same field the whole-body layers use), and the response-routing path is per-connection-and-body already — so a body switch (or a window response arriving in-flight across a body switch) is disambiguated by the enclosing `AtlasLayerResponse.body_id`, not left to `(center, n)` to catch. The client's **cache key is the full `(body_id, center, n)`** (§4) — `body_id` from the response envelope, `(center, n)` from the echo — so cross-body confusion is ruled out by the routing that encloses the echo, and the echo's job is narrowed to exactly what it is good at: disambiguating *which window of the current body* a response answers. ```rust pub struct DistrictWindowLayer { pub center: DistrictPos, @@ -1651,14 +1655,14 @@ Technical foundation decisions that constrain implementation: engine, client-ser - **`vegetation` palette/legend must be exhaustive over `Marine = 6`.** T-1126 appended `Marine` to `VegetationClass` (open water, morphology-derived, never a threshold of its own) specifically because the district tier is where the ocean-blind-vegetation bug (frozen bodies reading Forest over open sea) was caught. Any client palette/legend for this field that omits `Marine` reintroduces exactly that bug at window resolution — non-negotiable inclusion, not a nice-to-have. - **T-1127's ceiling ruling generalizes to this whole field list.** Its refinement text is explicit: *"the cap governs new top-level Option layers, not per-cell fields inside a shipped layer"* — the precedent it cites (`elev_q` already riding inside `DistrictGridLayer` with no separate governance gate) is the same shape as all six fields here riding inside one `DistrictWindowLayer`. Per-cell field additions to an already-shipped layer are engineering, not governance, unless a field would reopen a frozen vocabulary (D-239 §6's 17-zone freeze) — none of these six do; `morphology_zone` and `vegetation_class` both ship their existing frozen/T-1126-amended discriminant sets unchanged. - - **(4) Budget (binding numbers).** Per-cell wire cost is **7 bytes** (`morphology` 1 + `elev_q` 1 + `temp_dc` 2 + `moisture_q` 1 + `vegetation` 1 + `glaciation` 1), before MessagePack array-header overhead (negligible at these sizes — six flat byte/i16 arrays, no per-element framing). **`DISTRICT_WINDOW_MAX_N = 64`** (request-side hard clamp, §1): 64×64 = 4,096 cells → 28 KiB raw payload. This is deliberately the same `n` as `aliveness_probe --render`'s default window (T-1123) — a value already proven to render correctly server-side and matching the "regional inspection" altitude the ticket names (64 districts × 2.048 km ≈ 131 km per side — city-and-hinterland scale, not planetary). **`DISTRICT_WINDOW_DEFAULT_N = 32`** (client's interactive default, 1,024 cells → 7 KiB) — half the cap, chosen so the common pan-and-look case stays comfortably inside a single-digit-millisecond derive cost even before any release-build speedup: at the ~7 µs/derive debug-build rate T-1123's window renderer measured, 1,024 cells ≈ 7 ms, 4,096 cells (the n=64 cap) ≈ 29 ms. **This is a debug-build figure with no committed release-build number yet** — release is expected meaningfully faster (no debug assertions, inlining) but the design does not presume a specific multiplier; if profiling later shows the cap needs to move, `DISTRICT_WINDOW_MAX_N` is the one constant to tune, not a redesign. Neither figure is gated by D-200's 5 ms on-demand tile-fill budget — that budget governs the main-tick-thread chunk-boundary path; this request is served off the D-226 item (1) paused-sim bridge path (Snapshot/PostSnapshot stay alive while Movement/Simulation/Economy/etc. freeze), so its budget is interactive-UX responsiveness on a paused screen, not a per-tick deadline. **Re-request-on-pan policy:** the client re-requests only when a **pan** carries the view past the held window's edge — **never on zoom** (§5): only pan changes `center`, and because window derivation is deterministic (D-227) a zoom-triggered re-fetch of the same `(center, n)` would spam byte-identical responses for zero new information. The client debounces pan motion (do not fire on every drag-frame delta); the exact debounce interval and whether windows snap to a fixed grid vs. float on the pan center are client-side screen decisions (Araminta's §5) using the request/response contract fixed here. **Client cache policy:** windows are cacheable client-side keyed on `(body_id, center, n)` — D-227's determinism guarantee (same seed/body/position → same derived output, always) means a previously-fetched window is valid forever for that body+seed and can be kept in an LRU without a freshness check, exactly the same guarantee that makes the whole invention pipeline (D-227's "invented deterministically" clause, the actual subject T-1124 surfaces to a screen for the first time) safe to memoize; eviction policy (size, LRU depth) is a client implementation detail, not fixed here. + - **(4) Budget (binding numbers).** Per-cell wire cost is **7 bytes** (`morphology` 1 + `elev_q` 1 + `temp_dc` 2 + `moisture_q` 1 + `vegetation` 1 + `glaciation` 1), before MessagePack array-header overhead (negligible at these sizes — six flat byte/i16 arrays, no per-element framing). **`DISTRICT_WINDOW_MAX_N = 64`** (request-side hard clamp, §1): 64×64 = 4,096 cells → 28 KiB raw payload. This is deliberately the same `n` as `aliveness_probe --render`'s default window (T-1123) — a value already proven to render correctly server-side and matching the "regional inspection" altitude the ticket names (64 districts × 2.048 km ≈ 131 km per side — city-and-hinterland scale, not planetary). **`DISTRICT_WINDOW_DEFAULT_N = 32`** (client's interactive default, 1,024 cells → 7 KiB) — half the cap. The derive cost is **background-queue latency, not tick-thread cost** (per §1's serving model, the window derives on a Rayon worker and returns via a later tick's completion drain, never inline): at the ~7 µs/derive debug-build rate T-1123's window renderer measured, a full window is ≈ 7 ms (n=32) / ≈ 29 ms (n=64 cap) of **Rayon-worker time**, i.e. the enqueue-to-completion delay a client waits across (a tick or few, bridged by §5's border-fade), *not* time spent on the PreInput drain. **These are debug-build figures with no committed release-build number yet** — release is expected meaningfully faster (no debug assertions, inlining) but the design does not presume a specific multiplier. The cap's job in the background-queue model is to bound how long one window job occupies a worker (so it can't starve whole-body cascade jobs sharing the pool) and to keep the client-visible wait short; if profiling shows it should move, `DISTRICT_WINDOW_MAX_N` is the one constant to tune, not a redesign. Because the derive is off the tick thread, D-200's 5 ms on-demand tile-fill budget is irrelevant here (that governs the main-tick chunk-boundary path); the window is served over the D-226 item (1) paused-sim bridge path (Snapshot/PostSnapshot stay alive while Movement/Simulation/Economy/etc. freeze) exactly as whole-body layers are, and its responsiveness budget is the same background-queue-plus-poll latency those already accept, not a per-tick deadline. **Re-request-on-pan policy:** the client re-requests only when a **pan** carries the view past the held window's edge — **never on zoom** (§5): only pan changes `center`, and because window derivation is deterministic (D-227) a zoom-triggered re-fetch of the same `(center, n)` would spam byte-identical responses for zero new information. The client debounces pan motion (do not fire on every drag-frame delta); the exact debounce interval and whether windows snap to a fixed grid vs. float on the pan center are client-side screen decisions (Araminta's §5) using the request/response contract fixed here. **Client cache policy:** windows are cacheable client-side keyed on `(body_id, center, n)` — D-227's determinism guarantee (same seed/body/position → same derived output, always) means a previously-fetched window is valid forever for that body+seed and can be kept in an LRU without a freshness check, exactly the same guarantee that makes the whole invention pipeline (D-227's "invented deterministically" clause, the actual subject T-1124 surfaces to a screen for the first time) safe to memoize; eviction policy (size, LRU depth) is a client implementation detail, not fixed here. - - **(5) Screen (Araminta).** The regional view is a **zoom-threshold LOD swap on the existing `AtlasViewer`, not a new screen**: a second, higher zoom threshold (`DISTRICT_WINDOW_MIN_ZOOM`, proposed `6.0`, past the existing `SETTLEMENT_LABEL_MIN_ZOOM = 2.0`) crossed while the pan center is over/near a settlement swaps the draw target in place — same `AtlasViewer` node, same `RegionalScreen` nav-stack "regional" state `show_body()` already establishes, no nav-stack push, no second `Control` scene, no new crumb. Both thresholds live on the existing `_view_zoom` float. Crossing back out (zoom below the threshold, or Esc — the two exits are one code path, a zoom-value transition watcher) swaps back. This reuses the viewer's own established LOD vocabulary (zoom past a threshold reveals more detail — labels at 2.0, the district window here) and its "reveal more without leaving" instinct (city-click opens a sidebar, not a nav push). **Rejected:** a nav-stack push (the planetary→regional push earns its crumb because the rendering genuinely changes — galaxy scatter → heightmap texture; body→window is the same viewer at a smaller camera window, so a crumb would imply "you left somewhere" for a metric reached by scrolling in); click-to-open on the settlement dot (overloads the existing city-click sidebar gesture — two intents on one gesture, and D-013 argues the zoom gesture should own spatial descent); a dedicated "view district" button (duplicates what pan/zoom already promises at every other level of this map). + - **(5) Screen (Araminta).** The regional view is a **zoom-threshold LOD swap on the existing `AtlasViewer`, not a new screen**: crossing a second, higher zoom threshold (`DISTRICT_WINDOW_MIN_ZOOM`, proposed `6.0`, past the existing `SETTLEMENT_LABEL_MIN_ZOOM = 2.0`) swaps the draw target in place — same `AtlasViewer` node, same `RegionalScreen` nav-stack "regional" state `show_body()` already establishes, no nav-stack push, no second `Control` scene, no new crumb. **The swap trigger is the zoom threshold alone** — there is no "must be over a settlement" precondition; the district window derives for *any* `DistrictPos` (`derive_district` is defined everywhere, ocean included), so the player can descend anywhere they can pan to. **The first window centers on the pan-center's derived `DistrictPos`** — the `DistrictPos` nearest the current screen-center point at the moment the threshold is crossed (`true_district_of_pixel`-style inverse mapping), *consistent with §4's float-on-center pan model*, **not** snapped to the nearest settlement. (Settlements matter only as the *practical* reason a player zooms in on a spot — the map's markers draw the eye — but nothing in the mechanism keys on them; a player zooming into open coastline gets that coastline's window, correctly.) Both thresholds live on the existing `_view_zoom` float. Crossing back out (zoom below the threshold, or Esc — the two exits are one code path, a zoom-value transition watcher) swaps back. This reuses the viewer's own established LOD vocabulary (zoom past a threshold reveals more detail — labels at 2.0, the district window here) and its "reveal more without leaving" instinct (city-click opens a sidebar, not a nav push). **Rejected:** a nav-stack push (the planetary→regional push earns its crumb because the rendering genuinely changes — galaxy scatter → heightmap texture; body→window is the same viewer at a smaller camera window, so a crumb would imply "you left somewhere" for a metric reached by scrolling in); click-to-open on the settlement dot (overloads the existing city-click sidebar gesture — two intents on one gesture, and D-013 argues the zoom gesture should own spatial descent); a dedicated "view district" button (duplicates what pan/zoom already promises at every other level of this map). - **Pan re-fetches; zoom does not.** A **pan** past the held window's edge re-centers `window_center` and fires a new request (§1's optional fields, same request machinery); **zoom never re-fetches** — with `DISTRICT_WINDOW_MAX_N = 64` the composite is a texture the client zooms client-side (the existing `_view_zoom` mechanic, now on the smaller composite) to get from a coarse read (~14 px/cell at n=64, 1.0×) to a detail read (~28 px/cell at 2×, comparable to the T-1123 `w256` probe renders' native fine texture) **from already-held data**. In-window zoom doing real legibility work is what separates "fetch = ground coverage" from "render zoom = detail resolution"; a zoom-triggered re-fetch would spam byte-identical (D-227) responses for the exact `(center, n)` already on screen. This is the §4 re-request policy's client-side rationale. - - **Debounce + window origin (the two client-side calls §4 left open).** Re-fetch fires **150 ms after the last drag-release** (not per-drag-frame) — long enough to collapse a flick-and-resettle into one request, short enough that a deliberate single pan-and-stop never feels delayed (no competing tick-driven redraw under the D-226 pause). Windows **float on the pan center** (nearest `DistrictPos` to the new screen-center), **not grid-snapped** — snapping would jump the composite by up to half a window-width across a snap boundary (a visually discontinuous "invisible re-fetch"), and floating keeps the anchor settlement exactly centered on entry every time; the §4 client cache still gets real hit value because Esc-then-re-enter and pan-back reproduce the same `(body_id, center, n)` (D-227 makes exact-repeat the common case for the two navigation patterns that matter), without a grid forcing arbitrary alignment. + - **Debounce + window origin (the two client-side calls §4 left open).** Re-fetch fires **150 ms after the last drag-release** (not per-drag-frame) — long enough to collapse a flick-and-resettle into one request, short enough that a deliberate single pan-and-stop never feels delayed (no competing tick-driven redraw under the D-226 pause). Windows **float on the pan center** (nearest `DistrictPos` to the new screen-center), **not grid-snapped** — snapping would jump the composite by up to half a window-width across a snap boundary (a visually discontinuous "invisible re-fetch"), and floating keeps the spot the player is looking at exactly under the screen-center on entry and after every pan (consistent with the first-window centering above — the descent point stays put, whether it's a settlement or open coast); the §4 client cache still gets real hit value because Esc-then-re-enter and pan-back reproduce the same `(body_id, center, n)` (D-227 makes exact-repeat the common case for the two navigation patterns that matter), without a grid forcing arbitrary alignment. - **What renders during the wait** (the "invisible re-fetch"): the previous composite, panned to its new screen position, with a **border-fade to the underlying whole-body heightmap** (already resident, coarser `district_grid`/`region_grid` data — real data seen through, not a placeholder) at the newly-exposed edge; **no black, no spinner** unless the wait exceeds a ~0.5 s grace window (reusing the existing `_gen_pending_indicator`, not a new mechanism). Because §4's cache is D-227-valid indefinitely, a pan back toward a recently-cached window composites from cache with zero wait — the genuine-miss `Pending`/re-poll path (D-225's existing loop) becomes the minority case, not the default. - - **Overlay/legend reuse against §2–§4.** The base layer is **morphology, lightness-modulated by `elev_q`** (one `0.7 + 0.3*(elev_q/100)` multiply per cell — relief read without a second draw call, the T-1112 "shape=identity, cheap second channel=magnitude" instinct as hue=type / lightness=elevation), reusing the T-1123 probe's 17-entry `MORPHOLOGY_RGB` hues verbatim; it is always-on once the LOD threshold is crossed (it *is* this screen's `terrain` layer), so it takes no toggle id. Three switchable overlays get new `gen_dw_temp` / `gen_dw_moisture` / `gen_dw_veg` `OVERLAY_DEFS` ids (`group: "toggle"`, the `gen_l1_*` multi-toggle-over-one-base precedent): **temperature** reuses T-1118's region-grid ramp *exactly* (same `i16` deci-°C domain + `REGION_TEMP_NONE_DC` sentinel disposition — one colorizer across both zoom levels, §2's consistency ruling); **moisture** reuses the existing `SUB_BIOME_COLORS` dry-sand→wet-teal endpoints; **vegetation** is a green-family ramp with **`Marine = 6` rendered transparent** (lets the morphology water-blue show through — `Marine` is `derive_vegetation`'s bookkeeping answer for already-`OpenOcean`/`Lake` districts, not new player information; a second blue would fight or duplicate the morphology read — this is the exhaustive disposition §3 mandates). **Glaciation is a modifier, not a toggle**: an ice-tint wash gated on `glaciation_grade >= Light` (alpha scaling with grade) composited over whichever layer shows — the `aliveness_probe::apply_ice_tint` approach ported to the player composite; it keeps sea-ice (tint over `OpenOcean` navy → whitened blue) visually distinct from open ocean and from ice-capped land (tint over alpine grey → near-white) by alpha-compositing over different bases rather than three drifting hard-coded colors. Legend: one `GENERATION_LEGEND` entry per new id (existing data-driven `atlas_legend_panel.gd` table, no new panel class); the morphology base folds its 17 zones into ~5 family rows (water / coastal-transition / plains-river / upland / volcanic) with the full mapping in the city-click sidebar, mirroring T-1112's "not everything earns permanent screen space" discipline. Implant chrome discipline (D-169/D-170): `ImplantHeader` carries the anchor name + extent-in-real-units subtitle (e.g. "4.1 × 4.1 km · 2.0 km/cell") + one optional flavor line; the map-data palettes stay **out of** the theme's semantic accent roles (especially `ACCENT_ACTIVE` gold, which the settlement marker owns and must not compete with); no scanline/glitch dressing (the implant is confident working tech — a signal-quality state, if ever needed, rides `_gen_pending_indicator`, not cosmetic noise). Full color/ramp/compositing/legibility rationale and the n=32↔n=64 on-screen-scale math live in Araminta's companion T-1124 sections (visual encoding / implant aesthetic / legibility constraints), not re-derived here. + - **Overlay/legend reuse against §2–§4.** The base layer is **morphology, lightness-modulated by `elev_q`** (one `0.7 + 0.3*(elev_q/100)` multiply per cell — relief read without a second draw call, the T-1112 "shape=identity, cheap second channel=magnitude" instinct as hue=type / lightness=elevation), reusing the T-1123 probe's 17-entry `MORPHOLOGY_RGB` hues verbatim; it is always-on once the LOD threshold is crossed (it *is* this screen's `terrain` layer), so it takes no toggle id. Three switchable overlays get new `gen_dw_temp` / `gen_dw_moisture` / `gen_dw_veg` `OVERLAY_DEFS` ids (`group: "toggle"`, the `gen_l1_*` multi-toggle-over-one-base precedent): **temperature** reuses T-1118's region-grid ramp *exactly* (same `i16` deci-°C domain + `REGION_TEMP_NONE_DC` sentinel disposition — one colorizer across both zoom levels, §2's consistency ruling); **moisture** reuses the existing `SUB_BIOME_COLORS` dry-sand→wet-teal endpoints; **vegetation** is a green-family ramp with **`Marine = 6` rendered transparent** (lets the morphology water-blue show through — `Marine` is `derive_vegetation`'s bookkeeping answer for already-`OpenOcean`/`Lake` districts, not new player information; a second blue would fight or duplicate the morphology read — this is the exhaustive disposition §3 mandates). **Glaciation is a modifier, not a toggle**: an ice-tint wash gated on `glaciation_grade >= Light` (alpha scaling with grade) composited over whichever layer shows — the `aliveness_probe::apply_ice_tint` approach ported to the player composite; it keeps sea-ice (tint over `OpenOcean` navy → whitened blue) visually distinct from open ocean and from ice-capped land (tint over alpine grey → near-white) by alpha-compositing over different bases rather than three drifting hard-coded colors. Legend: one `GENERATION_LEGEND` entry per new id (existing data-driven `atlas_legend_panel.gd` table, no new panel class); the morphology base folds its 17 zones into ~5 family rows (water / coastal-transition / plains-river / upland / volcanic) with the full mapping in the city-click sidebar, mirroring T-1112's "not everything earns permanent screen space" discipline. Implant chrome discipline (D-169/D-170): `ImplantHeader` carries a location label (the nearest settlement's name when the window is over/near one, else a coordinate/region label — the window is not settlement-anchored, per the entry clause above) + extent-in-real-units subtitle (e.g. "4.1 × 4.1 km · 2.0 km/cell") + one optional flavor line; the map-data palettes stay **out of** the theme's semantic accent roles (especially `ACCENT_ACTIVE` gold, which the settlement marker owns and must not compete with); no scanline/glitch dressing (the implant is confident working tech — a signal-quality state, if ever needed, rides `_gen_pending_indicator`, not cosmetic noise). Full color/ramp/compositing/legibility rationale and the n=32↔n=64 on-screen-scale math live in Araminta's companion T-1124 sections (visual encoding / implant aesthetic / legibility constraints), not re-derived here. - **Rationale:** Reusing the real UI — rather than a parallel offline renderer or dumped files — means the debug/review surface never diverges from what ships, and a dropped artifact can't go stale. Agent-navigability converts qualitative "does the synthesis look natural?" review from a manual eyeball pass into an automatable sweep that flags the few outliers for a human. The harness rides seams that already exist (`TickRate::Paused`, the paused-allowlist, `gameplay_occluded`, the bridge framing, the `run-visual` capture primitive) — a naming-and-contract exercise, not a new subsystem. - **New surface:** server pause-gating (run-conditions on the world phases keyed to a pause command); client `AtlasAgentInterface` (`observe`/`act`, Control-tree walker) + its local transport; the generation overlay rendering + selector + legend; interactive capture wired to `run-visual`. diff --git a/server/src/atlas/layer_proxy.rs b/server/src/atlas/layer_proxy.rs index cab1fc57e..399a95a7e 100644 --- a/server/src/atlas/layer_proxy.rs +++ b/server/src/atlas/layer_proxy.rs @@ -75,12 +75,27 @@ pub struct DistrictGridLayer { /// T-1119), or a non-ready status. /// /// Growth ceiling (governance-bounded): the one-`Option`-field-per-layer -/// pattern tops out at six fields — D-226's 2026-07-13 amendment (d) rules -/// out any L5/tile Atlas layer ever, and `quarter_footprints` below is the -/// last candidate the 2026-07-16 T-1112 amendment named. **The budget is now -/// consumed** — a seventh field is not a naming exercise like the six before -/// it; what (if anything) carries a future generation-layer addition is a -/// T-1124 design-pass question, not something to resolve here. +/// pattern tops out at six fields for the **dense whole-body layer family** +/// (`district_grid`, `road_graph`, `settlements`, `region_grid`, +/// `quarter_footprints` — each a compute-once, cache-per-body snapshot) — +/// D-226's 2026-07-13 amendment (d) rules out any L5/tile Atlas layer ever, +/// and `quarter_footprints` below is the last candidate the 2026-07-16 T-1112 +/// amendment named. **That budget is now consumed:** a seventh *whole-body* +/// field is not a naming exercise like the six before it — a future +/// generation-layer addition needs its own governance, not a drive-by field. +/// +/// The D-226 T-1124 amendment (2026-07-18) RESOLVED what carries the next +/// addition, and it is NOT this family: a **windowed viewport query** is a +/// categorically different payload (keyed on the *request* `(body, center, n)`, +/// re-fetched per pan, not a per-body snapshot). T-1124 specifies a +/// `district_window: Option` field (wiring is a follow-up +/// ticket, T-1137 — not yet added here) that rides on `AtlasLayerResponse` but +/// is explicitly OUTSIDE the whole-body family and does not count against this +/// six-field ceiling (D-226 T-1124 §2). The windowed family has its own hard +/// cap: exactly ONE windowed-query field; a second windowed query (a second +/// viewport, a windowed chunk-preview) is a dedicated response message by rule, +/// not a second `Option` here (D-226 T-1124 §2, symmetric with the +/// request-side five-shape demux ceiling in `bridge/mod.rs`). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AtlasLayerResponse { pub body_id: String,