# Architecture Decisions Technical foundation decisions that constrain implementation: engine, client-server, ECS, simulation, testability, performance budgets, audio architecture. --- ### D-008: Action pillar design principles - **Date:** 2026-02-08 - **Decision:** The action/combat system follows these principles: - **Rimworld/XCOM hybrid** - simple mechanics, stats-driven. Simple gets complicated fast on its own. - **Z-levels** - floors, verticality. Not 3D rendering, but 3D space. Fights across floors of a building. - **Perception-bounded** - you see/hear what your character can. Rest is abstracted/occluded. Same principle as information asymmetry applied to physical space. - **LOD/occlusion** - simulation reduces outside player view. Both a design principle and performance optimization. - **Large, varied maps** - locations should feel big. Achieved through procedural generation (templates + procedural flesh), trickery, or both. Procedural layouts also feed replayability. - **Multiple maps** - different locations are separate maps. Step through a wormhole, load a new place. - **Wildly asymmetric encounters** - balanced fights are the exception. Power mismatches in both directions are the norm and the source of tension. - **Hubris wall as design principle** - let the player feel powerful, then recontextualize their power level. Not cheap difficulty spikes - genuine "you were playing a smaller game than you thought" moments. The books are not always balanced; the fun comes when a character hits their hubris wall. - **Death = information loss, not game over** - memory cell backup system means death costs you everything since your last backup. Lost knowledge, lost relationships, lost deals. The storyteller knows what you've lost and can exploit it. - **Scales with ascension** - baseline human fights with weapons, Higher fights with biononics, ANA-connected fights with something else entirely. The pillar transforms as the character evolves. - **Design references:** Rimworld (real-time, simple systems, personal stakes), XCOM (tactical asymmetry, pod activation moments), Dwarf Fortress (z-levels, simulation depth) - **Raised by:** Team Leader (Jeroen), with input from full team. ### D-009: Multiplayer - design for it, build single-player first - **Date:** 2026-02-08 - **Decision:** Single-player is the build target. Multiplayer is designed into the architecture from day one so it can be added without rewriting the game. - **Rationale:** Two players experiencing the same conspiracy from different keyholes (Senate insider + Guardian operative) is a killer feature. But building multiplayer too early kills projects. The compromise: architectural decisions now that make multiplayer a "add networking" problem later, not a "rewrite everything" problem. Team Leader flagged that bolting multiplayer on after the fact is one of the hardest things to do - so the architecture must be honest about this from the start. - **Cost:** ~15-20% slower single-player development due to required abstractions. Accepted as cheap insurance. - **Raised by:** Full team discussion. Tyre led technical framing, Team Leader insisted on architectural honesty. ### D-010: Multiplayer-ready architectural baseline - **Date:** 2026-02-08 - **Decision:** Four non-negotiable architectural principles that must be present from the first line of code: 1. **Client-server separation** - even in single-player. The game simulation runs as a "server," the player view is a "client." Single-player = local client + local server. This is the single decision that makes or breaks retrofitting multiplayer. 2. **Information boundaries as a first-class system** - every piece of game state is tagged with who knows it. Not fog-of-war bolted on - the engine fundamentally thinks in terms of "what does this observer have access to." Required for single-player asymmetric information anyway. Multiplayer just means multiple observers. 3. **No baking player identity into the game loop** - the simulation doesn't know there's "the player." It knows there are characters, some of which are player-controlled. Adding a second player-controlled character should be a configuration change, not a rewrite. 4. **Deterministic simulation with input events** - game state advances based on timestamped actions, not "whatever the local machine calculated." Enables synchronization later without rewriting the simulation. - **Side benefits (Nigel's observation):** Every one of these makes single-player better too. Information boundaries make NPC AI smarter about what they know. Client-server makes save/load cleaner. Deterministic simulation makes debugging easier. No sacrifice. - **Engine implication:** Client-server friendliness is now a hard requirement on the engine shortlist (see Q-001). - **Raised by:** Tyre (Technical Architect), endorsed by Team Leader as "sound architectural baseline." ### D-012: Chunk-based map architecture for future borderless generation - **Date:** 2026-02-09 - **Decision:** Maps use chunk-based generation and loading from day one. Chunks load/unload around the player. A bounded map is "only generate chunks within this boundary." Removing the boundary later to enable Minecraft-style borderless generation is a configuration change, not a rewrite. - **v0.1:** Bounded ~150x150 per world, 2-3 z-levels, chunk-based internally. - **Future:** Borderless generation. The world generates as you explore. The map can never be "solved" by walking to every corner. New areas develop, existing areas change. - **Rationale:** Same principle as D-010 (multiplayer architecture) - design for the future, build the simpler version now. - **Raised by:** Tyre (Technical Architect), endorsed by Team Leader. - **Amendment (2026-04-05):** The ~150×150 visual tile estimate is superseded by [D-094](#d-094-district-spatial-hierarchy--chunk-block-district-naming-and-sizes) (256×256 visual tiles per district, 4×4 blocks). D-094 explicitly amends this decision. - **Amendment (2026-07-24):** "The world generates as you explore" is now concretely the seed-to-tile cascade ([D-227](#d-227), [D-239](#d-239)): Phase-5 in-world streaming derives the chunk neighborhood around the player (minimum the 3×3 chunks needed to draw the scene; prefetch radius a tuning constant) via the **same derivation the Atlas ladder samples** ([D-255](#d-255)). Coarser-layer context is self-provided by function composition (D-255 (f), the cache-accelerated pure function) — **Atlas interaction is never a precondition**; a player arriving anywhere without ever opening the atlas gets the byte-identical world, and the step-canvas serving work's cache-hit==cache-miss determinism gate protects exactly this property. The Phase-4 epic (T-750) carries the matching deliverable note; the insert minimap (Q-093) is noted as a same-derivation consumer, design deferred to Phase 5. ### D-020: Engine and architecture selection — Godot client + Rust simulation via subprocess/IPC - **Date:** 2026-02-09 - **Decision:** The game uses a split architecture: **Godot 4** (GDScript) as the rendering client, **Rust** with **bevy_ecs standalone** as the simulation server. The two communicate via **subprocess/IPC** (local socket for single-player, TCP for multiplayer). **NOT via GDExtension.** - **Architecture:** - The Rust simulation is a standalone binary with zero Godot dependencies. It runs the ECS world, perception queries, AI, storyteller, combat — all game logic. - The Godot client is a pure renderer: receives `ObserverSnapshot` data, draws tiles/sprites/fog, plays audio, shows UI, captures input. No game logic in GDScript. - Single-player: Godot launches the Rust binary as a child process. Local Unix socket or localhost TCP. - Multiplayer: Godot connects to a remote Rust server. Same protocol. The simulation binary doesn't know the difference. - This IS the D-010 client-server architecture — literally, not simulated. - **Serialization:** - **MessagePack** for all client-facing communication (Rust↔Godot). Dynamic structure supports variable HUD composition driven by perception modes ([D-017](perception.md#d-017-perception-modes-as-character-build-system)). Cross-language, debuggable. - **bincode** reserved for future Rust↔Rust server-to-server sync (same binary, hot path, zero overhead). - **protobuf** rejected — solves deployment/versioning problems we don't have, poor GDScript support. - **Why subprocess over GDExtension:** - Eliminates entire risk categories: gdext pre-1.0 API churn, Godot version ABI breakage, FFI thread safety (`Gd` is `!Send`), cross-boundary memory management. - Decouples learning: build and test Rust simulation standalone, build Godot renderer standalone, connect when both work. - Maps directly to D-010 client-server with no simulation — it IS client-server from day one. - Either side can be upgraded, replaced, or scaled independently. - Cost: ~1-5ms serialization latency per tick. Acceptable for a detective/strategy game, not a twitch shooter. - **Key patterns:** - `ObserverSnapshot`: the only data structure crossing the boundary. Contains visible entities, fog state, sound events, monologue triggers, HUD widget data. Variable shape per character build. - `PlayerInput`: semantic actions (MoveNorth, Interact, UsePerceptionMode), not raw key events. Timestamped for deterministic processing. - `SimBridge` trait: abstracts transport. `LocalBridge` (subprocess, channels) and `NetworkBridge` (TCP, MessagePack) implement the same interface. - **Protocol versioning policy:** `PROTOCOL_VERSION` gates **wire format compatibility** — field names, types, message structure. Bump when deserialization would fail (added/removed/renamed fields, new enum variants, changed types). Do NOT bump for gameplay parameter changes that affect what data flows through the same format (vision cone angles, NPC behavior, map layout, balance tuning). Per-character variation (D-015 cone config, D-017 perception modes) means these parameters differ between entities on the same server simultaneously — they are game state, not protocol. The client renders whatever `ObserverSnapshot` the server sends; it has no awareness of cone angles or perception mode configuration. - **Kill switch:** If no working prototype (character + fog + one NPC) exists by week 8 of development, pivot to pure Godot. If bridge/sync code exceeds game logic for 3 consecutive sprints, the architecture tax is too high. - **Development sequence:** 1. Build Rust simulation as standalone binary (testable via terminal/logs) 2. Build Godot renderer as standalone project (hardcoded test data) 3. Connect via MessagePack protocol - **Evaluation reports:** `docs/architecture/eval-godot-rust-bridge.md` (Tyre), `docs/architecture/risk-godot-rust-bridge.md` (Troblum) - **Raised by:** Team Leader (Jeroen) proposed Godot client + Rust backend. Tyre designed architecture. Troblum's risk assessment shifted integration from GDExtension to subprocess/IPC. Full team endorsed. - **Dissent:** None. Troblum's CRITICAL risk flags on GDExtension were accepted; subprocess approach addresses them. ### D-026: Simulation tiers with timestamp-based eviction - **Date:** 2026-02-10 - **Decision:** Four simulation tiers: Active (30-80 NPCs, full sim at 10-20 ticks/sec), Background (500-2,000 NPCs, state machine ticks 1/game-minute with 4 machines: schedule, mood, relationships, job), State-saved (10,000+, frozen serialized structs ~1-2KB each), Ungenerated (doesn't exist yet). Eviction uses interaction-timestamp LRU against available sim-space. NPCs with active scope tags (neighborhood, active-quest, colleague, known-contact) stay fully simulated. State-saved NPCs reactivate on player return (~2-5ms). Density follows the player — content generated ahead of arrival, home system fully instantiated at game start. - **Rationale:** Timestamp-based eviction replaces categorical persistence rules with one priority queue. State-save makes disposal reversible. bevy_ecs dynamic component add/remove makes tier transitions seamless. Tyre confirmed all proposals fit within performance budgets. - **Cross-reference:** Content density implications in [D-029](content.md#d-029-population-entanglement-ratio--305020). - **Raised by:** Team Leader (timestamp model), Tyre (technical validation), Gestalt (scope tags) - **Dissent:** None ### D-030: Testability architecture — 8 decisions for ticket T-214 - **Date:** 2026-02-11 - **Decision:** The v0.1 testability architecture is confirmed with 8 sub-decisions from the Gap Analysis Workshop (Round 18): 1. **Rust test organization = Hybrid.** `#[cfg(test)]` for unit tests inside modules + `tests/` directory for integration tests. Both via `cargo nextest run`. 2. **Godot test framework = gdUnit4** (changed from GUT). Native JSON output, stable headless via `GdUnitCmdTool`, `GdUnitSceneRunner` for scene lifecycle tests, organizational maintenance. 3. **IPC testing = Three-layer architecture.** Layer 1: fixture-based serialization roundtrip (fast, every edit). Layer 2: mock subprocess protocol state machine (medium, every PR). Layer 3: real subprocess integration (slow, daily/pre-merge). 4. **Production code constraints + CauseChain.** No `#[cfg(test)]` in production. Public API is the test surface. ECS World setup replaces mock injection. CauseChain is a production component (monologue provenance, journal, debugging) that tests also leverage. 5. **Test runner tooling.** `cargo-nextest` (Rust) + gdUnit4 (Godot) + bash wrapper scripts in `test/` directory, whitelistable for agent use. 6. **Test output format = JSON summary.** Consistent schema across all runners (suite, total, passed, failed, failures array). JUnit XML as secondary CI format. 7. **T-201 (Deterministic replay) promoted to CRITICAL.** Simulation must consume time, randomness, and input exclusively through injectable resources (`SimulationTime`, `SimRng`, `InputQueue`). Required by D-010 principle 4. 8. **Test priority aligned with hard blockers.** Phase 1 (sprint 1-2): test infra + collision/pathfinding/time. Phase 2 (sprint 3-4): monologue pipeline integration test + information boundary negative tests. Phase 3 (sprint 5+): CauseChain verification + divergent snapshots. - **Rationale:** Two rounds of analysis by Tyre (Technical Architect) and Hoshe (QA Engineer) with cross-validation from all design agents. Key change: gdUnit4 over GUT driven by agent-driven development requirements (JSON output, headless stability, bus factor). CauseChain endorsed unanimously after all design agents independently identified the need for information provenance tracking. - **Raised by:** Tyre (architecture), Hoshe (testability analysis). Full workshop endorsed. - **Dissent:** GUT vs gdUnit4 resolved in Hoshe's favor — Tyre explicitly changed position. No remaining dissent. ### D-031: Time system — game clock and day phases - **Date:** 2026-02-11 - **Decision:** The v0.1 time system uses the following model: - **Tick-to-time mapping:** 10 simulation ticks = 1 game-minute (at 10 tps, 1 real second = 1 game-minute). A 30-minute real-time play session covers ~12-18 game-hours — enough for a full NPC daily cycle. - **Day phases:** Four phases drive routine transitions: Morning, Afternoon, Evening, Night. NPCs transition between routine activities at phase boundaries (e.g., go to work in Morning, to the bar in Evening). - **Time display:** Diegetic — shown on the player's neural insert HUD. The character checks their insert to see the time, consistent with [D-013](scope.md#d-013-diegetic-insertpoi-navigation-system). - **Pause:** Available in single-player. Simulation freezes, UI stays responsive. Compatible with future multiplayer (D-009) where pause would be disabled or vote-based. - **Time-skip:** Deferred for v0.1. The "wait/stake out" mechanic (if implemented) would advance time while the player observes from a fixed position. - **Not in scope for v0.1:** Deep time (years/decades), day/night lighting, seasonal cycles, time zones between locations. - **Resolves:** Q-009 - **Raised by:** Tyre (technical proposal), Gestalt (day-phase design). Confirmed in Round 18 Gap Analysis Workshop with full team consensus. - **Dissent:** None. ### D-041: Knowledge Graph Data Model - **Date:** 2026-02-11 - **Decision:** The knowledge graph is a per-entity bevy_ecs Component with BTreeMap storage for deterministic iteration. Each entity that has knowledge (player character, Active-tier NPCs, Background-tier NPCs) gets a `KnowledgeGraph` component containing: (1) entity knowledge map: `BTreeMap`, (2) fact knowledge map: `BTreeMap`. Knowledge confidence uses a 4-level hierarchy: `Suspects < KnowsOf < KnowsDetails < Direct`. Knowledge state tracks temporal/logical status: `Active` (believed true), `Contradicted` (conflicting information exists), `Stale` (aged beyond threshold). Knowledge source provides provenance per entry: `DirectObservation`, `Heard`, `ToldBy`, `Inferred`, `Background`. Stable entity IDs (`StableId(u64)`) replace bevy_ecs Entity handles in knowledge references, mapped via `EntityRegistry` resource for bidirectional `StableId <-> Entity` lookup. Knowledge updates flow through event-driven architecture: perception systems emit `KnowledgeEvent` to `KnowledgeEventQueue` resource, knowledge update system drains queue and writes to `KnowledgeGraph` components. Basic decay runs once per game-minute (every 10 ticks per D-031), downgrading confidence levels based on `last_observed_tick` age against configurable `DecayThresholds`. - **Sprint 2 scope:** Full data structures + direct observation flow + basic decay + observer snapshot integration (T-112). Deferred to Sprint 3+: NPC-to-NPC gossip, `ToldBy`/`Inferred` source generation, `Contradicted` state detection, `Stale` state logic, knowledge-driven dialogue filtering, monologue triggering, misinformation. - **Canonical reference:** Full Rust struct definitions at `docs/workshops/knowledge-graph-information-boundaries/round2-synthesis.md` Part 3 (lines 320-752). All implementation must conform to those types. - **Key design choices:** - **BTreeMap over HashMap:** D-010 principle 4 (deterministic simulation) requires stable iteration order. BTreeMap provides O(log N) lookups (~6-8 comparisons at N=50-200), deterministic serialization, and predictable replay behavior. HashMap iteration is non-deterministic and incompatible with deterministic replay (D-030 sub-decision T-7). - **Per-entity Component, not centralized Resource:** Enables `Changed` dirty tracking, per-entity serialization for D-026 tier transitions, no shared mutable state, natural ECS query patterns. - **4-level confidence hierarchy:** Resolves Q-016. `Suspects` = "something's off", gates initial investigation. `KnowsOf` = "X is involved in Y", gates topic-specific dialogue and peer-tier access (D-028). `KnowsDetails` = actionable detail, gates confrontation and secret-tier dialogue. `Direct` = currently in LOS, provides live position data and maximum rendering fidelity. Maps to D-028 access tiers: `surface` available at any level, `real` at KnowsOf+, `secret` at KnowsDetails+. - **KnowledgeState for contradiction detection:** THE FRIEND arc (D-034, D-039 wow moment T-3) requires detecting when a `ToldBy` entry conflicts with a `DirectObservation` entry. Both entries receive `Contradicted` state, triggering monologue event and relationship state shift (PersonOfInterest). Sprint 2 only uses `Active` state; contradiction detection ships Sprint 3. - **StableId for knowledge references:** Partially resolves Q-019 for server-side and knowledge graph purposes. Knowledge graphs reference `StableId(u64)` that persists across save/load cycles, not bevy_ecs `Entity` (generational index). `EntityRegistry` maintains bidirectional mapping. Assigned once at entity creation, never changes. Client-side mapping (Godot StableId -> scene node) remains open. - **Event-driven updates:** Phase 2 (perception) emits events. Phase 3 (knowledge) consumes events and writes graphs. Phase 4 (snapshot) reads graphs. Prevents mutable borrow conflicts in bevy_ecs. - **Performance budget:** ~14 KB per NPC knowledge graph (50 entities + 20 facts). Active tier (80 NPCs) = ~1.1 MB. Background tier (2,000 NPCs, 10 entries each) = ~5 MB. Total live memory: ~6 MB. Knowledge lookups are O(log N) at N=50 (~100ns per query). Not on critical path (shadowcasting/spatial queries consume 10-20ms per tick, knowledge operations <3ms). - **Resolves:** Q-016 (knowledge hierarchy). Partially resolves Q-019 (entity ID stability, server-side). - **Blocks:** T-352 (Observer Snapshot Pipeline Workshop) - **Raised by:** Tyre (architecture synthesis), Dudley (implementation analysis), Gestalt (mechanics validation), Paula (narrative requirements). Workshop participants: Tyre, Dudley, Gestalt, Paula, SI. Source: Knowledge Graph & Information Boundaries Workshop (Epic T-351), 2026-02-11. - **Dissent:** None. Gestalt initially proposed dual-axis model (confidence + understanding) but validated that single-axis confidence with future content-driven understanding progression is architecturally sufficient. Dudley proposed HashMap; synthesis chose BTreeMap per D-010 principle 4 with Dudley's explicit acknowledgment ("iteration order is a determinism time bomb"). ### D-042: UI microcopy format — YAML via GDScript autoload - **Date:** 2026-02-13 - **Decision:** UI strings (interaction prompt labels, knowledge panel labels, relationship state descriptors, HUD labels, tutorial text) are stored in YAML format at `client/data/ui-strings.yaml` and loaded via a dedicated GDScript autoload singleton (`UIStrings`). UI strings are NOT hardcoded as GDScript constants in `client/scripts/constants/ui_strings.gd`. - **Rationale:** YAML format enables editing UI text without rebuilding the client and supports future localization infrastructure (all player-facing text in one format). UI microcopy is **client-side rendering data** per D-020 (Godot is the renderer) — distinct from server-side game content (dialogue/monologue lines). UI labels are presentation metadata that never cross the protocol boundary, so they live in the client repository and load via a client-side autoload rather than the content loader system. Hardcoded constants would require client recompilation for copy edits. - **Related ticket:** T-409 (UI microcopy) - **Raised by:** Team decision in Sprint 5 planning - **Dissent:** None ### D-054: Tile-based movement with same-tile occupancy - **Date:** 2026-02-13 - **Decision:** All movement is tile-based (server-authoritative, discrete positions). Client-side Tween interpolation (100-150ms) hides the grid visually. Same-tile occupancy via TilePresence component (Standing/Prone/Seated/Fixture layers) allows multiple entities on one tile in different postures. Mouse facing is a client-side float; the server receives the facing octant only. Tile occupancy provides trivial collision detection. - **Rationale:** Determinism ([D-010](#d-010-multiplayer-ready-architectural-baseline) principle 4). Tile-based enables shadowcasting ([D-238](perception.md#d-238-symmetric-shadowcasting-albert-ford-selected-for-los-computation)), pathfinding, chunk-based maps ([D-012](#d-012-chunk-based-map-architecture-for-future-borderless-generation)), and trivial collision. Occupancy system adds positioning depth (doorway blocking, eavesdrop positioning, sitting at furniture) within tile-based constraints. ~150 lines server-side. - **Implementation:** TilePresence enum: Standing, Prone, Seated, Fixture. Multiple entities can share a tile if they occupy different posture layers. - **Cross-reference:** Stance system ([D-053](scope.md#d-053-movement-as-stance-toggle-system)), shadowcasting ([D-238](perception.md#d-238-symmetric-shadowcasting-albert-ford-selected-for-los-computation)) - **Source:** Control & Interaction Workshop (2026-02-13) - **Raised by:** Tyre (tile-based, non-negotiable), Dudley (tiles-per-tick model), Nigel (converted in Round 2: "tiles are BETTER for replayability — discrete positions = finite meaningful choices") - **Dissent:** Nigel initially proposed free movement with tile-based collision (Round 1). Converted in Round 2 after demonstrating that tile-based spatial puzzles (doorway decisions, corner peeks, eavesdrop corridors) create replayability. - **Amendment (2026-07-06, T-1088):** the "client-side Tween interpolation (100–150 ms)" sentence is **scoped to the 2D renderer** henceforth. A fixed-duration tween conflicts with the D-053 stance cadences (200–800 ms/step) — dash-then-stand stutter at every stance below Sprint. The 3D presentation layer interpolates per [D-248](#d-248-3d-locomotion-presentation--per-leg-constant-velocity-interpolation-keyed-to-the-stance-throttle) (per-leg constant velocity keyed to the stance throttle). Everything else in this record (server-authoritative discrete tiles, octant-only facing on the wire, TilePresence occupancy) is unchanged and remains binding for both renderers. ### D-055: Sprint explicitly suppresses interaction buffer - **Date:** 2026-02-13 - **Decision:** When in Sprint stance ([D-053](scope.md#d-053-movement-as-stance-toggle-system)), the server explicitly clears the interaction buffer. No interaction verbs are computed or sent to the client during sprint. Anomaly monologue survives sprint — the "sprint double-take" (if the character passes something anomalous while sprinting, a delayed monologue fires retroactively: "Wait — was that Kael? At this hour?"). - **Rationale:** Mouse gymnastics to click during sprint = bad UX. Explicit suppression is cleaner and deterministic. The sprint double-take preserves the feel that the character is still aware even when the player can't interact — sprint suppresses interpretation (monologue at 40%), not sensory data (overlays still render). - **Cross-reference:** Stance system ([D-053](scope.md#d-053-movement-as-stance-toggle-system)), monologue ([D-016](perception.md#d-016-internal-monologue-as-core-perceptionatmosphere-system)) - **Source:** Control & Interaction Workshop (2026-02-13) - **Raised by:** Dudley (explicit suppression), Ozzie (anomaly survival / double-take), Gestalt (interpretation vs data framing) - **Dissent:** Gestalt argued physics handles it naturally (player passes through interaction radius too fast to click). Lead ruled explicit suppression for clarity and determinism. ### D-066: Dual-scale grid — 0.5m simulation, 1m visual (2x retina factor) - **Date:** 2026-02-14 - **Decision:** The game uses two coordinate scales with a fixed 2x retina factor: - **Simulation grid: 0.5m tiles.** All movement, LOS/shadowcasting ([D-238](perception.md#d-238-symmetric-shadowcasting-albert-ford-selected-for-los-computation)), pathfinding, occupancy ([D-054](#d-054-tile-based-movement-with-same-tile-occupancy)), and interaction range operate at 0.5m per sim tile. The server knows only sim tiles. - **Visual grid: 1m tiles.** The Godot client renders floor art, wall art, and structural tiles as 2x2 blocks of sim tiles. Art is authored at 1m conceptual scale. - **World geometry: 2x2 sim tile minimum.** All walls, furniture, crates, doors, and environmental objects occupy a minimum of 2x2 sim tiles (= 1 visual tile). This ensures visual truth and sim truth agree on where solid things are — cover, LOS occlusion, and collision map 1:1 with what the player sees. - **Entities: 1x1 sim tiles.** Characters and small items occupy individual 0.5m sim tiles, giving sub-visual-tile positioning precision. Entities naturally take corners/edges within a visual tile's 1m space. - **Sprites: 2x2 sim tile footprint.** Entity sprites render across 2x2 sim tiles so they feel proportional to the 1m visual grid. Tween interpolation ([D-054](#d-054-tile-based-movement-with-same-tile-occupancy)) hides half-visual-tile movement increments. - **Mental model:** "Objects are where they look. I can position myself precisely within open space." The player reads cover and walls at visual scale (always correct). Fine movement granularity is felt, not counted. - **What the simulation does NOT know:** Visual tiles. The retina factor is purely a client rendering convention. The server operates exclusively on 0.5m sim tiles. - **Amendment (2026-05-22, [D-222](#d-222)):** canonical vocabulary. This 0.5m "sim tile" is now the **Subtile** — the granularity the server simulation, entity positioning, and render detail run on. The **Tile = 1m** (2×2 subtiles) is the unit sizes are quoted in and the one the world-generation cascade, pathfinding, and grids operate on. The dual-scale model and the 0.5m simulation granularity are unchanged; only the naming primacy flips. The Chunk/Block/Quarter/District ladder built on the Tile is canonical in D-222. - **Fog shader ([D-059](perception.md#d-059-fog--shader-based-five-layers-knowledge-graph-driven)):** Unaffected — fog is screen-space, driven by PointLight2D vision cone and LOS mask from sim-resolution shadowcasting. Gradient edge "3-4 tiles" is retuned to 6-8 sim tiles (= 3-4 visual tiles) to preserve the intended softness. - **Cursor/interaction:** No change — cursor already resolves to sim tile from pixel position. Interaction range of ~2 sim tiles = 1m (arm's length). - **Map authoring:** Author at 1m visual scale. Subdivision tool expands each visual tile to 4 sim tiles (2x2). Validation enforces 2x2 minimum on all world geometry layers. - **Amends:** OQ-01 resolution (ticket T-444). Sim tile size remains 0.5m; visual presentation changes from 1:1 to 2:1 retina factor. - **Amendment (2026-06-17):** The dual-scale coordinate model (0.5 m Subtile / 1 m Tile) and the 2×2-sim-tile **geometry minimum** are unchanged and load-bearing for the 3D architecture — cover/LOS/collision still map 1:1 to what the player sees. The "**Sprites: 2×2 sim tile footprint**" bullet and the "sprites look right" rationale are pre-Sprint-28 sprite-era: in-world entities/objects are now rendered as 3D ([D-149](#d-149-3d-characters-rendered-live-in-scene--not-pre-rendered-sprites) / [D-244](#d-244-asset-rendering--3d-objects-in-world-2d-limited-to-textures--flat-artwork)), so there is no pixel sprite footprint — the 2×2 figure persists only as the visual-tile proportionality the 3D entity occupies. The spatial math survives; only the rendering technology changed. - **Cross-reference:** Tile-based movement ([D-054](#d-054-tile-based-movement-with-same-tile-occupancy)), shadowcasting ([D-238](perception.md#d-238-symmetric-shadowcasting-albert-ford-selected-for-los-computation)), fog ([D-059](perception.md#d-059-fog--shader-based-five-layers-knowledge-graph-driven)), art direction ([D-043](perception.md#d-043-art-direction--visual-style-functional-warmth)), z-stack ([D-049](perception.md#d-049-z-level-rendering-stack-8-layers)), stance system ([D-053](scope.md#d-053-movement-as-stance-toggle-system)) - **Rationale:** 0.5m sim tiles give stealth-grade granularity for movement stances, cover peeking, and interaction range. 1m visual tiles make spaces feel proportional, sprites look right, and world geometry readable. The 2x2 minimum on geometry eliminates visual/sim mismatch for cover and LOS — the only sub-visual-tile positioning is entity movement, which is communicated through fog feedback, not tile counting. Analogous to macOS Retina: logical resolution (visual) differs from physical resolution (sim), but the system is coherent because both agree on where solid objects are. - **Raised by:** Team Leader (Jeroen) — proposed retina scaling analogy and 2x2 geometry constraint. Tyre (feasibility: trivial, half-day integration). Gestalt (approved with 2x2 constraint resolving LOS readability concern). Ozzie (approved: solves sprite scale without uncanny mismatch). - **Dissent:** Gestalt initially objected to dual-scale (mental model mismatch for cover/LOS). Resolved by the 2x2 geometry minimum constraint — all cover maps 1:1 at visual scale. ### D-068: 5-bus audio architecture - **Date:** 2026-02-16 - **Decision:** Audio uses a 5-bus architecture with player-facing volume sliders: 1. **Music** — future/empty in v0.1. Reserved for diegetic Meridian music in social spaces. 2. **Ambient** — station hum ([D-038](scope.md#d-038-audio-in-v01-scope--8-files-via-stable-audio-open) asset 1) + zone overlays (assets 2-4). Continuous soundscape. 3. **World SFX** — NPC footsteps, doors, environmental events. Diegetic world sounds not caused by player. 4. **Player Actions** — player footsteps (assets 5-6), future: combat sounds, item interactions. Sounds player directly causes. 5. **UI Sounds** — cursor hover, implant open, monologue chimes (assets 7-8), fog recognition. Interface feedback. - **Client implementation:** - AudioManager GDScript autoload singleton on client branch (lives with rendering, per [D-020](#d-020-engine-and-architecture-selection--godot-client--rust-simulation-via-subprocessipc)). - Audio assets committed to audio branch (content, not code). - Directory-scan registry pattern: AudioManager scans `res://audio/` on startup, maps filenames to AudioStream resources. No hardcoded asset list. - If directory empty or asset missing, all play methods no-op with visual fallback (per [D-038](scope.md#d-038-audio-in-v01-scope--8-files-via-stable-audio-open) architecture). - 5 player-facing volume sliders, one per bus. Accessible via settings. - **Bus routing dropped:** - **Dialogue bus removed** — no voice acting in v0.1. NPC conversation murmur goes on World SFX (event-driven, per [D-072](perception.md#d-072-universal-event-driven-conversation-murmur)). - **Rationale:** 5 buses provide player control granularity (disable UI sounds, boost World SFX for eavesdropping, mute Ambient for focus) without over-segmentation. Directory-scan registry eliminates hardcoded asset paths — audio branch can add files without touching client code. No-op fallback means client works identically with or without audio. - **Cross-reference:** Audio assets ([D-038](scope.md#d-038-audio-in-v01-scope--8-files-via-stable-audio-open)), audio dip ([D-069](perception.md#d-069-audio-dip-profiles-for-dialogue-and-confrontation)), client-server architecture ([D-020](#d-020-engine-and-architecture-selection--godot-client--rust-simulation-via-subprocessipc)) - **Raised by:** Team Leader (channel split directive), Tyre (architecture), Inigo and Gestalt (bus refinement) - **Dissent:** None ### D-073: Zone crossfade approach — hard boundary, soft audio transition - **Date:** 2026-02-16 - **Decision:** Zone audio transitions use hard tile boundary triggers with 1.5-2s audio crossfade tweens. Server sends zone_id per tile in ObserverSnapshot (server-authoritative zone assignment). AudioManager receives zone changes and tweens between ambient layers. No blended overlap zones — the transition smoothness comes from audio fade duration, not spatial blending. - **Implementation:** AudioManager stub in Sprint 7 (5-bus setup, directory registry). Full zone crossfade implementation deferred to Sprint 8+. - **Rationale:** Hard boundaries with soft audio = predictable for simulation, pleasant for player. Avoids complex overlap zone geometry. Crossfade duration (1.5-2s) is long enough to feel smooth, short enough that walking back-and-forth across boundary doesn't create audio chaos. - **Cross-reference:** Audio architecture ([D-068](#d-068-5-bus-audio-architecture)), client-server ([D-020](#d-020-engine-and-architecture-selection--godot-client--rust-simulation-via-subprocessipc)), ambient assets ([D-038](scope.md#d-038-audio-in-v01-scope--8-files-via-stable-audio-open)) - **Raised by:** Tyre - **Dissent:** None --- ### D-085: Per-game save directory structure - **Date:** 2026-02-25 - **Decision:** Every new game creates a dedicated directory under the user save path. All saves for that game (manual, quicksave, autosave) live inside the game's directory. Directory name includes a human-readable game identifier and creation timestamp. - **Rationale:** Natively groups saves by game without requiring a database or index file. Players can browse, back up, or delete game saves at the filesystem level. Avoids a flat save folder where 50+ files from different games are interleaved. - **Structure:** `user://saves//` where `` is `-` (e.g., `20260225-143022-a7b3f1/`). Inside: `quicksave.sav`, `autosave.sav`, `manual_001.sav`, etc. - **Constraints:** - Game directory created on "New Game" — even before the first save, so the path exists for quicksave/autosave. - F5 = quicksave (overwrites `quicksave.sav` in the active game dir). - F6 = quickload (loads `quicksave.sav` from the active game dir). - Loading screen lists game directories sorted by last-modified, shows most recent save per game. - **Raised by:** Team Leader (Jeroen) - **Dissent:** None ### D-088: 3-state pause system — Normal/Overlay/Paused, server-authoritative - **Date:** 2026-02-12 - **Decision:** Simulation runs at three speed states: Normal (100% tick rate), Overlay (50% — active during knowledge panel, dialogue, map view), Paused (0% — full pause via Esc). Server is authoritative: client sends pause requests, server sets `sim_speed` field in ObserverSnapshot. Client reads `sim_speed` and adjusts presentation. No client-side tick manipulation. - **Rationale:** Server-authoritative speed states preserve D-010 principle 4 (deterministic simulation). Client cannot modify simulation state directly. Overlay mode at 50% ensures UI interactions do not require a hard pause while still giving the player time to read and decide. - **Raised by:** Tyre, Dudley - **Dissent:** None - **Source:** v0.1 Content Scoping Workshop, closing round resolution - **Cross-reference:** D-031 (time system), D-020 (client-server architecture) --- ### D-094: District Spatial Hierarchy — Chunk, Block, District Naming and Sizes - **Date:** 2026-02-25 - **Decision:** The spatial hierarchy for map generation and streaming is defined as follows. **Chunk** = 64×64 sim tiles (32×32 visual tiles, 32m) — the streaming and serialization unit. **Block** = 128×128 sim tiles (64×64 visual tiles, 64m) — the generator planning unit, composed of 4 chunks arranged in a 2×2 grid. Each block contains 4 chunks; chunks within a block can merge into one large edifice, remain separate (small buildings, gardens, cafes), or form L-shaped buildings across chunk boundaries. **District** = 4×4 blocks = 512×512 sim tiles (256×256 visual tiles, 256m) per z-level, containing 16 blocks and 64 chunks. Large civic structures (gate terminals, horizon station installations, stadiums, parks) span multiple blocks. Three z-levels for the Transit District = ~1.35MB (trivial). This decision amends D-012 and overrides the ~150×150 visual estimate in D-014. - **Amendment (2026-05-22, [D-222](#d-222)):** naming + scale superseded. The unit this record calls a **District** (4×4 blocks) is renamed the **Quarter** (now 512m at Tile = 1m); **District** is promoted to a new 2048m tier (4×4 quarters). Tier *counts* and nesting structure here are unchanged — only the names and the metre values (per the Tile = 1m / Subtile = 0.5m scale of D-220). See D-222 for the canonical ladder and the lore-vs-code rule. - **Rationale:** Chunk size of 32×32 visual (64×64 sim) gives a 32m streaming cell — large enough to hold a meaningful space, small enough for efficient streaming. The 2×2-chunk block provides a generator planning unit with enough granularity for per-chunk variation. The 4×4 block district (256×256 visual) gives a full district footprint generalisable as a template for the Q-036 generator. The chunk-based fill system within blocks allows the generator to place buildings of varying scale without hard-coding building dimensions. - **Raised by:** Tyre (chunk/block spec and memory confirmation), confirmed by team. Lead ratified district = 4×4 blocks. - **Dissent:** Araminta preferred 32×32 visual chunk size (effectively halving the chunk to a 16m cell). Overruled by lead and team majority — 32m chunk is the minimum viable streaming cell for the simulation architecture. - **Source:** Station District Layout Workshop, Ticket T-153, Sprint 20. Round document: `docs/discussions/round-20-station-district-layout.md` - **Cross-reference:** D-012 (tile spec — amended), D-014 (v0.1 map spec — district bounding box superseded), D-066 (dual-scale grid), D-093 (Sova Transit District layout using this hierarchy), Q-036 (district generator) --- ### D-096: DistrictLayoutMode — Grid and Organic Support - **Date:** 2026-02-27 - **Decision:** Two layout modes coexist for district generation. `Grid`: Commission-planned districts with rectilinear block placement. `Organic`: pioneer/growth districts with block offsets (±16 sim tiles per axis), rotation (0–3 steps, 15° increments), variable street width (0.75–2.0×). Hard technical ceiling: maximum rotation ±45°. Beyond 45°, tile-based pathfinding produces unacceptable movement artifacts. Organic districts produce curved-street impressions through angular jogs and irregular setbacks. Grid vs. Organic proportions must vary per seed to prevent predictable meta-level patterns. - **Rationale:** Grid = power imposed (Commission-planned). Organic = power negotiated (pioneer settlements, organic growth). Both modes encode political and settlement history in spatial form. - **Source:** Generator Architecture Workshop (T-562), 2026-02-27. Full spec: `docs/workshops/generator-architecture/workshop-outcomes.md` §D-READY-1. - **Raised by:** Tyre (technical architecture), Miri (cultural grammar). Full team sign-off. - **Dissent:** None. - **Cross-reference:** D-094 (spatial hierarchy), Q-036 (district generator) ### D-097: Guarantee Tier System — Universal / Full-Only / Conditional - **Date:** 2026-02-27 - **Decision:** The district generator runs a `GuaranteeAuditResult` with three tiers of spatial guarantees. **Tier 1 — Universal (all inhabited):** Social Hub, Informal Zone, Encounter Corridor. **Tier 2 — Full-complexity:** Traffic Chokepoint, Institutional Space, Insider Space, Economic Node, Horizon View Corridor (coastal), BreachOnly Zone (≥1), Rooftop Discovery Zone (tall structures). **Tier 3 — Conditional:** A-1 Elevated Vantage, A-2 Egress Multiplicity, A-3 Temporal Opacity Window, A-4 Non-Institutional Route, Economic Asymmetry Signal, Power Gradient Visibility. A Full-complexity coastal urban hub gets up to 13 checks. Archetype placement must vary in angular position (not just distance) across seeds — audit fails if archetypes cluster predictably across a test batch of N seeds. - **Rationale:** The generator makes contracts it keeps. Guaranteed affordances ensure every playstyle has spatial affordances in any district, without hand-crafting each location. - **Source:** Generator Architecture Workshop (T-562), 2026-02-27. Full spec: `docs/workshops/generator-architecture/workshop-outcomes.md` §D-READY-2. - **Raised by:** Gestalt (tier structure + assassin lens integration), Tyre (GuaranteeAuditResult struct). Full team sign-off. - **Dissent:** None. - **Cross-reference:** D-103 (assassin lens guarantees A-1 through A-4), D-102 (horizon view corridor — Tier 2 coastal) - **Amended 2026-05-25 (D-229/D-231 — guarantee-audit engine hook):** The audit runs AFTER step-3 fill completes for a district, before the skeleton is marked "complete"; on a failed mandatory check, fill regenerates with adjusted parameters. **The mandatory-check set is keyed on `ComplexityTier`, not `WorldTier`** (T1 = all inhabited; T2 and T3/A-1..A-4 = Full only). Signature: `run_guarantee_audit(skeleton: &DistrictSkeleton, building_tags: &[BuildingPropertyTag], street_graph: &StreetGraph, seed: SeedChain) -> GuaranteeAuditResult`. Tier-1 checks (all inhabited): `social_hub_present` (≥1 building with entry_class ∈ {Public, Commercial} AND zone ∈ entertainment/market/transit); `informal_zone_present` (≥1 Organic + Public, or open ground in an Organic district); `encounter_corridor` (street graph has a continuous Public traversal path crossing the district). Tier-2 checks (full-complexity): `traffic_chokepoint` (a bottleneck node whose removal disconnects the graph); `institutional_space` (admin/judicial/checkpoint, Restricted); `insider_space` (Restricted + Hidden or non-primary-street door); `economic_node` (≥1 Commercial); `breach_only_zone` (≥1 BreachOnly). Tier-3 / A-1..A-4 (pure graph/geometry over step-3 tags + street graph — NO interior generation): A-1 elevated vantage (max above_ground ≥3 with unobstructed LOS to a chokepoint); A-2 egress multiplicity (≥2 distinct unique-street connections across the perimeter); A-3 temporal opacity (≥1 TemporalWindow door → reduced traffic in closed hours); A-4 non-institutional route (a Public→insider-space path exists that avoids all institutional buildings). `GuaranteeAuditResult` gains a bool per check above, retaining its existing `rooftop_discovery_zone` (D-106) and `horizon_view_corridor` (D-102) fields. Raised by: Gestalt, economic-built-world workshop round 2. ### D-099: WallBackside / TileBehindState — Dual Classification - **Date:** 2026-02-27 - **Decision:** Two complementary enums classify tiles behind wall surfaces. `WallBackside` (structural): what is physically there — `AdjacentSpace | StructuralFill | ServiceVoid | ChunkBoundary | Exterior`. `TileBehindState` (gameplay): what kind of space this represents — `StructuralFill | HiddenRoom | Interstitial`. Mapping: `ServiceVoid → Interstitial`; `AdjacentSpace → HiddenRoom or StructuralFill` depending on access tier. Era-tagged infrastructure cavity contents with standardized color codes: Era 1 power conduit only (`#c8b840`), Era 2 power + water/coolant (`#4888c8`) + comm lines (`#b8b8b8`), Era 3 full bundle. Backside assignments within a template must have seed-driven variation — not fixed template values. - **Rationale:** "Every wall is a secret keeper." No tile is ever void. Dual classification separates structural truth (what's there physically) from gameplay meaning (what does this imply for the player's investigation). - **Source:** Generator Architecture Workshop (T-562), 2026-02-27. `docs/workshops/generator-architecture/workshop-outcomes.md` §D-READY-4. - **Raised by:** Tyre (WallBackside), Gestalt (TileBehindState). Full team sign-off. - **Dissent:** None. ### D-100: Dynamic Modification via Overlay — DamageOverlay and RegenerationStrategy - **Date:** 2026-02-27 - **Decision:** Generator output is immutable after Phase 1. All post-generation modifications are applied via overlay, not re-generation. `DamageOverlay` struct: `overlay_type` (GasExplosion | Fire | Structural { collapse_direction } | Flooding), `epicenter: ChunkLocalPos`, `radius: f32`, `intensity: f32`, `scatter_seed: u64` (variation within zone only). `RegenerationStrategy` enum: `LocalOverlay(DamageParameters)` for in-playthrough events (MANDATORY), `SoftReseed { seed_modifier: u64 }` at scenario boundaries only, `FullReseed` at era-level discontinuities only. Trauma event → visual stage mapping: PhysicalDestruction/ViolenceEvent → Stage 2 (Fresh Aftermath), decays to Stage 3; EconomicDisruption/PoliticalShock/MigrationShock → quarter fill modifier. Full stage sequence: Stage 1 Active → Stage 2 Fresh Aftermath → Stage 3 Stabilized → Stage 4 Reconstruction → Stage 5 Healed Scar. Destruction palette is corruption-only: no new colors introduced by destruction. Single exception: `#c8d8f0` open-sky tile appears when a roofed structure has its roof removed. See D-109 for the XOR prohibition as architectural mandate. - **Rationale:** Modification history diverges per playthrough on the same seed. Same world, different event histories, different delta layers — this is the replayability engine. Causal legibility requires the player to be able to read what happened from the world state. - **Source:** Generator Architecture Workshop (T-562), 2026-02-27. `docs/workshops/generator-architecture/workshop-outcomes.md` §D-READY-5. - **Raised by:** Tyre (structs), Gestalt (LocalOverlay mandate). Destruction stages and palette constraint: Araminta (Round 5). - **Dissent:** None. - **Cross-reference:** D-109 (XOR prohibition as architectural mandate), D-107 (trauma events — cultural track) ### D-101: ZonePalette Modifier System - **Date:** 2026-02-27 - **Decision:** Zone palettes use `ZonePalette { base: BasePalette, modifiers: Vec }`. Eight canonical base terrain types: T1 temperate farmland (warm organic, natural lighting) / T2 industrial farmland (cool grey-green, artificial lighting) / T3 wilderness / T4 grassland / T5 coastal water (deep near-black blue, animated specular; referenced by D-102 horizon corridor guarantee) / T6 beach/coastal margin (warm dark tan) / T7 mountain/high terrain (dark blue-grey stone, snow at elevation) / T8 desert/arid. T1 and T2 are explicitly distinct farmland types. Additional terrain types must be specified with new numbers — not silent replacements for existing types. Modifier axes: A (heritage root → material character), B (economic tier → condition/density), C (era → material generation), plus faction overlay, climate, condition, season. Palette modifiers influence NPC appearance as well as environment (people dress like they're from here). - **Rationale:** A zone's visual identity must be legible at a glance. Palette modifiers create cultural visual identity without rewriting base terrain. - **Source:** Generator Architecture Workshop (T-562), 2026-02-27. `docs/workshops/generator-architecture/workshop-outcomes.md` §D-READY-6. - **Raised by:** Araminta (terrain types and color specs, canonical T5/T7 numbering corrected Round 5), Tyre (palette struct). Full team sign-off. - **Dissent:** None. - **Cross-reference:** D-102 (horizon view corridor — T5 coastal water is the referenced terrain type), D-104 (heritage grammar overlay — modifier axis A) - **Superseded (partial):** Modifier axis A (HeritageRoot) superseded by [D-167](content.md#d-167-heritage-roots-superseded--corridor-cultural-system-replaces-abstract-roots) (2026-03-24). Cultural palette modifiers are now authored per-system via the corridor framework; abstract heritage root IDs no longer drive axis A. - **Modifier axis A (`HeritageRoot`) superseded 2026-05-25 by D-232** → replaced by `FlavorTagFilter`; axes B/C and faction/climate/condition/season unchanged. ### D-102: Horizon View Corridor as Coastal Guarantee - **Date:** 2026-02-27 - **Decision:** A **negative-space** reservation for coastal districts: ≥8 visual tiles unobstructed view corridor from nearest public street to water's edge. No building, tree, or z=4 element may occupy this corridor. A low z=2 element (railing, bench, bollard) marks the waterfront point as a designed viewing location. Tier 2 Conditional guarantee — applies to Full-complexity coastal districts. Position within the district must vary per seed; the Wow Moment of seeing the horizon must be discovered, not expected. - **Rationale:** "Negative-space reservation" framing — the generator reserves space by prohibiting placement, not by placing something. The view of the horizon is a spatially guaranteed player experience. - **Source:** Generator Architecture Workshop (T-562), 2026-02-27. `docs/workshops/generator-architecture/workshop-outcomes.md` §D-READY-7. - **Raised by:** Araminta (visual grammar and negative-space framing), Tyre (implementation constraint). Full team sign-off. - **Dissent:** None. - **Cross-reference:** D-097 (guarantee tier system — Tier 2), D-101 (ZonePalette — T5 coastal water is the terrain type this guarantee references) ### D-103: Assassin Lens Spatial Guarantees — A-1 through A-4 - **Date:** 2026-02-27 - **Decision:** Four derived spatial properties validated by the guarantee audit for Full-complexity districts. These are **derived properties of existing spatial configuration**, not assassin-tagged features — they add no generation cost; the audit validates existing output. **A-1 Elevated Vantage** (Tier 3): ≥1 position with clear LOS cone to Traffic Chokepoint. **A-2 Egress Multiplicity** (Tier 3): ≥2 exit routes to adjacent districts. **A-3 Temporal Opacity Window** (Tier 3): ≥1 time window where Social Hub has reduced ambient NPC coverage. **A-4 Non-Institutional Route** (mandatory Full-complexity): ≥1 route to any Insider zone not passing through high-security institutional spaces. A-1/A-2/A-3 are Tier 3 Conditional (trigger on `complexity_tier == Full`). A-4 is mandatory for all Full-complexity districts regardless of playstyle. - **Rationale:** The investigator/assassin playstyle needs guaranteed affordances without the generator explicitly building for assassination. Derived properties keep generation cost zero while ensuring spatial conditions exist. - **Source:** Generator Architecture Workshop (T-562), 2026-02-27. `docs/workshops/generator-architecture/workshop-outcomes.md` §D-READY-8. - **Raised by:** Gestalt (assassin lens framing and derived-properties insight). Full team sign-off. - **Dissent:** None. - **Cross-reference:** D-097 (guarantee tier system — Tier 3) ### D-106: Vertical Scale Architecture and Rooftop Bar Clause - **Date:** 2026-02-27 - **Decision:** Four height tiers: S1 (1–2 z-levels, surface + roof/mezzanine), S2 (3–10), S3 (11–30), S4 (30+). Shadow length is the primary height signal in top-down view (2–40 visual tiles). Lazy z-level loading: `ZLevelLoadState: Loaded | Skeleton | Ungenerated` — only current + adjacent z-levels filled by Phase 2. **Rooftop Bar Clause:** Every tall structure (z_band_count ≥ 3) must assign `RooftopConfig: Restricted | PublicWithHiddenLayer`. Discovery layer mandatory in both configurations. Heritage root **weights the probability** between the two configs — it does not determine the outcome. Final config is seeded per-building; a minority of buildings of any heritage root may be the non-dominant type (a Frost building with a rooftop bar must be possible). Z-band floor boundaries must have seed-variation within cultural ordering constraints. Vertical access routes are playthrough-history dependent. - **Rationale:** Height has meaning — floor 30 has information floor 1 cannot have because it is harder to reach. Full determination of rooftop config by heritage root kills the discovery moment. - **Source:** Generator Architecture Workshop (T-562), 2026-02-27. `docs/workshops/generator-architecture/workshop-outcomes.md` §D-READY-11. - **Raised by:** Tyre (z-level architecture), Ozzie (Rooftop Bar Clause — discovery guarantee). Ozzie + Araminta corrected "determines" → "weights probability" in Round 5. - **Dissent:** None. - **Cross-reference:** D-094 (spatial hierarchy), D-097 (guarantee tier system — Rooftop Discovery Zone is Tier 2) ### D-108: MobileChunk Specification - **Date:** 2026-02-27 - **Decision:** Entity-carried interior space attached to a mobile world entity. Not a district — uses the same chunk fill primitives in a simpler flat structure (no Phase 1/Phase 2 split, no block grid, no zone negotiation). Key structs: `MobileChunk`, `MobileInterior`, `VesselClass`, `MobileMovementState` (Docked / InTransit / InterSystem / Idle), `TransitSocialModifier`, `MobileNpcSlot`, `NpcPersistence` (Crew | Passenger). `Idle` = vessel parked at a location but not docked to infrastructure (anchored ship, grounded shuttle). Vessels are **persistent world entities** — interior cache keyed by entity_id persists across voyages for crew state. `Docked` state requires `dock_position`, `connected_chunk: Option`, `docked_since: SimTick`, `scheduled_departure: Option`. `scheduled_departure` must be populated by the generator; vessels without departure schedules are an error state. Cultural grammar: `TransitSocialModifier` with `TransitVariant` (BoundedLinear | BoundedMobile | InterSystem). Vessel visual grammar (5 rules): (1) hull uses vessel-identity material, not zone palette; (2) windows reveal exterior context (docked vs. transit); (3) compression modifier tightens proportions; (4) section transitions use vessel-identity threshold elements; (5) class stratification via proportion, not palette. Replayability requirements R-V-1 through R-V-6 in `docs/workshops/generator-architecture/round-4-notes.md` §5. Memory: ~0.5–4KB metadata + up to 64KB ChunkData per vessel; paged by streaming model. - **Rationale:** "The journey is content — mobile environments are social pressure cookers, not loading screens with chairs." Vessel persistence and crew state continuity make the world feel real. - **Source:** Generator Architecture Workshop (T-562), 2026-02-27. `docs/workshops/generator-architecture/workshop-outcomes.md` §D-READY-13. - **Raised by:** Tyre (struct design), Miri (cultural grammar — `miri-round4.md`), Nigel (replayability requirements), Ozzie (player experience). Visual grammar: Araminta (`araminta-round4.md` §2). - **Dissent:** Nigel initially proposed instanced districts for vessels; lead ruled entity-carried MobileChunk for persistence. - **Note:** The `Idle` movement state is the canonical primitive for player-owned stationary installations (space stations, orbital platforms, parked vessels as permanent bases). A MobileChunk in `Idle` with no `scheduled_departure` is architecturally equivalent to a static chunk from the simulation's perspective — it participates in the same tile/zone system. This design prevents future over-engineering of a separate 'player installation' system. - **Cross-reference:** D-100 (DamageOverlay applies to vessel damage), D-109 (LocalOverlay mandate), D-111 (MobileChunk Idle state as stationary installation primitive), Q-046 (departure schedule — resolved by this D-record) ### D-109: DamageOverlay / RegenerationStrategy Prohibition — Architectural Mandate - **Date:** 2026-02-27 - **Decision:** XOR reseeding for in-playthrough events is **architecturally prohibited**. `LocalOverlay` is the mandatory modification strategy for all events that occur while the player is present. `SoftReseed` and `FullReseed` are permitted only at scenario-boundary and era-level discontinuities respectively — events the player was not present for, where causal legibility is not required. This prohibition is filed as a separate D-record from D-100 because it establishes the modification principle for the entire game, not just the overlay mechanics. - **Rationale:** Causal legibility: the player must be able to look at a damaged district and understand what happened. XOR reseeding destroys the causal thread. Unanimous consensus across all workshop participants — the strongest architectural agreement of the entire workshop. - **Source:** Generator Architecture Workshop (T-562), 2026-02-27. `docs/workshops/generator-architecture/workshop-outcomes.md` §D-READY-14. - **Raised by:** Gestalt (XOR prohibition framing), Tyre (RegenerationStrategy struct). Unanimous. - **Dissent:** None. - **Cross-reference:** D-100 (DamageOverlay + RegenerationStrategy full specification) ### D-110: Signed Z-Level Addressing — base_z u8 → i8 - **Date:** 2026-02-27 - **Decision:** All z-level base fields use signed integers (`i8`) instead of unsigned (`u8`). Specifically: `MultiBlockReservation.base_z: i8`, `FloorZone.z_level: i8`, and any struct that references a z-level position (not a count). Z-level *counts* (`z_levels: u8` on DistrictSkeleton) remain unsigned — they represent "how many floors", which is always positive. The distinction: `base_z` is "where does the bottom floor start" (can be negative for basements/subterranean spaces), `z_levels` is "how many floors total" (always ≥1). - **Rationale:** The Round 4 workshop designs (D-106, D-108) and the gestalt gas explosion example explicitly assume negative z-levels (basements at z=-1 to z=-3, utility sub-levels). The existing `u8` type contradicts the design intent. A deep mine is structurally an inverted skyscraper with `base_z: -30, z_levels: 30`. Lazy z-level loading (`ZLevelLoadState`) works identically for negative z — the loading system cares about relative offsets between adjacent levels, not the sign. - **Source:** Tyre architectural analysis, 2026-02-27. Confirmed existing Round 4 design intent (D-106 §height tiers, Gestalt Round 4 §gas explosion, Araminta Round 2 §infrastructure routing). - **Raised by:** Tyre (type-level fix), lead (edge case prompt: basements, deep mines). - **Dissent:** None. - **Cross-reference:** D-106 (vertical scale architecture), D-094 (spatial hierarchy), D-108 (MobileChunk) ### D-111: MobileChunk Idle State Covers Stationary Player Installations - **Date:** 2026-02-27 - **Decision:** The `MobileChunk` `Idle` movement state (D-108) is the canonical primitive for player-owned stationary installations — space stations, orbital platforms, parked vessels used as bases, or any persistent interior space the player controls that is not part of the district chunk grid. A stationary installation is a MobileChunk that does not move: it uses the same streaming, save/load, NPC simulation, and `LocalOverlay` modification system as vessels. Player construction within a MobileChunk (building rooms, placing equipment) requires the DLC construction system to emit valid `LocalOverlay` modifications — the architectural pattern is ready, the construction system is DLC scope. - **Rationale:** A separate "location instance" system for player bases would require a new coordinate system, new streaming/loading path, new save/load path, portal/transition logic, and duplicate pathfinding/perception/simulation — Tier 4 difficulty for zero benefit over existing primitives. The `Idle` state already exists in D-108; documenting its design intent for stationary installations costs nothing and prevents future over-engineering. - **Source:** Tyre architectural analysis, 2026-02-27. - **Raised by:** Tyre (architectural mapping), lead (edge case prompt: player bases, fortresses). - **Dissent:** None. - **Cross-reference:** D-108 (MobileChunk specification — `Idle` state), D-100/D-109 (LocalOverlay for modifications) ### D-112: No Separate Location Instancing System - **Date:** 2026-02-27 - **Decision:** The game does **not** use instanced locations (separate spatial domains outside the world chunk grid). All subterranean spaces, player bases, and special locations are represented using existing primitives: - **Basements / sub-levels:** `MultiBlockReservation` with negative `base_z` (D-110) - **Deep mines (shaft-style):** Downward `MultiBlockReservation`, lazy-loaded via `ZLevelLoadState` - **Deep mines (cave network):** Organic-mode district (D-096) with mine-specific template - **Player base in existing building:** `LocalOverlay` modifications (D-100/D-109) - **Player base as hidden bunker:** `MultiBlockReservation` with negative z, generated at world-gen - **Player-built space:** `LocalOverlay` + construction system (DLC scope) - **Stationary installation:** `MobileChunk` in `Idle` state (D-111) - **Vessel interior:** `MobileChunk` (D-108) - **Rationale:** Instanced locations would require: (1) new coordinate system for instanced space, (2) new streaming/loading path, (3) new save/load path, (4) portal/transition logic between world space and instanced space, (5) duplicate pathfinding, perception, and simulation tier logic. Every edge case maps cleanly to an existing primitive. Nigel's vessel instancing proposal was already ruled out in favor of entity-carried MobileChunk (D-108 dissent note); the same reasoning extends to all "separate space" cases. - **DLC/Mod implication:** `LocalOverlay` is the universal post-generation modification layer. DLC quest locations, mod-injected dungeons, and scenario-specific spaces can be delivered as overlay packages applied to existing districts under strict conditions — without touching the generator or reseeding. The generator output remains sacred (D-109); overlays are how the world changes after generation. This makes the overlay system the canonical content injection point for all post-generation content, whether player-driven, storyteller-driven, or DLC/mod-driven. - **Source:** Tyre architectural analysis, 2026-02-27. - **Raised by:** Tyre (comprehensive edge case mapping), lead (edge case prompt: deep mines, player bases, fortresses). - **Dissent:** None. - **Cross-reference:** D-110 (signed z-levels), D-111 (MobileChunk Idle for installations), D-108 (MobileChunk), D-096 (organic layout), D-100/D-109 (LocalOverlay) --- ### D-113: Tile data model — extensible per-tile properties - **Date:** 2026-03-05 - **Decision:** Replace the current single-character tile encoding (`F/W/V/R` strings in location YAML) with a **tile palette/registry** system (option A from the design space). Tiles are typed by a palette ID; per-type properties are defined once in the palette and inherited by all tiles of that type. Per-tile overrides are supported via a sparse overlay map. - **Current state:** Tiles are single characters in string arrays. Each character maps to a `TileKind` enum (`Floor`, `Wall`, `Door`, `Object`) and a walkability bool. `TileCell` in `WalkabilityMap` stores `{ walkable: bool, kind: TileKind }`. No per-tile properties (material, visual variant, sound, access lists, container contents, damage state, trigger zones) can be expressed. - **Design survey — what systems need tile-level data:** 1. **Doors** — access lists (who can open), open/closed state, locked/unlocked. Currently no tile-level door data; `TileKind::Door` exists but carries no properties. 2. **Containers** — contents, capacity, searched state. Currently handled by entity `ObjectType::Container` on separate entities, not tiles. Containers should remain entities, not tile properties. 3. **Damage state** — `DamageOverlay` (D-100) modifies tiles post-generation. Damage needs to degrade tile properties (walkability, visual, material) without replacing the base tile type. 4. **Visual variants** — same logical tile type (e.g., "industrial floor") with per-tile visual variation for visual richness. Currently impossible — all Floor tiles look identical to the client. 5. **Trigger zones** — tile-level triggers for entry/exit events (zone transitions, alarms, dialogue triggers). Currently handled by `ZoneMap` at zone granularity, not per-tile. 6. **Material properties** — footstep sound, movement speed modifier, surface type for particle effects. Currently all tiles produce the same footstep sound. 7. **WallBackside** (D-099) — structural classification behind wall surfaces. Already defined as an enum but not yet integrated into tile data. - **Chosen approach — Tile Palette + Sparse Override:** - **Tile palette** (YAML, per-district or global): defines tile types by string ID. Each type specifies: `walkable: bool`, `kind: TileKind`, `material: String` (footstep/SFX), `visual_base: String` (client sprite), `visual_variants: u8` (random variant count), `los_blocking: bool`, `movement_cost: f32` (default 1.0), optional `wall_backside: WallBackside` (D-099). The palette is the type-level contract — most tiles need no per-instance data beyond their palette ID. - **Tile map** (YAML): retains the string-array format for human readability, but each character is a palette key (single char or short code). Backward-compatible: `F`, `W`, `V`, `R` are reserved palette keys that map to current behavior. New tile types use additional characters or a separate palette layer. - **Sparse override map** (YAML): `overrides` key on Location — a list of `{ x, y, properties }` entries for tiles that differ from their palette type. Supports: door access lists, initial locked state, visual variant pinning, damage overlay data. Only tiles with non-default properties need entries. Keeps the string map clean for 90%+ of tiles. - **Runtime representation:** - `TilePalette` resource: `BTreeMap` loaded at startup. Immutable after load. - `TileCell` extended: `{ palette_id: char, walkable: bool, kind: TileKind, material_id: u16 }`. Material ID is a compact index into the palette's material table. - `TileOverrideMap` resource: `BTreeMap<(i32, i32, i32), TileOverride>` for per-tile overrides. Sparse — only tiles with overrides consume memory. - ECS queries: `WalkabilityMap` remains the primary interface for movement/pathfinding (unchanged API). `TilePalette` provides material/visual data when needed (snapshot construction, sound system). `TileOverrideMap` provides door state, access lists, damage overlays. - **YAML authoring format:** ```yaml # Palette definition (loaded once, reusable across locations) palette: F: { walkable: true, kind: Floor, material: metal-grate, visual_base: floor_industrial } W: { walkable: false, kind: Wall, material: bulkhead, visual_base: wall_heavy, los_blocking: true } D: { walkable: true, kind: Door, material: metal-door, visual_base: door_standard } G: { walkable: true, kind: Floor, material: glass-panel, visual_base: floor_glass } R: { walkable: false, kind: Floor, material: metal-grate, visual_base: floor_restricted } # Location tile map (unchanged human-readable format) tiles: - "WWWWWWWWWWWWWW" - "WFFFFDFFFFFFFW" - "WFFFFFFFFFFGFW" - "WWWWWWWWWWWWWW" # Per-tile overrides (sparse, only for non-default properties) overrides: - { x: 5, y: 1, door_access: [faction.commission], locked: true } - { x: 12, y: 2, visual_variant: 3 } ``` - **Loader contract:** `ContentPlugin` loads palette YAML first, then location tiles. The `apply_location_tiles()` function resolves each character via palette lookup instead of the current hardcoded match. Unknown characters fall back to `Floor` with a warning (same as current behavior). Overrides are loaded after tiles and applied to `TileOverrideMap`. - **Migration effort for existing locations (5 files):** - **Zero-migration path:** The default palette defines `F/W/V/R` with identical behavior to current hardcoded mapping. Existing location YAMLs work unchanged. No migration required for v0.1. - **Incremental enrichment:** Locations can opt into the new palette by adding a `palette:` key. Locations without `palette:` use the global default. Migration is per-location, at author pace. - **Estimated effort:** Palette definition = 0.5 day. Loader refactor = 1-2 days. Override system = 1 day. Total: 2-4 developer-days. No changes to location YAML files required for v0.1. - **Alternatives considered:** - **(b) Per-tile property bags** (arbitrary key-value per tile): Maximum flexibility but violates D-010 principle 4 (deterministic — dynamic typing makes serialization non-deterministic). Memory cost: ~100 bytes/tile vs ~6 bytes/tile with palette. Rejected. - **(c) ECS-style tile components** (tiles as entities): Each tile becomes a bevy_ecs entity with optional components. Elegant in theory but 150x150x3 = 67,500 entities per location, potentially 4M+ entities for a district. ECS entity overhead (~128 bytes each) makes this prohibitively expensive. Queries scale poorly at this count. Rejected for spatial data; tiles remain grid-based. Entities are reserved for interactive objects placed ON tiles. - **(d) Hybrid (palette + entity overlay):** Palette for base tiles, entities for interactive tile features (doors, containers, triggers). This is *almost* what we chose — the distinction is that our sparse override map is grid-indexed (O(1) lookup by position) rather than entity-query based. Interactive objects that have their own behavior (NPCs, containers, items) remain entities; tile properties that are spatial/static (material, visual variant, access) are grid data. - **Key design principles:** - Palette is the type; override is the instance. 90%+ of tiles need only a palette ID. - String-array tile maps remain human-readable and merge-friendly. No JSON, no complex nested structures. - `WalkabilityMap` API is unchanged — callers don't know about palettes. - BTreeMap for deterministic iteration per D-010 principle 4. - Palette keys are `char` (single Unicode codepoint) for direct mapping from tile string arrays. - **Raised by:** Tyre (architecture), requested by T-586 (Epic: extensible tile data model). - **Dissent:** None anticipated — this is a design-only D-record for post-v0.1 implementation. - **Cross-reference:** D-054 (tile-based movement), D-066 (dual-scale grid), D-094 (spatial hierarchy), D-099 (WallBackside classification), D-100 (DamageOverlay), D-012 (chunk architecture) --- ### D-133: Skills affect outcome — same verbs available, skill determines quality - **Date:** 2026-03-05 - **Decision:** The skills-to-verb coupling model is: everyone sees the same verbs (mostly). Skills determine how well you execute — bad at social means you can still talk, just badly. Some advanced verbs may still be gated by skill level, but the default is outcome-based, not access-based. This is the simplest learnable model: try anything, skill determines result. - **Rationale:** Verb access gating (skill gates whether you can even attempt an action) creates invisible walls and punishes players for trying. Outcome-based (skill determines quality of result) lets players learn by doing and creates organic differentiation. A tycoon with low social can still negotiate — they just negotiate poorly, which produces interesting consequences. - **Source:** Where's the Fun? Workshop, Round 4 Interview, Decision 5 - **Raised by:** Team Leader (Jeroen) — outcome model (option C) - **Dissent:** None - **Cross-reference:** [D-120](scope.md#d-120-no-skill-ceiling-in-v02--transhumanist-ladder-deferred-superseded) (no skill ceiling in v0.2) ### D-134: Full character customization — hair, clothing, colors at tile scale - **Date:** 2026-03-05 - **Decision:** Full character appearance customization is in scope: hair, clothing, colors. Readability at top-down tile scale is solved through outline and highlight mechanics, not by limiting customization options. The character creation screen is an emotional investment moment — the player should feel this is their character. - **Rationale:** Customization at this scale was assumed to be a readability risk. The workshop decision: solve the readability problem rather than limit the player. Readability via outline/highlight is a solved problem in the tile rendering pipeline. Limiting customization would undermine the identity investment that makes life-sim attachment possible. - **Source:** Where's the Fun? Workshop, Round 4 Interview, Decision 11 - **Raised by:** Team Leader (Jeroen) - **Dissent:** None ### D-135: Setting delivery via both layers — visual world + insert in parallel - **Date:** 2026-03-05 - **Decision:** Setting is delivered through two parallel layers: (1) the physical world — visuals and NPC behavior show context, atmosphere, place; (2) the neural insert — names, contextualizes, provides information the character would know from their background. Araminta (visual layer) and Mellanie (insert copy layer) work in parallel. Both layers are required from day one of the tycoon bookmark experience. - **Rationale:** Either layer alone is insufficient. Visuals without naming leave the player in a beautiful void with no cultural foothold. Naming without visuals produces an exposition dump. Both together produce the "this is a place" sensation the workshop identified as the missing ingredient of v0.1. - **Source:** Where's the Fun? Workshop, Round 4 Interview, Decision 12 - **Raised by:** Team Leader (Jeroen) — both layered (option C) - **Dissent:** None - **Cross-reference:** [D-128](content.md#d-128-culture-implicit-in-starting-location--van-maanens-star-system-equals-van-maanens-star-culture) (culture as context for insert copy) ### D-136: First Settled Reach moment — auto-generated apartment + insert activation - **Date:** 2026-03-05 - **Decision:** The first moment of The Settled Reach is two layered beats: (1) Waking up in YOUR auto-generated apartment (reflects your economic position from the tycoon bookmark; wealthy, modest, or constrained start matters). (2) Insert activation — the neural implant powering on is intimate, personal, tech-specific. The alarm clock is the Groundhog Day homage ([D-126](content.md#d-126-groundhog-day-alarm-clock-homage--first-game-day-only)). The apartment reflects the character's economic position — auto-generated, not hand-built. - **Rationale:** The apartment establishes place, economic status, and self without exposition. Insert activation establishes the neural lattice as intimate and personal — this is your character's relationship with their technology. Both beats together create the "this is MY character in MY world" moment that v0.1 lacked. - **Source:** Where's the Fun? Workshop, Round 4 Interview, Decision 13 - **Raised by:** Team Leader (Jeroen) - **Dissent:** None - **Cross-reference:** [D-126](content.md#d-126-groundhog-day-alarm-clock-homage--first-game-day-only) (alarm clock tone), [D-135](#d-135-setting-delivery-via-both-layers--visual-world--insert-in-parallel) (both layers active from first moment) ### D-137: Generator produces both structural and cosmetic variety at different scales - **Date:** 2026-03-05 - **Decision:** The generator must produce two types of variety simultaneously at different scales: (1) Structural variety — operates at seed level: different playthroughs have genuinely different world structures (economic landscape, faction power balance, crisis composition, NPC role distribution). (2) Cosmetic variety — operates within a structure: NPC names, faces, apartment layouts vary per instance. Structural variety is the higher-priority proof for the Sprint 25 spike ([D-119](scope.md#d-119-generator-spike-confirmed-for-sprint-25--critical-path)). - **Rationale:** Cosmetic variety without structural variety produces "same game with different wallpaper." Structural variety without cosmetic variety produces identical-looking characters with different internal states. Both are load-bearing for the life-sim experience — structural variety drives replay value, cosmetic variety drives in-session believability. - **Source:** Where's the Fun? Workshop, Round 5 Interview, Decision 23 - **Raised by:** Team Leader (Jeroen) - **Dissent:** None - **Cross-reference:** [D-114](scope.md#d-114-v02-proof-of-life--generator--graphics-not-hand-built-slice-superseded) (generator proof-of-life), [D-119](scope.md#d-119-generator-spike-confirmed-for-sprint-25--critical-path) (Sprint 25 generator spike) ### D-141: PlatformInfo — client-side OS abstraction autoload - **Date:** 2026-03-13 - **Decision:** All OS-dependent queries on the client are centralized in a single `PlatformInfo` autoload (`client/scripts/autoloads/platform_info.gd`), registered first in the autoload order. Individual systems (HardwareDetector, voice pipeline, settings UI) consume PlatformInfo properties and signals — they never call `OS.*` directly. PlatformInfo owns: power state (with `PowerProfile` enum: FULL, BATTERY, POWER_SAVER), memory queries, platform identity, and platform-dependent file paths. Power state is polled on a 30-second timer with a `power_profile_changed` signal; memory is refreshed on demand. The `PowerProfile` enum is the abstraction seam for future power-saver detection (GDExtension) without consumer code changes. PlatformInfo is **client-side only** — the client never relies on the server for hardware info, because the server may not be on the same hardware in multiplayer/remote hosting scenarios. Each side detects independently. - **Rationale:** OS calls were scattered across HardwareDetector, AiDialogueDetector (duplicate), and SimBridge. A central abstraction prevents duplication, provides a single seam for platform-specific behavior, and keeps the client self-sufficient per D-010 (information boundaries) and future multiplayer readiness. - **Raised by:** Team Leader (Jeroen) - **Dissent:** Centralized server detection was considered and rejected — server may not share hardware with client in future multiplayer scenarios. - **Cross-reference:** [D-138](content.md#d-138-llm-re-voicing-pipeline-for-npc-voice) (hardware detection for voice pipeline), [D-010](#d-010) (information boundaries), [Q-059](../questions/architecture.md#q-059-platforminfo-full-interface-scope) (full interface scope — open) --- ### D-148: 30° low-angle camera with 45° map rotation — supersedes D-019 - **Date:** 2026-03-17 - **Decision:** The default gameplay camera is an orthographic Camera3D at **30° tilt** (60° from horizontal) with the tile map rotated **45°** into a diamond grid. Not faked in art — an actual Camera3D tilt. Art direction reference: Hades, Divinity: Original Sin. Three camera angle presets: **frontal (-5°)** (default for character editor/mugshot, see D-158), **dramatic/low-angle (-30°)** (gameplay default), **overhead (-80°)** (near-top-down view; 0° in code convention is horizontal, not overhead). Camera tilt cycle (T key in prototype): top-down ↔ dramatic. 30° is the confirmed gameplay default. The 45° map rotation gives natural depth cues and the classic diamond-grid isometric layout. ~~45° isometric preset superseded — not part of the confirmed set.~~ - **Rationale:** The 30° angle (versus 45° or top-down) gives significantly more character front visibility and wall depth. Players can see faces, clothing, and character detail rather than primarily hat and shoulder. The diamond grid provides natural spatial depth cues without requiring Z-ordering hacks. The T-key prototype confirmed 30° as the most readable angle at gameplay scale. - **Architecture note:** Unlike D-019's amendment (which faked tilt in sprite art), this is a real Camera3D setting. The 45° map rotation is a Transform3D applied to the tile grid root — it does not affect simulation coordinates, which remain axis-aligned. Vision cone math and all server-side systems remain in unrotated space; the client applies the visual rotation. - **Raised by:** Team Leader (Jeroen) — confirmed during Sprint 28 character visuals spike review - **Dissent:** None - **Supersedes:** [D-019](perception.md#d-019-top-down-confirmed-as-primary-camera-3d-cutscenes-for-key-moments) - **Cross-reference:** [D-149](#d-149-3d-characters-rendered-live-in-scene--not-pre-rendered-sprites) (3D rendering), [D-151](#d-151-direction-count--8-server-side-facings-4-visual-groups-client-sprint-28) (direction count) - **Editorial note (2026-07-06, T-1088):** the opening "30° tilt (60° from horizontal)" contradicts this record's own preset list — **the preset list is authoritative**: pitch values are measured from horizontal (0° = horizontal), gameplay default **−30° from horizontal**, frontal −5°, overhead −80°. Implemented as such in the T-1088 sandbox camera. Also note "45° rotation" here means the **single static map rotation** (one Transform3D on the tile-grid root); stepped *player* camera rotation was never decided (parked under Q-084) and requires a new record before implementation. ### D-149: 3D characters rendered live in scene — not pre-rendered sprites - **Date:** 2026-03-17 - **Decision:** Characters are rendered as live 3D models in the Godot scene using a `CharacterCompositor` (Node3D). The camera is a real Camera3D at 30° tilt (D-148). Characters are not pre-rendered 2D sprite sheets. The 3D model is rotated to match the server-tracked 8-direction facing; the camera and lighting remain fixed. Clothing, hair, and accessories are separate mesh layers composited at runtime. - **Rationale:** The Sprint 28 spike used CSG placeholder characters (cylinders, spheres) and confirmed that even crude 3D shapes read as recognizable people at isometric scale — silhouette, proportion, and facing direction are all legible. Pre-rendered sprites would require 8× (or 4×) separate renders per outfit combination; live 3D compositing gives unlimited clothing/color combinations at negligible extra render cost. Direction changes are a model rotation, not a sprite swap. Future animation is natural. - **Architecture note:** The existing `EntityRenderer` (`client/scripts/rendering/entity_renderer.gd`) currently uses a single `Sprite2D` per entity. Under this decision, `EntityRenderer` is extended to instantiate a `CharacterCompositor` scene (Node3D subtree) instead. The compositor API is specified in `docs/design/compositor-api-spec.md`. - **Raised by:** Team Leader (Jeroen) — spike prototype confirmed; Sprint 28 workshop decision - **Dissent:** None - **Cross-reference:** [D-148](#d-148-30-low-angle-camera-with-45-map-rotation--supersedes-d-019), [D-150](#d-150-character-outline--inverted-hull-method), [D-151](#d-151-direction-count--8-server-side-facings-4-visual-groups-client-sprint-28), [D-152](#d-152-character-lod--performance-driven-budget-not-distance-threshold), ticket T-693 (compositor implementation) ### D-150: Character outline — inverted hull method - **Date:** 2026-03-17 - **Decision:** Character outlines are rendered via the **inverted hull method** (GPU vertex extrusion on a back-face-only render pass). Color: `#1e1e24` (very dark blue-grey) for all characters, always — not pure black. Specified by Araminta (art direction, Sprint 28 Round 2). No screen-space outline system. At LOD tier 2 (billboard impostor), the outline is baked into the impostor sprite — no separate draw call needed at that tier. - **Rationale:** Inverted hull is GPU-cheap, works correctly in 3D space, and produces clean consistent outlines. Screen-space methods (e.g., Sobel filter) are more expensive and produce artifacts at isometric angles. The billboard LOD tier naturally subsumes the outline into the baked sprite, so the system degrades gracefully under performance pressure without special outline handling. - **Raised by:** Sprint 28 workshop consensus (Tyre technical, Jeroen confirmed) - **Dissent:** None - **Cross-reference:** [D-149](#d-149-3d-characters-rendered-live-in-scene--not-pre-rendered-sprites), [D-152](#d-152-character-lod--performance-driven-budget-not-distance-threshold), [D-154](scope.md#d-154-character-outline-is-not-a-relationship-indicator--uniform-dark) ### D-151: Direction count — 8 server-side facings, 4 visual groups client Sprint 28 - **Date:** 2026-03-17 - **Decision:** The server tracks **8 facing directions** for all characters (N, NE, E, SE, S, SW, W, NW) — full resolution, future-proof. The client renders **4 visual groups** for Sprint 28: North (covers N, NW), East (covers NE, E), South (covers SE, S), West (covers SW, W). The 3D model is rotated to the true 8-direction angle; only the visual mesh/asset groups are 4-way. E and W groups share mirrored assets. Post-Sprint 28: additional direction-specific mesh variants can be authored for diagonal facings without protocol changes. - **Critical distinction — perception vs. rendering:** The 8 server facings are **perception system input** (fog-of-war, vision cone direction, all simulation logic). The 4 visual groups are **rendering output** (what the player sees). These are not the same thing. Diagonal facings (NE, NW, SE, SW) do not exist as visible character states — a character facing NE renders as East. EntityRenderer maps the server's 8-direction value to a 4-group CharacterFacing before calling the compositor. ModelRoot body rotation uses the true 8-direction angle for subtle lean. - **Rationale:** Hybrid approach (Tyre): 8 server facings ensures the data model never needs migration. 4 client visual groups keeps Sprint 28 asset authoring cost manageable — each new clothing item needs 2 unique meshes (N, E) plus mirroring, not 4 or 8. The 3D rotation to true angle (before the visual group snap) gives subtle body lean and positioning cues even within a visual group. Characters snap to nearest cardinal with smooth rotation interpolation. - **Raised by:** Tyre (hybrid proposal), confirmed by Team Leader (Jeroen) — Sprint 28 Round 2 - **Dissent:** None - **Cross-reference:** [D-148](#d-148-30-low-angle-camera-with-45-map-rotation--supersedes-d-019), [D-149](#d-149-3d-characters-rendered-live-in-scene--not-pre-rendered-sprites) ### D-152: Character LOD — performance-driven budget, not distance threshold - **Date:** 2026-03-17 - **Decision:** Character LOD degrades based on GPU **frame budget**, not distance or fixed character count. Three tiers: - **Tier 0 (full):** All characters at full 3D detail with all compositor layers active. - **Tier 1 (simplified mesh):** Reduced-poly model; clothing layers merged into combined mesh; inverted hull still active. - **Tier 2 (billboard impostor):** Flat sprite impostor. Outline baked in. Must visually preserve: (1) body size tier (slim/average/stocky silhouette), (2) dominant clothing color (`cloth_primary`). Applied to characters furthest from player first. LOD trigger is **proactive on projected character count**, not reactive on frame drop. Reactive triggering produces visible hitches; proactive demotion is invisible. LOD demotes characters outward from player: nearest characters always stay at Tier 0 longest. When paused, render budget is fully freed and all characters restore to Tier 0 — player can inspect the scene at leisure. In a chaotic moment (400+ characters on screen), peripheral detail naturally degrades, which matches the cognitive experience of chaos. - **Rationale:** A fixed distance or count threshold would produce visually jarring sudden LOD pops when character density changes (a crowd gathering). Performance-driven budget is adaptive and invisible to the player. The "paused = full detail" design is a deliberate player affordance — it makes the pause button feel powerful. - **Raised by:** Sprint 28 workshop (Jeroen — Q2 resolution) - **Dissent:** None - **Cross-reference:** [D-149](#d-149-3d-characters-rendered-live-in-scene--not-pre-rendered-sprites), [D-150](#d-150-character-outline--inverted-hull-method) ### D-160: Body meshes must be segmented into 18 bone-group regions - **Date:** 2026-03-19 (updated 2026-03-19: 17→18, added torso_upper) - **Decision:** Character body meshes are segmented into 18 bone-group regions (head, neck, torso, torso_upper, arm_upper_l/r, arm_lower_l/r, hand_l/r, leg_upper_l/r, leg_lower_l/r, foot_l/r, eyes, eyebrows). Each segment is a separate skinned GLB on the shared 65-bone skeleton. Segments have a 1-ring vertex overlap at boundaries to eliminate visible seams during animation. Segments can be individually hidden when clothing covers them. The `torso_upper` variant covers chest-only (for jackets/vests that leave the waist exposed). - **Rationale:** Validated in the Quaternius aesthetic spike. Monolithic body meshes block the compositor: hiding the body to show clothing also removes the head and limbs. Segmentation enables per-slot visibility, head separation for Trellis-generated faces, and limb loss mechanics. The 1-ring vertex overlap was validated as seamless at gameplay zoom. - **Raised by:** Jeroen + Tyre, during spike validation. - **Cross-reference:** [D-159](scope.md#d-159-character-body-type-enum-5-types--2-genders--1-child-skeleton-11-total) (body type enum), [D-149](#d-149-3d-characters-rendered-live-in-scene--not-pre-rendered-sprites) ### D-161: Head is always a separate mesh on the Head bone - **Date:** 2026-03-19 - **Decision:** Character heads are never baked into body or clothing meshes. The head is always a separate mesh attached to the Head bone via BoneAttachment3D. Trellis generates unique head shapes per character. Hair attaches to the same bone as a separate swappable mesh (enables hairdresser mechanic). The neck stays with the body mesh as a separate segment. - **Rationale:** Validated in the Quaternius spike. The Quaternius pack bakes the head into the body mesh — this blocks clothing display (hiding body = losing head) and prevents per-character face variation. Separating the head unlocks: Trellis-generated faces, idle look-around animation, helmet/hood equipment, hair as a swappable accessory. - **Raised by:** Jeroen, during spike validation. - **Cross-reference:** [D-160](#d-160-body-meshes-must-be-segmented-into-18-bone-group-regions), [D-163](#d-163-trellis-generates-unique-heads-per-character-via-boneattachment3d) ### D-162: Clothing is pre-baked per body type via Blender Surface Deform - **Date:** 2026-03-19 - **Decision:** Clothing meshes are authored on a reference body type, then batch-fitted to each of the 9 body types via Blender's Surface Deform modifier in an offline pipeline. The fitted variants are exported as separate GLBs. At runtime, the compositor loads the variant matching the character's body type. Runtime clothing scaling does not exist — it was tested (bone pose scaling in Godot) and failed catastrophically. - **Rationale:** Three approaches were tested during the spike: (1) runtime bone scaling — failed with mesh collapse at joints, (2) Blender bone scaling — same failure, (3) Blender Surface Deform — produced output (visual quality at extremes is Q-060). The Surface Deform pipeline is the only viable path. Pre-baking is required; runtime fitting is impossible. - **Raised by:** Tyre + Araminta, during spike research and validation. - **Cross-reference:** [D-159](scope.md#d-159-character-body-type-enum-5-types--2-genders--1-child-skeleton-11-total), Q-060 ### D-163: Trellis generates unique heads per character via BoneAttachment3D - **Date:** 2026-03-19 - **Decision:** Each character in the game receives a unique Trellis-generated head mesh. The head is a static (unskinned) mesh attached to the Head bone via BoneAttachment3D. Hair is a separate mesh on the same bone. Head generation happens offline during character creation/world generation. The concept image prompt controls face shape, ethnicity, age, and features. - **Rationale:** Validated in the spike: Trellis head + BoneAttachment3D + toon shader produces a coherent character at gameplay zoom. This is the only Trellis output that works on the Quaternius rig — clothing and body meshes both failed. Head generation gives per-character visual identity without hand-modeling. - **Raised by:** Jeroen (insight that Trellis output works as heads), validated by Tyre. - **Cross-reference:** [D-161](#d-161-head-is-always-a-separate-mesh-on-the-head-bone) ### D-164: Fork Quaternius skeleton, replace all body meshes - **Date:** 2026-03-19 - **Decision:** The Quaternius 65-bone skeleton and Universal Animation Library are adopted as the character rig foundation. All body meshes are replaced with hand-authored meshes on the same skeleton. The CC0 license permits unrestricted forking. Quaternius Source tier ($5/mo Patreon, one month) is recommended for .blend source files to support the Blender batch scripting pipeline. - **Rationale:** The spike validated the skeleton and animation library as clean, compatible, and well-structured. The Quaternius Superhero body mesh is retained as the **muscular** body type (D-159); the Regular mesh becomes **average**; the Teen mesh becomes **teen**. The remaining body types (thin, heavy, child) are new meshes authored on the same skeleton. Procedural body type generation via bone scaling failed — separate meshes are required. The skeleton is the expensive part to create from scratch; keeping it and replacing/extending the meshes is the correct split. - **Raised by:** Tyre + Araminta, confirmed by Jeroen. - **Cross-reference:** [D-159](scope.md#d-159-character-body-type-enum-5-types--2-genders--1-child-skeleton-11-total), [D-160](#d-160-body-meshes-must-be-segmented-into-18-bone-group-regions) --- ### D-166: Development Cascade — 6-Phase First-Things-First Build Order - **Date:** 2026-03-24 - **Decision:** Development follows a strict 6-phase cascade. Each phase completes before the next begins. No v0.2 target. No scoping negotiations. | Phase | Focus | Deliverable | |-------|-------|-------------| | 1 | Wiki content complete — all planets, moons, stations, heightmaps, artwork | Implant-ready Godot map with click-throughs + wiki/GTTR popups | | 2 | Economics layer — supply/demand, transport, corporations, supply chains | Economics spreadsheets/graphs with runtime-tweakable simulation | | 3 | Planetary/moon maps & station layouts — cities, rivers, mountains, roads, biomes | Atlas of the Reach (implant app) | | 4 | World generation (tile/chunk/block) — deterministic multilayer seed→tile cascade; each layer viewed as a map in the implant Atlas (no in-world rendering) | Deterministic walkable-world data + per-layer Atlas maps + asset catalog | | 5 | Player control & in-world rendering — character, walls/stairs/doors, lighting, drawn on generated tiles (no test map) | Player viewport with final-version assets on the generated world | | 6 | Detail coloring — room-level content, cultural architecture | Only when the world is walkable | - **Amendment (2026-05-22):** Phases 4 and 5 swapped — **world generation now precedes player control**. The rule: no player-control or in-world rendering work begins until the generator can deterministically seed-generate every tile of every world via the full multilayer cascade. The original Phase 4 "2-floor test map" is **dropped** — test layers are produced by the generator itself once layer-drawing begins; we start drawing the world only when generation knows what to draw. Generation progress is viewed as **per-layer maps in the implant Atlas** (the Phase 3 deliverable, already built), not via an in-world renderer. The existing in-world rendering code is **left as-is until Phase 5** — neither built upon nor removed before then. Rationale: building player systems against a throwaway test substrate means rebuilding them against real generated tiles later; gating player work on deterministic generation avoids that waste. Epics T-749 (now Phase 5) and T-750 (now Phase 4) and the CLAUDE.md cascade table are updated to match. - **Amendment (2026-06-12):** Build order **within and after** the cascade clarified (Jeroen). The world is built **outside-in**: (1) geo layer; (2) the economic layer drawn onto the world — cities, buildings, streets, blocks, and the inter-settlement road/rail network; (3) building templates; (4) **door boundary contracts** defining the seam between exteriors and building interiors. Only then (5) the background NPC layer and building interiors (Phase 6), and only after that does gameplay/scenario design resume ("making it a game again" — tracked in the post-cascade gameplay parking epic, outside any phase). **Player control and visual rendering (Phase 5) ramp in parallel once outside generation produces walkable exteriors** — the Phase-5 trigger is "outsides generate deterministically and are walkable", not "all Phase-4 polish complete"; T-962 holds the gate and is updated to this trigger. Guiding statement: *"Before this is a game I want it to be a Reach a character can travel through."* NPCs, scenarios, and gameplay systems are conceived, designed, and built only after the traversable world exists. This amendment absorbs the v0.2 scope cluster — D-114, D-115, D-117, D-118, D-120 are marked superseded by this record, with each one's surviving design substance noted on the record itself. - **Amendment (2026-07-21):** **Seamless zoom ladder is a hard Phase-4 deliverable condition** (Jeroen, reviewing the Groombridge/Lendel Atlas zoom-ladder captures). The guiding statement (BHAG) is deliberately **unchanged** — *"Before this is a game I want it to be a Reach a character can travel through."* What changes is the Phase 4 exit bar: the Atlas must provide a **continuous zoom ladder from the planetary map down to tile scale**, where every zoom level shows **deterministically calculated information at that level's native granularity** ("LoD on the information in view" — the T-1143 ruling), never magnified interpolation of a coarser composite. Evidence that forced the condition: at Lendel's scale one heightmap pixel spans ~19 districts, so the 16-district window is *sub-pixel* relative to its source — beyond fit-zoom the current viewer can only linearly magnify the same 16×16 composite (captures 2026-07-21, `.cache/screenshots/groombridge/`), and the real waterline sat ~36 districts from the pixel-edge estimate, i.e. **all** information at window scale and below is invented detail (D-227) that must be *derived*, not smoothed. T-1143 is the design pass for the ladder (granularity rungs, wire budget, the D-226(d) floor question); the Phase-4 epic (T-750) does not close until the ladder stands. CLAUDE.md's cascade table is updated to carry the condition. **Corollary (Jeroen, same day):** the authored heightmap PNGs are thereby demoted to **invisible input to the seed/derivation calculation** — no zoom level displays them directly, *including the orbital/planetary level*, which must itself be **fully derived**. Display at every rung samples the derivation at **canvas resolution** (the ladder is a continuous field, not a stack of fixed display rasters), which removes both zoom oversampling (magnified interpolation) and undersampling (the ~40–78 km/px source-raster floor) by construction, and lets the map adapt to any viewport size. The `import_heightmaps` bake into systems.db survives as derivation *input* storage only; the AtlasViewer's direct heightmap-texture display path retires when the derived planetary rung lands (in T-1143's design-pass scope). - **Amendment (2026-07-23, body-map-viewer workshop — stepped ladder supersedes continuous; see [D-255](#d-255)):** the 2026-07-21 corollary sentence — *"Display at every rung samples the derivation at canvas resolution (the ladder is a continuous field, not a stack of fixed display rasters)"* — is **repointed, not deleted.** The zoom mechanism is now **stepped**: the server generates one data canvas per discrete zoom step, sampled at that step's native [D-243](#d-243) gridunit spacing. The corollary's *guarantee* survives, re-expressed per-step: **a gridunit is never derived coarser than its step's own rung floor, and never displayed finer than the display-ratio tunable** (1×1 ideal, ≥5×5 px/gridunit acceptable). The "continuous field" framing is retired as literally false (it never was continuous once server-side per-step canvases replaced the client `_canvas.scale` model). The corollary now reads: *"each zoom step's data canvas is a derivation sampled at that step's native gridunit spacing; display within a step holds that canvas at a fixed, texel-exact ratio; between-step magnification of the held canvas is bounded to one step interval and is the ladder's only sanctioned display-time scaling."* **This amendment owns the one honest new gap it creates:** in the interval just before a step-cross, the held (coarser or adjacent) canvas is magnified to fill the new step's viewport for the fetch duration — the exact operation the original corollary was written against, now a **named, bounded exception** rather than a silent violation. The bound is joint in two knobs — step count (more steps → smaller per-step magnification factor) and the display ratio — and the fetch interval is short (measured: step-canvas texture upload 0.03–4.6 ms at every size, uncached 330K-gridunit step derive+encode ~80 ms — a double-digit-millisecond transient, not a resting display state). **Ladder extent:** the 2026-07-21 amendment's *"down to tile scale"* phrasing is narrowed by [D-255](#d-255) — the Atlas ladder bottoms out at **chunk (64 m)**, not tile (1 m); tile/voxel is Phase-5 in-world content, not an Atlas rung. - **Rationale:** The pattern of negotiating pragmatic v0.2 cuts while discussing room-level detail repeatedly produced superseded decisions, confused agents, and distracted from building the actual product. The cascade enforces a first-things-first discipline: each layer of the game is grounded in the layer below it before detail is added. - **Raised by:** Jeroen, established 2026-03-24 during world generation workshop. - **Dissent:** None. - **Cross-reference:** Initiative T-745, Epics T-746–751, CLAUDE.md cascade table, `docs/workshops/world-generation/workshop-outcomes.md`, [D-255](#d-255) (body-map-viewer stepped render architecture — the corollary's stepped successor). --- ### D-169: Implant UI component library — Control node tree with custom Theme - **Date:** 2026-04-05 - **Decision:** The implant UI (all diegetic neural overlay panels — star map info, travel planner, station profiles, GTTR reader, cargo manifest, etc.) uses **Godot Control node components** composed in scenes, styled via a shared `Theme` resource. Not `_draw()` draw-objects, not hand-rolled rendering. - **Rationale:** The implant system is tagged as dynamic — different hardware, upgrades, damage states, and faction overlays will change the UI's look and behavior at runtime. This requires layout automation (reflow, resize, expand/collapse), accessibility support, and theme swapping — all things Godot's Control tree provides and a `_draw()` approach would need to reimplement. The team (Tyre, Stig, Araminta) initially favored `_draw()` for pixel control, but the dynamic implant requirement makes Control nodes the better long-term investment. - **Architecture:** - `ImplantPanel` — `PanelContainer` with theme override, serves as root for any implant overlay - `ImplantHeader` — `HBoxContainer` (title label + subtitle label) - `ImplantSeparator` — `HSeparator` with theme override - `ImplantDataRow` — `HBoxContainer` (key label + value label, two-column) - `ImplantTextBlock` — `RichTextLabel` for wrapping narrative text - `ImplantStatusBadge` — colored dot + label - `ImplantProgressBar` — two-tone, no gradients - `ImplantTabRow` — uppercase labels, underline-active pattern - `ImplantExpandable` — chevron + collapsible section - Theme resource (`.tres`) defines: colors (primary, dim, accent active/positive/negative/warning), spacing (line height, separator padding, panel padding), font sizes (header, body, small, caption) - Different implant hardware swaps the entire Theme resource at runtime - **Rejected alternative:** R-NNN `_draw()` RefCounted components — pixel-precise, zero scene tree overhead, but would require reimplementing layout automation, scroll, and interaction handling as the implant system grows. Correct for a static HUD; wrong for a dynamic system. - **Raised by:** Jeroen + Tyre/Stig/Araminta design discussion, 2026-04-05 - **Dissent:** Tyre and Stig initially favored `_draw()` for aesthetic control. Reversed when the dynamic implant requirement was surfaced. - **Semantic color roles:** `PRIMARY_TEXT` (#c8d0e0), `DIM_TEXT` (#667788), `ACCENT_ACTIVE` (#f0d060), `ACCENT_POSITIVE` (#44aa66), `ACCENT_NEGATIVE` (#aa4444), `ACCENT_WARNING` (#aa8844), `SEPARATOR` (#c8d0e0 at 12%) - **Interaction pattern:** Invisible until touched. Hover shows 0.3 alpha rect. Active shifts text to `ACCENT_ACTIVE`. No button chrome — the implant doesn't label its affordances. --- ### D-170: HUD visibility groups — gameplay vs implant layers - **Date:** 2026-04-05 - **Decision:** HUD elements register into named visibility groups via a `HudGroups` autoload. Groups control show/hide of related elements as a unit. Exclusive groups (gameplay, implant) are mutually exclusive — showing one hides the other. Non-exclusive groups (modal, debug) overlay independently. - **Rationale:** Full-screen implant panels (star map, travel planner, GTTR reader) need to hide gameplay HUD elements (stance, minimap, health, interaction prompts). Without groups, each panel manually hides/shows individual nodes — error-prone and unsustainable as the panel count grows. - **Groups (hierarchical):** - `gameplay` — stance indicator, minimap, HUD status, interaction prompts, cursor, inventory, news ticker, examine display. Visible during normal gameplay. - `implant/map` — star map navigator. Mutually exclusive with gameplay and other implant apps. - `implant/wiki` — GTTR reader. Mutually exclusive with gameplay and other implant apps. - `implant/journal` — knowledge journal. Mutually exclusive with gameplay and other implant apps. - `implant/travel` — travel planner. Mutually exclusive with gameplay and other implant apps. - `implant/station` — station profile. Mutually exclusive with gameplay and other implant apps. - `implant/cargo` — cargo manifest. Mutually exclusive with gameplay and other implant apps. - `modal` — settings, bug report, loading screen, debug console. Independent — overlays on top of anything. - `debug` — debug overlay, gauntlet HUD, checklist. Independent. - **Exclusivity rules:** Opening any `implant/*` app hides gameplay and any other implant app. Closing an app returns to gameplay. Modal and debug are independent layers. - **API:** `HudGroups.register(node, "implant/map")`, `HudGroups.open_app("implant/map")`, `HudGroups.close_app()`, `HudGroups.toggle_app("implant/map")`, `HudGroups.is_app_active("implant/map")`, `HudGroups.is_implant_active()` - **Implementation:** `client/scripts/autoloads/hud_groups.gd` — lightweight autoload. Uses z-index layer management (not visibility toggling) so all nodes stay active in the tree. Emits `gameplay_occluded` signal when a fullscreen app covers gameplay — renderers extend `GameplayRenderer` base class to pause automatically. - **Raised by:** Jeroen, 2026-04-05 — "Walk" badge visible over fullscreen star map. - **Dissent:** None. --- ### D-188: Rename biome_summary to planet_class across codebase - **Date:** 2026-04-06 - **Decision:** The `biome_summary` field in `systems.db` and all references throughout the codebase are renamed to `planet_class`. "Biome" describes per-zone vegetation classification (Whittaker table — tropical rainforest, temperate deciduous, etc.). "Planet class" describes the overall planetary character (temperate, arid, frozen, etc.). The two concepts were conflated under one name, causing confusion in the planet generator pipeline where both exist. - **Scope:** DB column (`bodies.biome_summary` → `bodies.planet_class`), schema SQL, all Rust atlas code, wiki table headers (`Biome` → `Class`), atlas proposal JSONs, design docs, decision refs, planet generator scripts. - **Raised by:** Team Leader (Jeroen), during planet generator spike. - **Dissent:** None. --- ### D-191: Atlas of the Reach — Phase 3 Scope and Pipeline - **Date:** 2026-04-10 - **Decision:** Phase 3 delivers the Atlas of the Reach as an extension of the implant map (`implant/map`), adding planetary and regional zoom levels to the existing star map. The Atlas is a read-only spatial intelligence tool — the player looks at it to plan, not to execute actions. It serves dual purpose: gameplay information layer and world-texture content system. **1. Zoom Hierarchy** - 4 levels: Reach map (Phase 1, exists) → System view (orbital diagram, new) → Planetary view (hemisphere, new) → Regional view (hundreds-of-km, new — core Phase 3 deliverable) - Atlas is the star map extended downward, not a separate app. `implant/map` at different zoom levels. - Station maps and underground/cave city maps are DEFERRED to a later sprint (similar bounded generation pattern). - **Amended 2026-07-13:** generation-layer maps (D-226/T-960) bottom out at settlement/quarter-skeleton granularity — chunk/tile/voxel fill is never a planetary map layer; it is harness-verified and inspected in-world in Phase 5 (see the D-226 amendment of the same date). **2. Existing Foundation** - 2,394 heightmap PNGs (1024×512 equirectangular, production quality) - 2,394 markers.json files with procedural geometry (rivers, oceans, mountains) — all names null, all cities/roads/rail/POIs empty - Planet-gen pipeline (`tooling/planet-gen/`) explicitly designed for Phase 3: `render_heightmap.py` line 26-27 defers cultural overlay to the atlas app - 301 systems in systems.db, 273 inhabited bodies, 466 stations, 668 gate links **3. Content Pipeline — Sequential Settlement Growth Simulation** - City placement is terrain-aware and sequential (not scatter) - Capital first: favor river mouths (~50% of capitals), scored by habitability (temperature, moisture, slope), coastal access, flat hinterland - Subsequent cities: grow along rail corridors from capital (multi-source Dijkstra on cost grid) - At cities 3–4: first foothold on a new continent if one exists (port city) - Roads and rail: A* pathfinding on terrain cost grid (water=impassable, mountains=expensive, rivers=cheap corridors), minimum spanning tree — NO straight lines - Quadrant distribution: after 2 cities in the same map quadrant, subsequent cities must prefer unoccupied quadrants unless those quadrants have no habitable land - Variation: ±25% noise on scoring per seed — same terrain, different seed → different city network - Settlement pattern modifies spacing (`urban_concentrated`=tight, `dispersed`=wide) - All systems same depth, scaled by population. No manual tier classification. **4. Naming Pipeline — Gemma 2 Voice Pipeline** - Geographic names generated by the existing Gemma 2 voice pipeline (`server/src/voice/`, `sr-voice` binary) - Input: body wiki page (`planet_class`, `cultural_corridor`, `settlement_pattern`, population, economic_role, atmospheric_tone) + corridor naming palette - Output: culturally-appropriate names for rivers, oceans, mountains, regions, cities - Corridor palettes: north_reach (Anglo-Saxon), south_reach (Iberian/Portuguese), east_reach (East Asian), west_reach (Germanic/Nordic), inner_orbit (institutional Latin/Anglo) - Dual purpose: Phase 3 content generation AND quality/consistency test of the in-game LLM pipeline - Batch throughput: ~80 min for all 2,394 bodies (placement) + ~40 min for 273 inhabited (naming). Parallelizable. - Earth-name blocklist as post-processing safety net. Dedup check against full name corpus. **5. Core Systems as Templates** - Core systems (Gateway/Sirius, Groombridge/Lendel, etc.) hand-authored as templates - Templates establish rulesets and quality bar for the generator - Generator produces all remaining bodies → hand-author refinements over the batch - Lendel (GJ 380c) is the first proving ground: arid, `urban_concentrated`, financial hub, 900M pop **6. Atlas Panel Architecture** - FULLSCREEN implant app (z=20), same component library as economics panel - 3 navigation levels: system picker → orbital diagram → body atlas (heightmap + overlays) - Heightmap displayed as `Texture2D` with pan/zoom - Marker overlay renders cities, roads, rail, POIs, named features on top of heightmap - Click city → City Data Panel (name, population, currency zone, Commission presence, gate distance, economics panel link) — shadow economy zone dropped per §7 amendment (2026-05-22) - Economics panel link: click-through to Phase 2 economics panel pre-filtered to that node — primary Phase 2/3 integration point **7. Overlay System — 8 MVP Overlays** - **Amendment (2026-05-22):** dropped the `shadow economy` toggleable overlay (9 → 8 overlays). Shadow economy is an *underwater simulation modifier* — `shadow_economy_intensity` (D-174) feeds derived signals such as `collection_efficiency` and signal 7 `official_coverage_ratio` (D-181), but it is not a user-navigable data point, so it does not warrant a player-facing atlas overlay or City Data Panel field. The simulation layer (D-174) is unaffected. Also removed from §6 (city data panel) and §10 (completion criteria) for consistency. - 5 always-on: terrain, infrastructure, named features, gate/spaceport POIs, political zones - 3 toggleable: population density, production zones, corporate presence (Tier 1 only) - Deferred overlays visible in toggle bar but locked with unlock requirements shown on hover (creates pull toward Phase 4+ systems) - Maps to D-181 signal visibility ladder **8. Settlement Data Model** - **Amendment (2026-05-22, [D-223](#d-223)):** superseded — markers.json is reduced to **names only** (a flavored name pool); it no longer carries position arrays or topographic-feature geometry. River/mountain positions derive from the heightmap + drainage (D-208); settlement positions from the economic sim + placement (D-211). The 6 hand-authored templates and their `reserved` pinning are removed (bodies stay as ordinary named bodies). The pixel-space schema below is retained as historical reference only. - **Amendment (2026-04-15):** The original §8 (below) described marker positions as lat/lon objects and city records keyed by `population_tier`/`primary_function`/`gate_terminal`/`continent_id`. That shape was aspirational — neither the generator nor the hand-authored templates ever emitted it. Both ended up writing pixel-space row/col arrays against a `512 × 256` storage grid, and PR #129 canonizes that shape so the code and the decision stop drifting. The original prose is preserved immediately below; the current shape follows. - **Original (2026-04-10, superseded):** markers.json schema per body: `cities` (name, lat/lon, population_tier, primary_function, gate_terminal, continent_id), `roads` (path polylines, connects), `railroads` (path polylines, connects), `pois` (name, kind, position), plus existing rivers/oceans/mountains with names filled. Population tier → city count: `floor(log10(pop/1M))`, modified by `settlement_pattern`. Gate terminal POI at largest population center, sometimes scattered to a smaller one. Moons: same depth as planets, scale with population. - **Current canonical format:** markers.json is stored in heightmap pixel space. Every marker file declares a `grid: { w, h }` header — the generator, the 6 hand-authored templates (Lendel, Edict, Vuurkloof, Røros, Cairnside, Estrade), and all 2394 procedural seed files ship `{"w": 512, "h": 256}`. Every position is a two-element **array** `[row, col]` of integer pixels into that grid, where `row ∈ [0, h)` and `col ∈ [0, w)` (row is the first axis to match NumPy convention and the flood-fill / A* / cost-grid code that `tooling/planet-gen/` already runs in). Polyline geometry (`roads[*].path`, `railroads[*].path`, `rivers[*].path`) is `[[row, col], [row, col], ...]`. - **markers.json top-level schema:** - `grid`: `{"w": 512, "h": 256}` - `cities[]`: `{id, name, kind, center: [row, col], population}` — `kind` is `capital` or `city`; `name` is empty when awaiting gemma_naming.py (T-833). - `roads[]`: `{id, name, kind, path: [[row, col], …]}` — `kind` is `commercial` by default for generated roads; hand-authored roads use `highway`, `rural`, etc. - `railroads[]`: same shape as `roads[]`; generated default `kind` is `passenger_freight`. - `pois[]`: `{id, name, kind, center: [row, col]}` — generated POIs are `kind: "transit"`; hand-authored POIs use `institutional`, `cultural`, `corporate`, etc. - `rivers[]`: `{id, name, path: [[row, col], …]}` - `oceans[]`: `{id, name, kind, center: [row, col], area_fraction}` where `kind` is `lake` | `sea` | `ocean`. - `mountain_ranges[]`: `{id, name, center: [row, col], peak: [row, col], area_cells}` - Pixel space is what the heightmap analysis (flood-fill, A* cost grid, city placement) natively operates on; it is deterministic and avoids double-conversion through a projection. - Lat/lon strings are a **display-time derivation**, not a storage format. The atlas UI converts `[row, col]` + `grid: {w, h}` into an equirectangular `lat°N/S, lon°E/W` string for the city data panel and hover tooltips, using the body's radius for any great-circle distances it needs. This keeps the immersive surface without paying conversion cost in the generator, the DB, or the diff churn on hand-authored files. - Atlas DB index (`atlas_cities`, `atlas_roads`, `atlas_railroads`, `atlas_pois`, `atlas_rivers`, `atlas_oceans`, `atlas_mountain_ranges`, `atlas_body_grids`) mirrors these scalar fields row-by-row for implant-app and development queries; polyline geometry stays in the JSON files next to the heightmaps. - Population tier → city count: `floor(log10(pop/1M))`, modified by `settlement_pattern` - Gate terminal POI: at largest population center, sometimes scattered to a smaller one - Moons: same depth as planets, scale with population **9. Implementation Pipeline** - Python: `tooling/planet-gen/generate_atlas.py` — reuses `planet_simulation.simulate()`, adds `city_placement.py`, `infrastructure_gen.py`, `gemma_naming.py`, `markers_writer.py` - `make atlas-generate` runs the full batch. Deterministic per seed. Incremental (skips up-to-date bodies). - Pipeline order: simulate terrain → analyze (continents, habitability, river mouths, cost grid) → place cities sequentially → generate infrastructure (A* MST) → name via Gemma 2 → write markers.json **10. MVP Completion Criteria** 1. Navigation chain works end-to-end (Reach → system → planet → regional) 2. Regional map content complete for all inhabited bodies 3. Population-scaled depth (core systems rich, frontier sparse) 4. City data panel works (click any city, see data + economics link) 5. Economics panel integration works (link opens Phase 2 panel filtered to node) 6. All 8 MVP overlays present and functional 7. Atlas is read-only (no verbs execute from map) 8. Stations show at system view with mini data panel (no drill-down) - **Rationale:** The heightmap pipeline was designed with Phase 3 in mind — 2,394 base maps exist. The critical path is content (filling empty markers.json arrays), not technology (the atlas panel follows the established implant component pattern). Sequential settlement growth simulation produces more realistic city networks than scatter placement — each city's location is informed by previous placements, terrain, and economic logic. Using the Gemma 2 voice pipeline for naming tests the in-game LLM quality while generating content. Population-scaled depth without manual tier classification keeps the pipeline simple and the authoring burden manageable. - **Raised by:** Full planning team workshop, Sprint 34 (T-748). Participants: Gestalt (systems design), Tyre (technical architecture), Miri (worldbuilding), with Jeroen as workshop participant. - **Dissent:** None. - **Cross-reference:** [D-166](architecture.md#d-166) (development cascade — Phase 3), [D-036](content.md#d-036) (Sova as canonical setting), [D-093](content.md#d-093-sova-transit-district--spatial-layout-and-district-topology) (Sova spatial layout), [D-094](#d-094) (district hierarchy), [D-095](content.md#d-095) (Horizon stations), [D-170](#d-170) (HUD visibility/implant apps), [D-169](#d-169) (implant component library), [D-181](economics.md#d-181-signal-vocabulary) (signal vocabulary/visibility ladder), [D-174](economics.md#d-174-shadow-economy-layer) (shadow economy intensity), [D-175](economics.md#d-175-corporation-taxonomy-and-prerequisite) (corporation taxonomy), [D-138](content.md#d-138-llm-re-voicing-pipeline-for-npc-voice) (Gemma 2 voice pipeline) ### D-192: Drop PROTOCOL_VERSION lockstep handshake - **Decision:** Deprecate the snapshot envelope `version` field, the `PROTOCOL_VERSION` constants on both server (`server/src/bridge/types.rs`) and client (`client/scripts/protocol/protocol.gd`), and the version-mismatch guard in `Protocol.decode_snapshot()`. Removal is tracked in ticket **T-868** (server + client coordinated, sprint 37 or later). Once removed, genuine schema mismatches will surface as MessagePack decode errors or missing-field errors at the consumer; that signal is sufficient for our deployment model. Until T-868 lands, the field and guard stay in place — they are no longer load-bearing, but removing them requires coordinated edits on both sides and fresh fixture regeneration. - **Rationale:** The version constants were designed for a network deployment where client and server can ship out of sync. Our actual deployment is a subprocess: the Godot client launches the Rust server it was built with. They are *always* in sync at runtime — the version check has never caught a real mismatch in the field, only dev-time forgetfulness. The cost has been measurable: every protocol-shaping sprint requires bumping two constants in lockstep, and we accumulated tautological tests asserting `PROTOCOL_VERSION == N` (deleted in sprint 36 — see ticket from this D-record). Removing the handshake makes the per-sprint cost zero. **Reversibility:** When/if networked multiplayer arrives (no firm date — see [D-005](#d-005-architecture-godot-client--rust-server-via-subprocess)), the natural fit is a one-time handshake at connection time (a single client-version vs. server-version exchange in the connection protocol), not a per-snapshot version stamp. So even the multiplayer path doesn't argue for keeping the per-snapshot field — that field would be doubly redundant once a connection-time check exists. The design space hasn't been narrowed. - **What we lose:** A single eager, human-readable error at connect time ("client v22 ↔ server v23"). A genuine dev-time schema drift will now surface as a downstream decode/missing-field error, possibly seconds into a session rather than at handshake. - **What we keep:** All field-presence and roundtrip tests in `test_protocol_bridge.gd`, `test_signal_sprint24.gd`, etc. — these cover the *behavior* the version constant was meant to gate. Decode failure in `Messagepack.decode()` still rejects malformed payloads. - **Amendment (2026-07-23, body-map-viewer workshop — persistent-cache scope note; see [D-255](#d-255)):** the co-ship "always in sync at runtime" guarantee holds for the **live wire** but does **not** extend to a **disk-backed persistent cache**. [D-255](#d-255)'s client-side Atlas cache (`user://atlas_cache/`) survives a game update by construction — a cache file written by version N read back by version N+1 crosses exactly the version boundary this record assumes away elsewhere. That boundary is handled in [D-227](#d-227)'s cache schema/version-tag amendment (a mismatch = cache miss, re-fetch, never decode), not here — this note only records that D-192's reasoning is scoped to the live wire and the persistent cache is the one place it does not reach. D-192's live-wire reasoning is unchanged. - **Raised by:** Jeroen, sprint-36 client triage. Triggered by stale `test_protocol_version_is_19` assertions failing across two suites after the v23 bump, requiring mechanical edits in both places to "fix." - **Dissent:** None. - **Cross-reference:** [D-005](#d-005-architecture-godot-client--rust-server-via-subprocess) (subprocess model — always co-shipped), [D-227](#d-227) (persistent-cache version-tag — where the disk-format version boundary is handled), [D-255](#d-255) (body-map-viewer render architecture — the persistent Atlas cache). --- ### D-194: Three-Component District Mix Algorithm for City District Type Distribution - **Date:** 2026-05-01 - **Decision:** District type distribution for a generated city is computed from three components combined at generation time: 1. **Population tier guarantees** — minimum district counts enforced by city size. Population tier is `floor(log10(pop / 1_000_000))`, capped at 5. Larger populations guarantee minimum counts of Transit, Commercial, and Residential districts. 2. **10×9 economic multiplier table** — rows are 10 `economic_role` values (manufacturing, financial, agricultural, extraction, service_mixed, institutional, transit_hub, research, military, residential); columns are 9 `DistrictType` variants. Each cell is a weight multiplier (0.0–3.0) applied to that district type's base probability for cities of that economic role. 3. **Political archetype modifiers** — `PoliticalArchetype` shifts weights for Institutional, Restricted-access, and Civic district types. Corporate archetype boosts Commercial + Restricted. Commission archetype boosts Institutional + Administrative. Pioneer archetype boosts Mixed-use + Organic residential. - **Founding age character** is applied as a post-mix adjustment to `BlockIrregularity` (see D-216), not to the district type distribution itself. - The mix is self-contained per city: two cities with the same economic role, population tier, and political archetype produce the same district type distribution (modulo seed-driven noise). No city-to-city state dependency. - Integer weights throughout — no f32 for D-010 determinism. - **Rationale:** Economic role should visibly shape a city's physical form. A financial hub looks different from a mining hub. Population tier prevents cities from being too small to sustain their economic function. Political archetype encodes power structure in spatial form — Corporate settlements are commercially dense, Commission settlements are institutionally heavy. The three-component model is the minimum set to produce legible variety; adding more inputs risks over-constraining the generator. - **Ticket:** T-920 - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-191 (atlas pipeline), D-213 (DistrictType enum), D-214 (PoliticalArchetype), D-216 (BlockIrregularity) ### D-195: Attractor-Matching Compatibility Matrix for Generative City Placement - **Date:** 2026-05-01 - **Decision:** City placement on a planetary surface uses an attractor-matching model. A `GeographicAttractor` is a terrain feature that increases city placement score at nearby positions. Seven `AttractorType` variants: `RiverMouth`, `CoastalAccess`, `RiverCrossing`, `ValleyFloor`, `PassEntrance`, `LakeShore`, `PlainCenter`. A `CompatibilityMatrix` is a 10×7 scoring table (10 `economic_role` values × 7 attractor types) whose cells are float weights (0.0–3.0) representing how strongly that economic role favors that terrain feature. Examples: manufacturing → RiverMouth 2.8, ValleyFloor 2.1; financial → CoastalAccess 2.5, PlainCenter 1.8; agricultural → ValleyFloor 3.0, PlainCenter 2.5. The matrix is authored data (not computed at runtime). Attractor extraction from heightmaps is defined in D-209. Matching algorithm is defined in D-211. - **Rationale:** Terrain-naive city placement produces spatially incoherent worlds. The compatibility matrix gives different city types different terrain affinities, so financial hubs appear on coasts and agricultural cities appear in fertile valleys — without hard-coding placement rules per city type. The weight matrix gives graduated preference, not binary requirement. - **Amended 2026-06-03 (T-955):** the matrix weights and the whole placement-scoring path are **integer basis-points, not f32** (D-010 determinism / D-227 save-critical). When T-955 wired `match_cities` into the deterministic generation cascade, the original f32 scoring became a live cross-platform divergence risk (a near-tie score comparison or the Hungarian's f32 reductions can round differently per platform → a different world from the same seed). Now: `CompatibilityMatrix.weights` are `i32` bps (10000 = 1.0×; the examples above are 28000 / 30000 / 25000 …), `GeographicAttractor.strength` and `terrain_modification_cost` are bps, and `cell_score` / the Hungarian / `CityPlacement.score` use integer arithmetic. The 0.0–3.0 affinity semantics are unchanged; only the representation is now integer. - **Ticket:** T-919, T-925, T-955 - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-191 (atlas pipeline), D-208 (D8 drainage — attractor extraction), D-209 (feature tag extraction), D-211 (attractor-matching pipeline) ### D-196: SettlementClass Enum and Latent Settlement Active/Ghost Logic - **Date:** 2026-05-01 - **Decision:** Every settlement (city marker in `atlas_city_names`) has a `SettlementClass` that determines how it enters and exits active simulation: ```rust enum SettlementClass { NameLocked, // Named in wiki; always active regardless of population PopulationBudget, // Active if pop > threshold; ghost if below EconomicTriggered, // Active only while economic role condition is met OrganicGrowth, // Emergent; generated by simulation, no prior wiki record } ``` - **Active threshold** (applies to `PopulationBudget`): population ≥ 50,000 for a city to receive full Phase 1 district skeleton generation. Below threshold: 1-district stub with Minimal ComplexityTier. - **Ghost threshold** (applies to `PopulationBudget`): population < 5,000. Settlement is present in atlas data but receives no NPC population; structures are generated as abandoned (Worn/Derelict condition baseline). - `NameLocked` settlements bypass both thresholds — they are always simulated regardless of population (handles narrative-significant small towns). - `EconomicTriggered` settlements collapse to ghost state when their triggering economic condition lapses (e.g., a mining outpost depopulates when the mine is exhausted). - `OrganicGrowth` settlements are not in `atlas_city_names` at generation time; they are written to the table during simulation when a settlement emerges organically. - **Rationale:** Not every named location needs full generation, and not every simulated location is named. The classification separates authorial intent (NameLocked) from economic reality (PopulationBudget, EconomicTriggered) and simulation emergence (OrganicGrowth). Ghost settlements are important for world texture — abandoned mining towns and depopulated frontier outposts are as legible as thriving hubs. - **Ticket:** T-913 - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-191 (atlas pipeline), D-200 (CityGenerationContext), D-203 (BodyWorldState), D-207 (atlas_city_names) ### D-197: prosperity_baseline Derivation Formula with Topographic Gradient - **Date:** 2026-05-01 - **Decision:** Each city's `prosperity_baseline` (f32, 0.0–1.0, used as economic pressure state seed) is derived at generation time from four components: 1. **Economic role base** — lookup per `economic_role` value: manufacturing=0.55, financial=0.70, agricultural=0.50, extraction=0.45, service_mixed=0.60, institutional=0.65, transit_hub=0.60, research=0.65, military=0.55, residential=0.50. 2. **Population log-scale bonus** — `0.04 × floor(log10(pop / 1_000_000 + 1))`, capped at +0.12. Larger cities are generally more prosperous. 3. **Topographic gradient bonus** — terrain features that historically correlate with prosperity add to the baseline: river mouth +0.08, coastal access +0.06, valley floor +0.04, pass entrance +0.03. At most one terrain bonus applies (the highest-scoring attractor at the city's position). 4. **Seed noise** — ±0.05 uniform noise applied last (integer-seeded per city, D-010 determinism). - Formula: `base + pop_bonus + terrain_bonus + noise`, clamped to [0.1, 0.95]. - `prosperity_baseline` is not the current prosperity level — it is the simulation's starting point and decay/growth target. The live pressure simulation (D-026) drifts from this value based on trade flows, events, and faction pressure. - **Rationale:** A flat random baseline produces economically incoherent worlds. Terrain-informed prosperity encodes real-world patterns: port cities are wealthy, river-mouth cities are strategic. The log-scale population bonus prevents megacities from dominating without eliminating small-city character. Clamping to [0.1, 0.95] prevents degenerate all-thriving or all-collapsing starting states. - **Ticket:** T-920 (consumer of prosperity_baseline) - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-194 (district mix — consumes prosperity_baseline), D-195 (attractor types that produce terrain bonus) ### D-198: Economic Simulation Independence from Layer 1–2 Spatial Data - **Date:** 2026-05-01 - **Decision:** The economics simulation (Phase 2, D-026 background tier) runs independently of Layer 1 (galaxy graph) and Layer 2 (location profiles / planetary topography). Economic state is seeded at game start from `systems.db` data (economic roles, trade flows, corporate presence) and then drifts via the pressure simulation. The generator (Layer 7 district skeleton) reads economic pressure state as an input but does not feed back into the simulation model. The two layers communicate one-way: simulation → generator (pressure state used to set district condition and density), never generator → simulation. - **Prohibited:** Generator code must not modify `PressureState`. Generator code must not query live simulation state during async background generation tasks (race condition risk). Generator reads a snapshot of pressure state taken at generation dispatch time. - **Allowed:** The generator reads `economic_health`, `prosperity_baseline`, `industries`, and `faction_influence` from the snapshot. These are read-only inputs to Phase 1 skeleton classification and Phase 2 condition application. - **Rationale:** Bidirectional coupling between generator and simulation creates initialization order dependencies and potential circular references. The one-way data flow (simulation → generator snapshot → generator) keeps both systems independently testable and avoids race conditions in the Rayon thread pool (D-206). The generator is a consumer of economic state, not a participant in economic evolution. - **Ticket:** T-915 (CityGenerationContext reads economic snapshot) - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-026 (simulation tiers), D-200 (CityGenerationContext), D-206 (background generation queue) - **Amended 2026-05-25 (D-233 — structural-fill / condition-overlay two-pass split):** The "Phase 2 condition application" language is replaced by a formal two-pass model. **Pass 1 — Structural fill** (frozen; reads t=0 initial-economics only; re-derivable from `seed + initial_economics_snapshot`): zone-type per block, building footprint shape+position, building-type vocabulary tags (`BuildingTag`), `founded_era`, operations-surface extent (bulk industries), and the labor-demand signal feeding adjacent residential blocks. **Pass 2 — Condition overlay** (the sanctioned rolling-economy consumer; a paint layer OVER the frozen fill; refreshable on a cadence): `BuildingConditionState = New|Maintained|Worn|Derelict|Abandoned`; `OccupancyState = Full|Partial|Vacant`; `VegetationEncroachment` (Abandoned in appropriate sub-biomes, D-210); feeds the D-100 tile `DamageOverlay`. **Hard wall:** the condition overlay CANNOT change a `BuildingTag` (a mine plant becomes an *abandoned* mine plant, never an office). The fill generator's signature is `fill_chunk(ctx: CityGenerationContext, seed: SeedChain)` — NO access to `PressureState`, price signals, or tâtonnement output. The condition overlay is a SEPARATE Bevy system, triggered by `EconEvent` (D-180), with its own read set. Cross-ref D-233, D-100, D-180, D-210, D-217. Raised by: Burnelli/Tyre/Gestalt, economic-built-world workshop round 2. ### D-199: 6-Field Minimum Economic Read Set for City Generation Context - **Date:** 2026-05-01 - **Decision:** When building a `CityGenerationContext` (D-200), the generator reads exactly 6 fields from the economic pressure snapshot per city. Reading more fields is permitted but these 6 are the minimum required for correct Phase 1 skeleton classification: 1. `economic_role` — primary function of the city (determines DistrictType distribution via D-194) 2. `prosperity_baseline` — starting economic health (0.0–1.0, see D-197) 3. `population` — city population (determines ComplexityTier ceiling, BlockSkeleton density) 4. `dominant_faction` — faction with highest `faction_influence` at this location (affects Institutional and Restricted district bias) 5. `founding_age_years` — years since settlement founding (drives BlockIrregularity via D-216, era distribution) 6. `settlement_class` — `SettlementClass` enum value (D-196, determines whether to generate at all) - Fields 1–5 are read from `systems.db` (bodies table + economics tables). Field 6 is derived at generator dispatch time. - All 6 fields must be present before a generation task is dispatched. Missing fields abort the task with a logged error; generation does not proceed with partial context. - **Rationale:** A fixed minimum read set prevents generators from accumulating unbounded dependencies on simulation state. The 6 fields cover the minimum information needed to produce a correctly-classified skeleton. The abort-on-missing-fields rule ensures generator output is always deterministic from a complete context, never silently degraded from a partial one. - **Ticket:** T-915 (CityGenerationContext implementation) - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-196 (SettlementClass), D-197 (prosperity_baseline), D-198 (economic simulation independence), D-200 (CityGenerationContext struct), [D-237](#d-237) (authored specialization layer — read-set extended) - **Amended 2026-05-25 (D-229/D-232/D-233):** `CityGenerationContext` gains `morphology_zone`, `flavor_profile`/`architecture_flavors`, `dominant_bulk_class`, `dominant_production_ubiquity` (the record already permits >6 fields). - **Amended 2026-05-31 ([D-237](#d-237) — authored specialization layer):** the read set gains `economic_specialization` and `cultural_specialization` (both new columns on `system_economy`). `dominant_faction` (field 4) is now sourced from authored values on `system_factions` where present, heuristic fallback otherwise. `economic_specialization` is the upstream source of `dominant_bulk_class`/`dominant_production_ubiquity` (resolved via `specialization_vocabulary`, D-233 re-amendment); `cultural_specialization` feeds the D-232 template pool. Still bounded — three authored fields, all NULL-safe with deterministic fallbacks. ### D-200: Three-Tier Execution Model (Build-Time / Runtime-Background / Runtime-On-Demand) - **Date:** 2026-05-01 - **Decision:** The generation pipeline operates at three distinct execution tiers with no cross-tier mutation: 1. **Build-time (Python pipeline):** Runs `make regen-db`. Produces `systems.db` tables including `atlas_city_names`, `atlas_province_boundaries`, `body_radius_km`. Output is a static artifact committed to the repo. Never runs during gameplay. *(Amended T-963, D-202: `atlas_body_heightmaps` is no longer produced — elevation is now a per-body 16-bit `heightmap.png` file, not a DB table.)* 2. **Runtime-background (Rayon thread pool, D-206):** Triggered by content-spidering events (player approaches a system, NPC names a location, news ticker references a place). Runs D8 drainage analysis (D-208), attractor extraction (D-209), settlement placement, and Phase 1 district skeleton generation. Output goes into `BodyWorldState` cache (D-203). Transparent to main tick thread. 3. **Runtime-on-demand (main tick thread):** Triggered when the player crosses a chunk boundary. Runs Phase 2 chunk fill for the approaching chunk. Must complete within 5ms. Reads from `BodyWorldState` cache (always populated before this tier runs). - **Tier boundary rules:** - Build-time outputs are read-only at runtime. - Runtime-background tasks read from `systems.db` and write to `BodyWorldState` only. - Runtime-on-demand reads from `BodyWorldState` and writes to the active ECS world (chunk tile data, NPC spawns). - No tier may write to a higher tier's outputs. No circular dependencies. - `CityGenerationContext` struct (see below) is the data contract between tiers 1→2. ```rust struct CityGenerationContext { city_id: u64, political_archetype: PoliticalArchetype, prosperity_baseline: f32, surrounding_biome: SettingType, road_entry_directions: Vec, // compass octants (0–7) footprint_radius_km: f32, founding_orientation: FoundingOrientation, world_tier: WorldTier, } ``` - **Rationale:** Three tiers with explicit boundaries eliminates the "where does this code run?" question. Build-time is deterministic and committable. Runtime-background is parallelizable. Runtime-on-demand has strict latency budgets. Cross-tier mutation would create race conditions between the Rayon thread pool and the main tick thread. - **Ticket:** T-915 - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-026 (simulation tiers), D-194 (district mix), D-203 (BodyWorldState), D-206 (background generation queue), tyre-sw1r3.md Layer 5/6 architecture ### D-201: Spatial Hierarchy — Eight Tiers with Locked Dimensions - **Date:** 2026-05-01 - **Decision:** The generation pipeline has eight spatial tiers from galaxy to tile. Dimensions are locked and cannot be changed without amending this decision: | Tier | Name | Dimensions | Purpose | |------|------|------------|---------| | 1 | Galaxy | 300 systems | Galaxy graph, gate topology, cultural corridors | | 2 | System | — | Orbital mechanics, body catalog | | 3 | Body | ~512×256 pixels (equirectangular heightmap) | Planetary topography, climate zones | | 4 | Province | ~50–500km | Province boundaries (watershed-derived, D-205), biome zones | | 5 | Settlement | ~1–30km radius | City footprint, district layout | | 6 | District | 2048×2048 tiles (2048m, 4.19 km²) | 4×4 quarters — large urban division (D-222) | | 7 | Quarter | 512×512 tiles (512m, 0.262 km²) | 4×4 blocks — settlement footprint cell, Phase 1 skeleton grid (D-222) | | 8 | Block | 128×128 tiles (128m) | Generator planning unit, 2×2 chunks (D-222) | | 9 | Chunk | 64×64 tiles (64m) | Streaming/serialization unit (D-222) | - Tiers 6–9 (District → Chunk) are the sub-settlement spatial hierarchy, canonical in [D-222](#d-222) — renamed/resized from the original D-094 ladder (the old 512m "District" is now the Quarter; District is now 2048m), at the Tile = 1m / Subtile = 0.5m scale of D-220. This decision formalizes Tiers 1–5 with equivalent lock status. - Tier 3 heightmap resolution (512×256 equirectangular working grid; 1024×512 PNG) is the canonical format. Deviation requires amending D-191. **Amended (T-963, D-202):** the canonical *stored* heightmap is now a per-body 16-bit grayscale `heightmap.png` at **1024×512** carrying native elevation (the prior PNG was a 1024×512 RGB *relief*, now renamed `reliefmap.png`). PNG dimensions are unchanged (1024×512); Layer 1 downsamples to the 512×256 working grid. This amendment is the explicit deviation gate being satisfied — format/content changed, resolution preserved. - Tier 4 province boundaries are pre-computed at build-time and stored in `atlas_province_boundaries` (D-205). They are not re-computed at runtime. - **Amended 2026-06-14 ([D-243](#d-243)):** Tier 4 renamed **Region → Province** — it always *was* the watershed/political tier (D-205), and the name "Region" is now reserved for [D-243](#d-243)'s ~205 km metric containment cell (the top hard block of the absolute scale ladder + the climate/weather lockdown scale). Province is an **overlay** painted across regions, not a containment rung; the two sit at overlapping scales but are different kinds (irregular lore-bearing boundary vs fixed metric grid cell). [D-243](#d-243) owns the metric containment ladder (voxel→chunk→block→quarter→district→region); Tiers 1–5 here (Galaxy/System/Body/Province/Settlement) are organizational scopes/overlays, not metric rungs, and the ~1 km RegionProfile scale this record's tier-4 collided with is dropped (its carrier role moves onto the district, D-243 §5). - The `SettingType` enum on `DistrictSkeleton` is the interface between Tier 5 (settlement planning) and the skeleton cell (the 512m Quarter, Tier 7 — `DistrictSkeleton` is pending rename to match D-222). - **Rationale:** Locking spatial dimensions prevents the generative layers from drifting in incompatible directions. The heightmap pipeline, atlas pipeline, and district generator all assume these dimensions and would need coordinated migration if they changed. Formalization prevents silent per-system variation. - **Ticket:** T-912 (WorldTier enum), T-913 (SettlementClass) - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-094 (district hierarchy — Tiers 6–8), D-191 (atlas pipeline — Tier 3), D-205 (province boundaries — Tier 4), D-208 (D8 drainage — Tier 3 analysis) ### D-202: Heightmap BLOB Storage Schema (atlas_body_heightmaps) - **Date:** 2026-05-01 - **Decision:** Heightmap elevation data is stored in `systems.db` as a BLOB in the `atlas_body_heightmaps` table. Schema: ```sql CREATE TABLE atlas_body_heightmaps ( body_id INTEGER PRIMARY KEY REFERENCES bodies(id), width INTEGER NOT NULL, -- pixel columns (canonical: 512) height INTEGER NOT NULL, -- pixel rows (canonical: 256) data BLOB NOT NULL, -- float32 little-endian, row-major, width×height floats sea_level REAL NOT NULL DEFAULT 0.0, imported_at TEXT NOT NULL DEFAULT (datetime('now')) ); ``` - `data` is a `float32` little-endian BLOB. Size: `width × height × 4` bytes. Canonical: 512×256×4 = ~512KB per body. - Values are normalized elevation in [0.0, 1.0]. `sea_level` is the fraction below which terrain is underwater (default 0.0 = no ocean, overridden per body). - The Rust loader reads the BLOB via `bytemuck::cast_slice::()` after fetching from SQLite. No endian conversion needed on LE-native systems; the pipeline stores LE explicitly. - Only inhabited bodies receive heightmap rows at build-time. Uninhabited bodies are generated on-demand (runtime-background tier, D-200). - This table is populated by the `import_heightmaps` build-time step in the asset pipeline (D-191 §9 pipeline order). It is read-only at runtime. - **Amendment (2026-05-23, [T-963](#)):** the DB-BLOB store is **superseded** by a per-body **file-based 16-bit grayscale `heightmap.png`** stored next to the body's other assets (path from `bodies.terrain_reference`). Rationale for the change: raw float grids committed inside a binary `systems.db` are the exact binary-merge-conflict trap the asset-pipeline rule warns against, and ~512KB×N bloats the DB; a per-body file matches D-203's on-demand model and keeps `systems.db` lean. Concretely: - **Naming fix:** the existing color hypsometric render (today's `heightmap.png`, 1024×512 RGB) is renamed **`reliefmap.png`** — it is a relief visualization, not elevation. Display-only. - **Canonical elevation:** a new **`heightmap.png`** = 16-bit grayscale (luminance = normalized elevation), **1024×512** (2× per axis / 4× the cells of the old 512×256 sim grid — the PNG dimensions are unchanged from the prior canonical 1024×512 in D-201; only the *content* changed from an RGB relief to native 16-bit elevation). 1024×512 bounds install size (~190 MB across 267 inhabited bodies) while sub-pixel detail is synthesized by the lower cascade layers. Single source of truth — the reliefmap and all computed geography derive from it, so it is bit-identical/deterministic by construction. - **Multi-resolution:** the stored heightmap is high-res for the lower layers (region/block/tile sample local detail); **Layer 1** (continental drainage/basins/mountain-ranges) calls `BodyHeightmap::downsample` to the `GRID_W×GRID_H = 512×256` working resolution first, decoupling continental compute cost (~45ms) from stored resolution. - **Rust loader:** `heightmap.rs::load_heightmap_png` reads the 16-bit grayscale PNG (via the `png` crate), normalizes to f32 [0,1]; rejects RGB (a reliefmap can't be misread as elevation). `sea_level` is stored **in the PNG** as a `tEXt` chunk (the heightmap is self-describing), with a caller-supplied default as fallback. - **`atlas_body_heightmaps` is dropped**; `import_heightmaps.py` writes the PNG file instead of a DB row. The bake runs in the content pipeline (numpy/scipy) once; the runtime cascade is pure Rust loading the file. - **Implementation status (T-963):** Consumer done — `heightmap.rs::load_heightmap_png` reads the 16-bit grayscale PNG + `sea_level` tEXt chunk, rejects RGB, downsamples for Layer 1. Producer done — `import_heightmaps.py` is the bake (rename legacy `heightmap.png`→`reliefmap.png` for all bodies incl. Sol; for non-Sol inhabited bodies write a fresh clean `reliefmap.png` + 16-bit `heightmap.png` from `simulate()` at the bumped 1024×512 grid). `atlas_body_heightmaps` dropped via MIGRATION_SQL + removed from `systems-schema.sql`. Godot client (`atlas_viewer.gd`) loads `reliefmap.png` for display. Sim determinism guarded by `test_sim_determinism.py`. - **Rationale:** Storing heightmaps in `systems.db` keeps the DB as the single source of truth for all generation inputs, avoids a separate file-fetching path in the Rust server, and allows the pre-push hook (asset pipeline rules) to detect stale heightmap data. The float32 LE layout matches what NumPy and PIL produce natively, minimizing conversion overhead in the Python pipeline. *(Superseded by the T-963 amendment above — the file-based model won out because the DB-as-single-source goal conflicts with binary-merge-conflict avoidance and DB size; a per-body committed PNG is itself a queryable, diffable-by-render asset.)* - **Ticket:** T-901 (schema), T-906 (import), T-916 (Rust loader) - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-191 (atlas pipeline — heightmap source), D-203 (BodyWorldState — consumer), D-208 (D8 drainage — reads this table) ### D-203: BodyWorldState Bevy Resource with LRU Cache - **Date:** 2026-05-01 - **Decision:** `BodyWorldState` is a Bevy `Resource` holding the Layer 1–2 output for each recently-accessed planetary body. It functions as an LRU (Least Recently Used) cache: - **Cache capacity:** 50 bodies. - **Memory budget:** ~5MB total (50 bodies × ~100KB per entry average). A single body's Layer 1–2 data includes: processed heightmap (float32 grid, ~512KB pre-downsampled to ~8KB working resolution), river network (`RiverNetwork` struct: river cells, confluences, mouths), drainage basin polygons, attractor list, province boundary references. - **Eviction policy:** On cache overflow, evict the body with the oldest `last_accessed` timestamp. Bodies that are the current player location or adjacent-system neighbors are pinned (not evicted). - **Population:** The runtime-background tier (D-200) populates cache entries via Rayon tasks. Main thread reads are always from the cache; main thread code must never perform blocking DB reads for heightmap data. - **Struct:** ```rust struct BodyWorldState { body_id: u64, heightmap: Vec, // downsampled working grid river_network: RiverNetwork, // D-208 output drainage_basins: Vec, attractors: Vec, // D-195 types last_accessed: SimTick, } ``` - The resource is initialized empty and populated on demand. Accessing a body not in the cache triggers a background generation task (D-206). - **Rationale:** The D8 drainage analysis (D-208) and attractor extraction (D-209) are expensive (target: ~50ms/body). Running them on the main tick thread would cause frame drops. The LRU cache ensures the main thread only reads pre-computed data. 50-body capacity covers the typical gameplay scenario (player in one system, neighboring system pre-cached) with margin. - **Ticket:** T-917 - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-200 (three-tier execution model), D-202 (heightmap BLOB — input), D-206 (background generation queue — populates cache), D-208 (D8 drainage — produces river network) ### D-204: body_radius_km Column on bodies Table - **Date:** 2026-05-01 - **Decision:** A `body_radius_km REAL` column is added to the `bodies` table in `systems.db`. This value is the mean radius of the planetary body in kilometers, used to: - Compute `area_count` (number of districts a settlement can contain, scales with surface area) - Convert province boundary pixel coordinates to real-world km distances - Derive the `footprint_radius_km` field on `CityGenerationContext` (D-200) - Schema change: `ALTER TABLE bodies ADD COLUMN body_radius_km REAL` (nullable, populated by import step) - **Fallback derivation** (applied when `body_radius_km IS NULL`): `planet_class` lookup table with canonical radii: - `super_earth`: 8,000 km - `earth_like`: 6,371 km - `sub_earth`: 4,500 km - `ocean_world`: 6,500 km - `arid`: 5,800 km - `ice_world`: 3,000 km - `gas_giant`: 50,000 km (no settlements) - `moon`: 1,737 km - `other` / unknown: 6,371 km (Earth default) - Fallback is applied at query time, not stored back. The column remains NULL until authoritative data is available. - **Rationale:** Surface area scales with radius squared; a body twice Earth's radius has four times the potential settlement density. Without this field the generator must use a flat default for all planets, producing physically implausible city counts on super-earths and moons alike. The fallback ensures the generator works before all bodies have explicit radius data. - **Ticket:** T-905 (schema), T-910 (populate from planet_class fallback) - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-191 (atlas pipeline — body catalog), D-200 (CityGenerationContext — footprint_radius_km) ### D-205: Province Boundary Pre-Computation (atlas_province_boundaries) - **Date:** 2026-05-01 - **Decision:** Province boundaries (drainage basin divides) are pre-computed at build time from the D8 drainage analysis (D-208) and stored in `atlas_province_boundaries`: ```sql CREATE TABLE atlas_province_boundaries ( body_id INTEGER NOT NULL REFERENCES bodies(id), basin_id INTEGER NOT NULL, path TEXT NOT NULL, -- JSON array [[row, col], ...] pixel-space polyline area_pct REAL NOT NULL, -- fraction of body surface area in this basin PRIMARY KEY (body_id, basin_id) ); ``` - Boundaries are stored as pixel-space polylines in the same `[row, col]` convention as `markers.json` (D-191 §8 canonical format). - `area_pct` is the fraction of the body's total surface area contained within this drainage basin. - Province boundaries are the basis for district-level political zoning and cultural corridor assignment at Tier 4 (Region) in D-201. - **Province count target:** 4–12 provinces per inhabited body, derived naturally from watershed analysis. Bodies with less topographic relief (plains worlds, ocean worlds) produce fewer, larger provinces. - **At runtime:** Province boundaries are read from `atlas_province_boundaries` at generation dispatch time and cached in `BodyWorldState` as `drainage_basins` (D-203). They are not re-computed at runtime. - **Rationale:** Province boundaries define the cultural geography of a world — the mountain ranges and river systems that separated civilizations and produced distinct regional identities. Pre-computing them at build time keeps the runtime-background tier focused on city placement and district generation rather than watershed analysis. Storing as polylines (not rasterized masks) keeps the table compact and human-readable. - **Ticket:** T-904 (schema), T-907 (populate from watershed analysis) - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-191 (atlas pipeline), D-201 (spatial hierarchy — Tier 4 Region), D-203 (BodyWorldState — caches province data), D-208 (D8 drainage — source of basin divides) ### D-206: Background Generation Priority Queue and Rayon Thread Infrastructure - **Date:** 2026-05-01 - **Decision:** All non-urgent generator work runs through a prioritized Rayon thread pool: - **Thread count:** `available_parallelism - 2`, minimum 1. Reserves 2 cores for the main tick thread and Bevy scheduler. - **Priority queue:** Four levels: `Immediate` (player will arrive within 1 game-minute), `High` (player will arrive within 5 minutes), `Medium` (player is in the same system), `Low` (player has seen or heard of this location via NPC or news). Work items at higher priority pre-empt lower-priority items. - **Work item types:** `AnalyzeBody(body_id)` (D8 drainage + attractor extraction), `GenerateSkeleton(city_id, context)` (Phase 1 DistrictSkeleton), `FillChunk(district_id, block_pos)` (Phase 2 chunk fill for pre-loading). - **Event-driven pre-generation:** A `SystemNameIndex` (Aho-Corasick automaton over all body/system names from `systems.db`) scans NPC dialogue output and news ticker text. When a scan match hits, the referenced body is queued at `Low` priority if not already cached. This is the mechanism by which "NPC mentions a place → player travels there → world is already generated on arrival." - **Completion notification:** Completed tasks send a `GenerationComplete` event to the main tick thread via a `crossbeam` channel. The main thread drains this channel once per tick. - **Rationale:** The Rayon thread pool handles the D-200 runtime-background tier. The priority queue prevents low-priority speculation from blocking urgent work (player approaching). The Aho-Corasick name index enables cheap always-on scanning — NPC dialogue is low-bandwidth enough that scanning every output line has negligible cost. Pre-generation triggered by narrative content (NPC mentions a place) is the mechanism for making the world feel pre-existing rather than loading-on-demand. - **Ticket:** T-924 (background queue), T-926 (SystemNameIndex) - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-200 (three-tier execution model — runtime-background tier), D-203 (BodyWorldState — output of background tasks), D-208 (D8 drainage — enqueued as AnalyzeBody) ### D-207: Fully Generative Placement — markers.json Stripped to Topographic Features - **Date:** 2026-05-01 - **Amendment (2026-05-22, [D-223](#d-223)):** superseded in part. markers.json no longer retains topographic-feature *geometry* — it is reduced to **names only** (a flavored name pool). River/mountain positions are derived from the heightmap + drainage (D-208); settlement positions come from the economic sim + placement (D-211). The `reserved=1` pinning and the 6 hand-authored templates (Lendel, Edict, Vuurkloof, Røros, Cairnside, Estrade) are removed — their bodies remain as ordinary named bodies, only the authored machinery is gone. See D-223. - **Amendment (2026-07-16, [D-242](#d-242)):** the `corp_id` column and the "corp HQ cross-reference" (T-909, `populate_atlas_city_names_corps`) this record's `Decision` section describes below are **removed**. That insert path (kept alive through D-223's amendment above) had no `UNIQUE(body_id, name)` and produced duplicate co-named "cities" — one `atlas_city_names` row per corp HQ (e.g. 10 `Groombridge` rows on GJ380c). The corp↔settlement relationship now lives with the corp (`corporations.headquarters_body`/`headquarters_city_id`), not as a row in the city pool. `atlas_city_names.corp_id` is left in the schema (additive-safe) but is permanently `NULL` going forward — see D-242. - **Decision:** The `atlas_city_names` table replaces the authored city positions in `markers.json`. Going forward, `markers.json` files contain only topographic features (rivers, oceans, mountain ranges — per D-191 §8 canonical format). City positions, road networks, and rail networks are NOT authored in `markers.json`; they are generated from the terrain data and stored in `atlas_city_names` and derived tables. ```sql CREATE TABLE atlas_city_names ( id INTEGER PRIMARY KEY, body_id INTEGER NOT NULL REFERENCES bodies(id), name TEXT NOT NULL, economic_role TEXT NOT NULL, population INTEGER NOT NULL, corp_id INTEGER REFERENCES corporations(id), -- nullable, corp HQ if applicable reserved INTEGER NOT NULL DEFAULT 0, -- 1 = reserved for authored scenario use kind TEXT NOT NULL DEFAULT 'city' -- 'capital' | 'city' ); ``` - `name` and position in the markers.json come from different sources: position is generated by the city placement algorithm; name is either authored (wiki), LLM-generated (Gemma 2 naming pipeline), or reserved (corp HQ name). The split allows position generation and naming to run independently. - `corp_id` links to the `corporations` table when a city is a corporation's headquarters or major hub city. - `reserved = 1` rows are scenario-specific cities that must not be relocated by the generation algorithm; the generator places other cities around them. - **`markers.json` authored city data** (hand-written city center positions in the 6 hand-authored templates: Lendel, Edict, Vuurkloof, Røros, Cairnside, Estrade) is migrated to `atlas_city_names` and treated as `reserved = 1` rows. The markers.json files for these templates then have their city arrays cleared. - **Rationale:** Authored city positions in markers.json created a split between hand-authored content and procedurally generated content that was impossible to query, diff, or validate consistently. Moving city identity to a table allows: SQL joins against economic data, corp HQ cross-references, scenario reservations, and attractor-matching validation. The topographic features (rivers, mountains) remain in JSON because they are polygon/polyline geometry better suited to JSON than relational rows. - **Ticket:** T-902 (schema), T-908 (populate from wiki), T-909 (corp HQ cross-reference) - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-191 (atlas pipeline — markers.json canonical format), D-196 (SettlementClass — column in atlas_city_names), D-200 (CityGenerationContext — reads from this table) ### D-208: D8 Priority-Flood Drainage Routing — Layer 1 Empty World - **Date:** 2026-05-01 - **Decision:** Drainage routing (the computation of flow direction, flow accumulation, and river network extraction) uses the **D8 priority-flood** algorithm on the body's float32 heightmap. This is the first Layer 1 computation run on a body before any city placement or attractor extraction. - **Algorithm:** D8 assigns each cell's flow direction to one of 8 neighbors based on the steepest descent. Priority-flood fills depression cells before routing to avoid spurious sinks. Flow accumulation is the count of upstream cells draining through each cell. - **River threshold:** A cell is classified as a river cell when `flow_accumulation > 200`. This threshold produces river networks of realistic density on the 512×256 Layer-1 working grid (D8 runs at 512×256, downsampled from the 1024×512 stored heightmap per D-202 amended T-963). - **Outputs** stored in `BodyWorldState.river_network`: - `river_cells: Vec<(u16, u16)>` — pixel positions of all river cells - `confluences: Vec<(u16, u16)>` — positions where two or more rivers merge - `mouths: Vec<(u16, u16)>` — positions where rivers reach sea level or the heightmap edge - **Province/basin output:** Cells that divide adjacent drainage basins become province boundary candidates (D-205). Boundaries are traced as polylines after flow accumulation is complete. - **Performance target:** ~50ms per body on a single Rayon thread for canonical 512×256 resolution. - **Determinism:** Integer-only arithmetic throughout. No f32 in the priority-flood comparisons (use integer-scaled elevation). D-010 compliant. - **Rationale:** D8 is the standard GIS drainage routing algorithm and produces the river networks that drive attractor scoring (river mouths, confluences = high-value `RiverMouth` attractors). The flow accumulation threshold of 200 was chosen empirically against the Lendel heightmap to produce ~8–15 named rivers per inhabited body — enough for cultural geography without over-fragmenting the landscape. - **Ticket:** T-918 - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-195 (attractor types — river mouth is highest-scoring), D-203 (BodyWorldState — output stored here), D-205 (province boundaries — derived from drainage divides), D-209 (feature tag extraction — reads river network) ### D-209: Geographic Feature Tag Extraction (7 Settlement Attractor Tags) - **Date:** 2026-05-01 - **Decision:** After D8 drainage analysis, 7 `AttractorType` tags are extracted from the heightmap + river network and stored as `Vec` in `BodyWorldState`. Each attractor has a position `[row, col]` and a `strength: f32` (0.0–1.0) derived from local terrain quality. - **Extraction rules per type:** - `RiverMouth`: cells in `river_network.mouths`. Strength = `flow_accumulation[cell] / max_flow_accumulation` (normalized). Always high-value. - `CoastalAccess`: cells within 3 pixels of a sea/ocean polygon (from `oceans[]` in markers.json), not already `RiverMouth`. Strength = 0.6 baseline + coast length bonus. - `RiverCrossing`: cells at confluences or where a river crosses a topographic saddle. Strength = `flow_accumulation / max_flow_accumulation × 0.7`. - `ValleyFloor`: local elevation minima in non-river cells with positive habitability score (slope < 5°, elevation 10–60% of range). Strength = habitability score. - `PassEntrance`: local saddle points between adjacent drainage basins. Strength = inverse of elevation percentile (lower passes score higher). - `LakeShore`: cells adjacent to `lake` polygons in markers.json. Strength = 0.5 baseline. - `PlainCenter`: cells in flat terrain (slope < 2°) away from all other attractors. Strength = habitability score × 0.4. - Sub-biome classification (vegetation, aridity, temperature zones) is derived in parallel and stored as `SubBiomeVariant` on the attractor for use by the ZonePalette modifier system (D-101). - **Rationale:** The 7 attractor types cover the terrain features that historically determine city placement. Their extraction from the heightmap is deterministic and cheap given the D8 analysis is already complete. The strength normalization ensures attractor scores are comparable across bodies with different elevation ranges. - **Ticket:** T-925 (types), T-919 (matching pipeline that consumes these) - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-195 (CompatibilityMatrix — attractor types), D-203 (BodyWorldState — storage), D-208 (D8 drainage — prerequisite), D-211 (attractor-matching pipeline — consumer) ### D-210: Sub-Biome Variant Classification and terrain_modification_cost - **Date:** 2026-05-01 - **Decision:** Each `GeographicAttractor` (D-209) carries a `sub_biome: SubBiomeVariant` tag that classifies the local terrain more finely than the top-level `SettingType`. This drives two systems: ZonePalette modifier selection (which visual variant to use) and `terrain_modification_cost` (how expensive it is to build infrastructure at this location). - `SubBiomeVariant` values: `TropicalWet`, `TemperateForest`, `TemperateGrassland`, `BorealForest`, `Tundra`, `Desert`, `Savanna`, `Alpine`, `Wetland`, `CoastalLowland`, `Volcanic`. - `terrain_modification_cost: f32` (1.0 = baseline, higher = more expensive): derived from sub-biome + local slope. Flat grassland = 1.0. Volcanic = 4.5. Wetland = 3.2. Alpine = 3.8. Coastal lowland = 1.4. Used by the attractor-matching pipeline (D-211) to penalize high-cost terrain for economically marginal cities. - Sub-biome classification uses: elevation percentile (of body total), local slope, moisture proxy (distance to nearest river mouth or coast), and temperature proxy (latitude of the equirectangular pixel). - Sub-biome data is stored in `BodyWorldState` alongside the attractors; it is not a separate DB table. - **Rationale:** Two cities on coastal terrain feel different when one is a tropical lowland port and the other is a cold Nordic fjord. Sub-biome tags enable the ZonePalette to select the correct visual register (T6 beach/coastal with tropical modifier vs T7 mountain/high with coastal modifier). The `terrain_modification_cost` gives the generator a principled reason to prefer some attractor positions over others for lower-prosperity cities. - **Ticket:** T-919 (attractor matching — uses terrain_modification_cost) - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-101 (ZonePalette modifier system — consumes sub_biome), D-195 (attractor types), D-209 (feature tag extraction — assigns sub_biome) ### D-211: Attractor-Matching Five-Phase Pipeline for Settlement Placement - **Date:** 2026-05-01 - **Decision:** Given a body's `Vec` and a set of cities from `atlas_city_names`, settlement placement runs a five-phase matching pipeline: 1. **Score matrix build:** Compute a `city_count × attractor_count` score matrix. Each cell = `CompatibilityMatrix[economic_role][attractor_type] × attractor.strength × (1.0 / terrain_modification_cost)`. 2. **Tier A greedy assignment:** For each city with `SettlementClass::NameLocked` or population ≥ 1,000,000, assign the highest-scoring unoccupied attractor using greedy selection. These cities must be placed first to anchor the spatial layout. 3. **Hungarian algorithm for Tier B+C:** Apply the Hungarian algorithm to the remaining cities (population 50,000–999,999) and remaining attractors. Produces optimal global assignment maximizing total score. 4. **Synthetic attractor overflow:** Cities that cannot be matched to a real attractor (attractor pool exhausted) receive a synthetic `PlainCenter` attractor generated at a position that respects minimum city spacing (15 pixels minimum on 512×256 grid = ~50km minimum separation). 5. **Name fulfillment check:** After placement, verify that all `atlas_city_names` entries for this body have been assigned a position. Log a warning for any unplaced city. - **Two-tier mismatch flagging:** If a matched city-attractor pair has score < 0.35, log a `WARNING` (below expected quality). If score < 0.15, log an `ERROR` and flag for manual review. Generation proceeds in both cases; the flags are for content auditing, not hard blockers. - **Output:** `Vec` written to `atlas_city_positions` at build time. - **Rationale:** Greedy-first for large/locked cities ensures anchor cities (capitals, corp HQs, wiki-named cities) are placed at terrain features that match their lore role. Hungarian for medium cities finds the globally optimal assignment, not just locally optimal. Synthetic overflow prevents the algorithm from failing on bodies where city count exceeds natural attractor count (dense, flat worlds). The two-tier warning system enables content QA without blocking generation. - **Ticket:** T-919, T-925 - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-195 (CompatibilityMatrix), D-207 (atlas_city_names), D-209 (GeographicAttractor — input), D-210 (terrain_modification_cost — input) ### D-212: TerritorialStatus Priority-Ordered Derivation Algorithm - **Date:** 2026-05-01 - **Decision:** Each `Province` (watershed-derived drainage basin, D-205) receives a `TerritorialStatus` value derived by priority-ordered classification at generation time: ```rust enum TerritorialStatus { CommissionControlled, // Commission faction_influence ≥ 0.6 in this province CorpTerritory, // Single corporation faction_influence ≥ 0.5 ContestedZone, // Two or more factions each ≥ 0.3, no dominant faction FrontierUnclaimed, // No faction with influence ≥ 0.2 IndigenousHeld, // Cultural corridor has indigenous autonomy flag Derelict, // population_density < 0.01 AND no faction ≥ 0.1 } ``` - Classification applies checks in priority order: `CommissionControlled` checked first, `Derelict` last. The first condition that is true sets the status. - `placed_at_generation: bool` flag on `Province` distinguishes classification at build time (true) from runtime re-classification during simulation (false). Build-time status is the starting state; simulation can change it, and the flag ensures the original classification is recoverable for reset/new-game scenarios. - Faction influence values are read from `systems.db` (economics tables) at build time using the same D-199 economic read pattern. - **Rationale:** Territory status is a high-level descriptor visible to the player on the Atlas overlay (D-191 §7, political zones overlay). It must be derivable from the generation inputs without runtime simulation state. The priority-ordered algorithm ensures clear, predictable classification — no ambiguous provinces. The `placed_at_generation` flag enables the game to show "how this province was at settlement time" vs. "how it is now." - **Amended 2026-06-05 (T-956 — implementation):** two changes from the original spec. 1. **New variant `AutonomistHeld`** added between `FrontierUnclaimed` and `IndigenousHeld`. The original four control buckets (Commission/Corp/Contested/Frontier) predate the richer faction canon (`wiki/factions/`): the **Compact of Westphalia** is a self-governing autonomist bloc that rejects Concord Assembly authority — it governs its systems firmly, so it is neither `CommissionControlled` (it is the Assembly's *rival*), nor `FrontierUnclaimed` (it is *not* ungoverned), nor locally `ContestedZone` (the Compact is locally dominant). `AutonomistHeld` is its bucket, and gives the Compact its own colour on the political-zones overlay. 2. **Derivation source.** The numeric per-faction `faction_influence` thresholds the original record specifies are **not present in the data** — only a single authored `dominant_faction` per system exists (D-237's 8-value vocabulary). So the implementation maps `dominant_faction → TerritorialStatus` instead (`attractor_matching::territorial_status_from_faction`), grounded in faction canon: `concord_assembly`/`veil_institute` → `CommissionControlled` (the Assembly is the Reach's central government; the Veil Institute is Assembly-funded and -aligned); `syndic_dominant` → `CorpTerritory`; `compact`/`compact_sympathetic` → `AutonomistHeld`; `disputed`/`mixed` → `ContestedZone`; `independent`/NULL/unknown → `FrontierUnclaimed`. `IndigenousHeld` and `Derelict` remain unreachable from `dominant_faction` alone (they need the cultural-corridor autonomy flag / population density) — deferred. `dominant_faction` is system-level, so status is uniform across a body's provinces for now; it is still stored per-basin on `DrainageBasin.territorial_status` (forward-compatible for per-province faction data). `placed_at_generation` is not yet modelled (no runtime re-classification exists yet). - **Ticket:** T-921, T-956 (implementation + amendment) - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-191 (atlas overlay — political zones), D-199 (economic read set), D-205 (Province — this status is a field on it), [D-237](#d-237) (authored `dominant_faction` source), D-214 (PoliticalArchetype — consumes this) ### D-213: FoundingOrientation Enum and Spatial Grid Rotation - **Date:** 2026-05-01 - **Decision:** `FoundingOrientation` describes the primary spatial axis of a city's original street grid, derived from the terrain feature that anchored the founding settlement. It controls the rotation of the district grid skeleton. ```rust enum FoundingOrientation { Coastal { facing_degrees: u16 }, // street grid perpendicular to coastline RiverAligned { bearing_degrees: u16 }, // street grid parallel to founding river TerrainFollowing, // grid rotated to follow local contours Cardinal, // grid aligned to N/S/E/W (commission-planned) Free { bearing_degrees: u16 }, // arbitrary bearing (pioneer settlements) } ``` - `facing_degrees` and `bearing_degrees` are integer degrees 0–359 (0 = North, clockwise). Integer to preserve D-010 determinism. - The founding orientation is derived from the matched attractor type (D-211): `RiverMouth` → `Coastal`; `RiverAligned`; `CoastalAccess` → `Coastal`; `ValleyFloor` → `TerrainFollowing`; `PlainCenter` + Commission-controlled province → `Cardinal`; `PlainCenter` + other → `Free`. - The district skeleton generator (Phase 1) applies `FoundingOrientation` as the base rotation for the outermost district ring. Interior districts inherit the orientation unless overridden by a `PoliticalArchetype` modifier. - **Hard constraint:** Maximum ±45° deviation from the parent orientation per district (same limit as D-096 `BlockPlacement.rotation_steps`). Beyond ±45°, tile-based pathfinding produces movement artifacts. - **Rationale:** Street grids reflect the terrain and founding logic of the original settlement. Roman camps faced cardinal directions. River towns align with the river. Coastal cities face the water. Encoding this as a named enum rather than a raw angle makes the orientation legible in the data model and debuggable during generation. - **Amendment (T-1076 §4, 2026-07-16 — `RailHeadFacing` variant added; the D-242-gated follow-on):** the enum gains a sixth variant, `RailHeadFacing { bearing_degrees: u16 }` — the street grid faces the settlement's rail head. Unlike the five attractor-derived variants, it is **assigned post-placement by the Layer-2 road-graph pass** (`road_graph::assign_railhead_orientations`, run in the cascade immediately after `build_road_graph`): every settlement that is a **high-connectivity junction** (`RoadGraph::high_connectivity_junctions()`, incident-edge degree ≥ 3 — the criterion from this record's workshop source, paula-round3.md) has its attractor-derived orientation **overridden** to `RailHeadFacing`, with `bearing_degrees` the **octant-snapped** compass bearing (0 = N, clockwise; one of the eight 45° octants — integer-only math, D-010; the diagonal band is the `|minor|·2 > |major|` integer approximation, sound because the consumer snaps octants to quarter-edges anyway) from the settlement toward the **dominant incident edge** (longest `length_cells`, ties to the lowest edge index) — the direction the freight frontage faces. The Layer-4 skeleton consumes it via `skeleton_gen::railhead_edge` — the same octant→cardinal-edge snap as `coastal_edge`, feeding the same D-234b flush-frontage machinery: blocks on the rail-facing quarter edge present flush to the rail head the way waterfront blocks present flush to the quay (coastal wins if a settlement somehow carries both; variants are exclusive by construction). Deterministic: a pure function of the (already deterministic) road graph. - **Ticket:** T-914; T-1076 §4 (RailHeadFacing) - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-096 (DistrictLayoutMode — inherits orientation), D-211 (attractor-matching — derives orientation), D-214 (PoliticalArchetype — may override orientation), D-215 (spatial arrangement patterns — uses orientation), [D-242](#d-242) (the road-graph hub refinement this variant shipped under; T-1076) ### D-214: PoliticalArchetype Enum and Settlement Spatial Character - **Date:** 2026-05-01 - **Decision:** `PoliticalArchetype` classifies a settlement's dominant power structure and its physical expression in district layout: ```rust enum PoliticalArchetype { Commission, // Top-down Commission planning; rectilinear, institutional core Corporate, // Corp-dominated; commercial density, restricted zones, campus blocks Pioneer, // Self-organized; organic growth, mixed use, ad-hoc infrastructure Military, // Garrison or fortification origin; defensible geometry, restricted perimeter Academic, // University or research origin; campus-quad structure, green space Industrial, // Factory-first; large-footprint industrial blocks, worker residential rings } ``` - `PoliticalArchetype` is derived at generation time from `TerritorialStatus` (D-212) + `economic_role`: `CommissionControlled` province → `Commission`; `CorpTerritory` → `Corporate`; `FrontierUnclaimed` → `Pioneer`; military economic role → `Military`; research economic role → `Academic`; manufacturing + extraction → `Industrial`. - When multiple signals conflict (e.g., Commission-controlled manufacturing hub), `TerritorialStatus` takes precedence over `economic_role` for archetype derivation. - **Spatial effect on district mix:** See D-194. Each archetype applies weight multipliers to district type selection. - **`AttractorAssignment` disambiguation:** `OrganicGrowth` (a `DistrictType` value and also an `EraCause` value) is always unambiguous in context. On `DistrictType`, it means the district grew without a planning mandate. As `EraCause`, it means the era tag was acquired through organic settlement expansion rather than a discrete historical event. Both usages are permitted; the type system distinguishes them. - **Rationale:** Power structure should be legible in a city's spatial form without the player reading a wiki entry. Commission cities look different from Corporate cities look different from Pioneer cities — not just in palette, but in street geometry, district type distribution, and building scale. Encoding this as a named enum ensures the distinction is consistent across all generation code. - **Amended 2026-05-31 ([D-237](#d-237) — authored specialization layer):** for named systems `dominant_faction` (a D-214 derivation input via `TerritorialStatus`/D-199) is now an **authored** value (8-value vocabulary: `concord_assembly | compact | compact_sympathetic | syndic_dominant | veil_institute | independent | disputed | mixed`) rather than one the heuristic guesses from hop-distance/currency; the existing derivation remains the fallback for unauthored systems. The archetype mapping itself is unchanged — it now reads a more trustworthy faction for the ~40–60 named systems where the heuristic was demonstrably wrong (e.g. Groombridge resolves `syndic_dominant` → Corporate, not the hop-2 default that would yield Commission). `lattice_commission` is deliberately **not** a faction value — the Commission is a regulator, not a governing faction (ACB and Bastion are `concord_assembly`). - **Implemented 2026-06-05 (T-956):** `attractor_matching::political_archetype(territorial_status, economic_role)` lands the derivation, stored per settlement on `CityPlacement.political_archetype`. `TerritorialStatus` precedence is enforced (a Commission-controlled manufacturing hub → `Commission`, not `Industrial`); statuses that don't dictate an archetype (`ContestedZone`/`IndigenousHeld`/`Derelict`) fall through to `economic_role`, and the new `AutonomistHeld` (D-212 amendment) → `Pioneer` (self-organized, no central planner). The "spatial effect on district mix" (D-194 weight multipliers) is consumed later by the Quarter-skeleton generator (T-957). - **Ticket:** T-914, T-956 (archetype derivation + storage) - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-194 (district mix — archetype modifiers), D-212 (TerritorialStatus — primary input), D-213 (FoundingOrientation — archetype may override), D-215 (spatial arrangement patterns), [D-237](#d-237) (authored `dominant_faction` source) ### D-215: Five Explicit Political Archetype Spatial Arrangement Patterns - **Date:** 2026-05-01 - **Decision:** Each `PoliticalArchetype` maps to one of five spatial arrangement patterns that govern district adjacency and the placement of landmark multi-block reservations: 1. **Radial core** (Commission, Academic): Central landmark (civic square, institutional plaza, or university quad) surrounded by mixed-use rings. Transit spokes radiate outward. Districts are denser near center. 2. **Campus grid** (Corporate): Restricted campus block occupies 2–4 blocks in the district interior. Commercial districts ring the exterior. Worker residential on periphery. 3. **Ribbon development** (Pioneer, Industrial): Districts string along a linear feature (river, road, industrial rail). No dominant center. Mixed adjacency at every edge. 4. **Fortified perimeter** (Military): Restricted and Secured districts at the edge of the footprint. Open access in the interior core. Single controlled access point per district edge. 5. **Hub-and-spoke** (transit_hub economic role, any archetype): Transit district at center, all other district types accessible via direct corridors. Maximum 2-district travel between any two districts. - The arrangement pattern constrains block adjacency during Phase 1 skeleton generation. Specifically: the first 2–3 districts placed in a settlement follow the pattern. Later districts are constrained only by the road network, not by the pattern. - Arrangement patterns must **vary in angular orientation** per seed (not just position) — the same archetype's radial core must not always face the same direction across seeds. - **Rationale:** The 14 D-ready items from the generator-architecture workshop established that spatial arrangement should encode power structure. These five patterns are the minimal set to cover the 6 archetypes (Pioneer and Industrial share ribbon development; hub-and-spoke is a cross-archetype pattern for transit-primary cities). Pattern variation in angular orientation prevents players from pattern-matching settlement layout after the first playthrough. - **Implemented 2026-06-05 (T-956):** the `ArrangementPattern` enum (the five patterns) and its derivation (`attractor_matching::arrangement_pattern`, from `PoliticalArchetype` + transit_hub override) land here, and the chosen pattern is **stored** per settlement on `CityPlacement.arrangement_pattern`. The block-adjacency **enforcement** (constraining the first 2–3 quarters' layout) is the Quarter-skeleton generator's job and is deferred to T-957; the per-seed angular variation rides on `FoundingOrientation` (D-213, seed-derived `Free` bearing). - **Ticket:** T-914 (types), T-956 (enum + derivation + storage), T-899/T-957 (skeleton-gen enforcement) - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-094 (spatial hierarchy — district sizes), D-194 (district mix — archetype modifiers), D-214 (PoliticalArchetype — pattern assignment) ### D-216: BlockIrregularity from founding_age — Layout Age Character - **Date:** 2026-05-01 - **Decision:** `block_irregularity: f32` is a derived value on each block (range 0.0–1.0) that controls how much a block deviates from the district's canonical grid. It is computed from `founding_age_years` and `PoliticalArchetype`. The formula: ``` base_irregularity = (founding_age_years / 1000.0).min(1.0) archetype_step = match archetype { Commission | Military => -0.3, // suppresses organic deviation Corporate | Academic => -0.1, Industrial => 0.0, Pioneer => +0.3, } block_irregularity = (base_irregularity + archetype_step).max(0.05).min(1.0) ``` - Minimum 0.05 is enforced — no block is perfectly regular, even new Commission-planned settlements. - `block_irregularity` feeds the `BlockPlacement.offset` magnitude in `DistrictLayoutMode::Organic`: `max_offset_sim_tiles = (block_irregularity × 16.0) as i16`. - An old Pioneer settlement (age 800+ years) can have `block_irregularity ≈ 1.0`, producing maximum ±16 sim tile offsets and ±45° rotations. A new Commission district (age < 50 years) will have `block_irregularity ≈ 0.05`. - All arithmetic uses integer-scaled intermediates wherever possible (age is integer years; archetype_step is stored as integer basis points internally). The f32 in the formula above is for documentation clarity only. - **Rationale:** Age is the single most reliable predictor of urban irregularity in the real world. Old cities that grew organically have crooked streets; new planned cities have grids. Encoding this as a formula rather than a lookup table allows continuous variation along the age axis while preserving the political meaning of the archetype modifier. - **Ticket:** T-922 - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-096 (DistrictLayoutMode::Organic — consumes block_irregularity), D-194 (district mix — founding_age is an input), D-214 (PoliticalArchetype — archetype_step source) - **Amendment (2026-05-26 — `founding_age_years` backfill, ticket T-1000):** `founding_age_years` is backfilled on every inhabited body in `tooling/economy-db/import_economics.py` MIGRATION_SQL via `COALESCE(events-first, wave-fallback)`. **Events-first:** the system's authored `historical_events.age_years` for `event_type = 'colonial_charter'` — the canonical founding event authored during the economics built-world workshop. 9 systems in the live DB carry a colonial_charter row, driving 11 inhabited body values; 5 of those diverge meaningfully from their wave fallback (e.g. GJ 144 = 580 vs wave-1 fallback 600, GJ 338B = 590, GJ 380 = 480). **Wave-fallback** — canonical **founding-edge** of each wave's range (`docs/design/systems-framework-final-miri.md:495–499`): | `settlement_wave` | era | range (years ago) | founding-edge fallback | |---|---|---|---| | `wave_1` | Core Founding | 500–600 | **600** | | `wave_2` | Trade Expansion | 300–500 | **500** | | `wave_3` | Working Reach | 100–300 | **300** | | `wave_4` | Frontier Push | 40–100 | **100** | | `wave_5` | Activation Edge | 0–40 | **40** | | `origin` | Sol — predates the wave taxonomy | — | **NULL** (Sol excluded per [D-236](scope.md#d-236)) | Founding-edge (not midpoint) so a wave-1 body reads as ~600 years old, matching the canonical Reach age. ±50yr drift from per-system reality is negligible for the `founding_age_years / 1000` arithmetic above. ### D-217: Tile Condition Thresholds (0.63 / 0.43 / 0.23) - **Date:** 2026-05-01 - **Decision:** A tile's visual condition is derived from the district's `prosperity_score` (live pressure simulation value, 0.0–1.0) using four threshold bands: | Band | Condition | prosperity_score range | Tile visual state | |------|-----------|----------------------|-------------------| | 1 | Intact | > 0.63 | Clean, undamaged, well-maintained | | 2 | Worn | 0.43 – 0.63 | Scuff marks, minor discoloration, partial repairs | | 3 | Cracked | 0.23 – 0.43 | Visible damage, incomplete repair, graffiti | | 4 | Broken | < 0.23 | Structural damage, debris, derelict appearance | - **Cache invalidation:** A tile's condition only changes when `prosperity_score` crosses a threshold boundary (from band N to band N±1). This avoids per-tick visual updates. The simulation checks threshold crossings once per game-minute (D-031 day-phase tick rate). - **Baseline floor:** The block's `EraCause` sets a minimum condition floor: - `Decay` era: minimum Cracked (no tile in a Decay-era block is ever Intact or Worn without an active renovation event) - `EmergencyExtension` era: minimum Worn - All other eras: no floor (condition follows prosperity_score freely) - **Phase 2 application:** Chunk fill applies the baseline condition at fill time. Subsequent condition updates from simulation crossing thresholds are applied as `ChunkMutations.tile_overrides`. - Condition thresholds are authored constants, not computed. Any change to the thresholds (0.63 / 0.43 / 0.23) requires amending this D-record. - **Rationale:** Threshold-crossing invalidation is a standard visual LOD technique that avoids expensive per-frame recalculation. The four bands (Intact/Worn/Cracked/Broken) match the visual fidelity budget for the current art direction — more bands require more tile variants per palette. The era-based floor ensures that historical context is always visible: a Decay-era block cannot spontaneously look pristine from a prosperity spike alone. - **Ticket:** T-923 - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-100 (DamageOverlay — post-condition modification), D-194 (district mix — prosperity_baseline is the seed for prosperity_score) ### D-218: WorldTier Enum Canonical Values (Epicenter/Regional/Backwater/Passage/Waypoint) - **Date:** 2026-05-01 - **Decision:** The canonical `WorldTier` enum values are: ```rust enum WorldTier { Epicenter, // Hub system. Full simulation. High faction pressure. Multi-district cities. Regional, // Regional hub. 1–4 districts per city. Partial full-budget districts. Backwater, // Small community. Dense isolated settlement. Full sim budget — NOT capped. Passage, // Transit stop. Pass-through. ComplexityTier ceiling: Moderate. Waypoint, // Not simulated until player approaches. ComplexityTier ceiling: Minimal. } ``` - The values `Peripheral`, `Connected`, and `Core` used in generator.rs prior to Sprint 38 are **incorrect** — they were stubbed values that do not match the workshop design (workshop-outcomes.md §WorldTier and ComplexityTier). They must be replaced with the five canonical values above. - **ComplexityTier ceiling per WorldTier:** - `Epicenter` → Full - `Regional` → Full - `Backwater` → Full (critical: `Backwater` is network-insignificant, NOT budget-capped; isolated communities can be socially complex) - `Passage` → Moderate - `Waypoint` → Minimal - **Source of truth:** workshop-outcomes.md §WorldTier and ComplexityTier table (generator-architecture workshop, lead decision L-3). - All code referencing `WorldTier::Peripheral`, `WorldTier::Connected`, or `WorldTier::Core` must be updated to the canonical values. This includes generator.rs, any tests, and any serialized data that references these variants. - **Rationale:** The three-value stub (Peripheral/Connected/Core) was authored before the generator architecture workshop established the five-value canonical model. The mismatch between the code and the design means any generator code built against the stub types would need rewriting anyway. Correcting it now before the Phase 1 implementation work begins eliminates that rework. The `Backwater` full-budget exception is architecturally significant: dense isolated communities (mining towns, research outposts) should be as socially rich as regional hubs — their isolation is their drama, not their limitation. - **Ticket:** T-900 (bug fix), T-912 (full enum implementation) - **Raised by:** Generation cascade workshop (T-897). Original workshop-outcomes.md §WorldTier (generator-architecture workshop, lead decision L-3). - **Cross-reference:** D-096 (DistrictLayoutMode — WorldTier is a DistrictSkeleton field), D-097 (GuaranteeAuditResult — tier-conditional guarantees), D-201 (spatial hierarchy — WorldTier assigned at Tier 2 System level) --- ### D-220: Settlement Density Model — Tile Scale, Density Classes, and Vertical Pressure - **Date:** 2026-05-03 - **Decision:** Settlement footprint, building height, and basement depth are all driven by a single **density scalar** on each settlement, derived from population, available land, terrain constraints, and economic pressure. This decision establishes the tile scale, the density classification system, and the rules for how density shapes the built environment. **Tile scale (amends D-201 Tier 0):** | Unit | Dimensions | Real-world size | Notes | |------|-----------|-----------------|-------| | Subtile | 1×1 | 0.5m × 0.5m | Rendering unit — character positioning, furniture placement, visual detail | | Tile | 1×1 | 1m × 1m | Spatial unit — generation pipeline, pathfinding, district grids | | Chunk | 64×64 tiles | 64m × 64m | Streaming unit (was 32m at 0.5m/tile; now 64m at 1m/tile) | | Block | 128×128 tiles | 128m × 128m | 2×2 chunks | | District | 512×512 tiles | 512m × 512m = 0.262 km² | 4×4 blocks | Each tile contains 2×2 subtiles. The generation cascade operates in tiles. The renderer subdivides to subtiles for visual fidelity. All higher tiers (Province, Area, Body) remain as defined in D-201. - **Amendment (2026-05-22, [D-222](#d-222)):** the 512m unit this table calls **District** is renamed the **Quarter**; **District** is promoted to a new 2048m tier (4×4 quarters). The Tile/Subtile scale above is unchanged. Throughout this record — including the footprint formula (`district_count = footprint / 0.262`) and the density examples — "district" denotes the **0.262 km² Quarter cell**, not the new 2048m District. See D-222 for the canonical ladder and the lore-vs-code rule. **Density classes:** Density is measured in people per km² of settlement footprint. It is the primary driver of settlement character — not population alone. The same population at different densities produces completely different settlements. | Class | Density (ppl/km²) | Horizontal character | Vertical character | Infrastructure cost | Who builds at this density | |-------|-------------------|---------------------|-------------------|--------------------|-----------------------------| | Frontier | ~400 | Large gaps between structures, open ground dominant, private roads | 1 floor, no basement | Very high per-capita (long utility runs, private maintenance) | **The very rich** (villa estates, corp executive compounds, prestige vanity builds) OR **the very poor** (pre-utility squatter settlements, homesteads with no services). Frontier density is a luxury or an absence, never the default. | | Settled | ~1,500 | Planned grid, moderate spacing, shared streets, visible utility corridors | 1-2 floors, optional basement | Efficient — the utility-cost-optimized default | **Default for most Reach colonies.** Every meter of sewer, power conduit, and road is new capital expenditure on a virgin world. Settled density is what you get when infrastructure cost constrains the plan. Kiruna is the archetype. | | Established | ~3,000 | Mixed residential, shared walls appearing, streets narrowing, green space intentional not incidental | 2-3 floors, basement common | Amortized — mature infrastructure absorbing incremental growth | **Mature settlements.** Second-generation growth over an existing utility grid. The settlement has existed long enough for the infrastructure investment to pay off and densification to begin. Tiel, Périgueux. | | Dense | ~6,000 | Shared walls dominant, narrow streets, minimal private outdoor space, commercial ground floors | 3-6 floors, 1-2 basement levels (parking, storage, utilities) | High per-area but efficient per-capita | **Economic hubs.** Transit nodes, extraction chokepoints, terrain-constrained sites where the jobs pull people in faster than the settlement can spread. Kanazawa, Uzès. Density is a signal that economic gravity is compressing the settlement. | | Compressed | ~15,000 | Vertical dominant, street canyons, multi-use buildings, public space carved from built volume | 6-12+ floors, 2-3 basement levels (metro, parking, utilities, subsurface logistics) | Very high per-area, requires vertical infrastructure (elevators, pressurized systems, multi-level utilities) | **Extreme economic pull.** Major corp HQs, gate-adjacent trade hubs, capital worlds. Rare in the Reach. The land itself is too valuable for anything but vertical. Paris, Amsterdam inner ring. | **What drives density — the four pressures:** 1. **Utility cost (compressing force).** The dominant pressure on new colonies. Infrastructure is expensive per meter. This pushes settlements toward Settled as the floor, not Frontier. Every colony pays this tax unless someone absorbs the cost deliberately. 2. **Economic pull (compressing force).** Jobs, trade, resources, transit — anything that draws people to a specific location faster than the settlement can grow outward. The stronger the pull, the higher the density. Corp HQ worlds, gate-adjacent systems, extraction sites all have high economic pull. This is the force that pushes settlements from Settled through Dense to Compressed. 3. **Terrain constraint (compressing or dispersing force).** Coastal strips, canyon floors, mountain passes compress settlements along one axis (Bisbee: 2.5km × 300m). Open plains allow radial spread. River valleys elongate. The terrain doesn't set the density class directly — it shapes the footprint geometry within that class. 4. **Wealth/status (dispersing force — at a premium).** The only force that pushes density DOWN below the utility-cost floor. Low density in an otherwise dense settlement is conspicuous consumption of space — private utility runs, private road maintenance, private security perimeters. Villa estates, executive compounds, prestige campuses. This is Frontier density as luxury, not as poverty. **The U-shaped wealth curve:** Density has a U-shaped relationship with wealth. The very rich and the very poor both live at low density — but for opposite reasons. The rich can afford the infrastructure cost of space. The poor live where no infrastructure exists. Everyone in between clusters at Settled-to-Dense because that's what utility economics demands. The player reads this instantly: low-density district on a dense world = money. Low-density district on a frontier world = nothing built yet. Same tile spacing, completely different story — and `prosperity_baseline` (D-197) disambiguates. High prosperity + low density = wealth. Low prosperity + low density = absence. **Density → vertical pressure mapping:** Density drives building height and basement depth via simple thresholds. These are tendencies, not hard limits — individual buildings vary, but the district-level character follows the pattern. | Density class | Typical above-ground floors | Typical basement levels | Vertical pressure description | |---------------|---------------------------|------------------------|------------------------------| | Frontier | 1 | 0 | Single-story. Horizontal only. | | Settled | 1-2 | 0-1 | Mostly single-story, occasional two-story. Basement where terrain allows. | | Established | 2-3 | 1 | Two-story default. Three-story at intersections and commercial streets. Basements standard. | | Dense | 3-6 | 1-2 | Multi-story default. Ground-floor commercial. Structured parking appears underground. | | Compressed | 6-12+ | 2-3 | Highrises. Street canyons. Multi-level subsurface: parking, metro, utilities, logistics. | The transition from "buildings grow out" to "buildings grow up" happens at Dense. Below Dense, the settlement solves population pressure by adding more districts horizontally. At Dense and above, horizontal expansion is constrained (by terrain, by utility cost, by land value) and vertical growth begins. The transition from "no basement" to "buildings grow down" happens at Established. Below Established, basements are optional amenities. At Established and above, basements become structural necessities — storage, utilities, parking that can't fit at grade. **Density derivation:** The density scalar for a settlement is derived from existing systems.db data: ``` base_density = SETTLED_DEFAULT // 1,500/km² — the utility-cost floor // Economic pull increases density if economic_role in [transit, commercial, manufacturing]: base_density *= 1.5–2.0 if is_corp_hq_world: base_density *= 1.5 if gate_adjacent: base_density *= 1.3 // Terrain constraint modulates if terrain_constraint_factor > threshold: // coastal strip, canyon, etc. base_density *= 1.2–1.8 // Wealth disperses (for specific districts within a settlement) if district.prosperity_baseline > 0.8: effective_density *= 0.4–0.6 // villa district within a dense city // Clamp to class boundaries density_class = classify(effective_density) ``` The exact multipliers are tunable. The structure — base at Settled, economic pull compresses, wealth disperses — is the locked architectural decision. **Settlement footprint formula:** ``` footprint_km2 = population / effective_density district_count = ceil(footprint_km2 / 0.262) grid_side = ceil(sqrt(district_count)) ``` Not all districts within the grid are built. A Settled-density village of 2,000 people covers ~1.3 km² = 5 districts in a 3×3 grid, but only 5 of 9 grid cells contain structures. The remaining 4 are open ground, agricultural land, or wilderness. The fill ratio increases with density class: | Density class | Typical grid fill ratio | What fills the empty cells | |---------------|------------------------|---------------------------| | Frontier | 20-40% | Wilderness, private grounds, no development | | Settled | 50-70% | Agricultural plots, utility corridors, planned expansion reserves | | Established | 70-90% | Parks, low-density transition zones, institutional grounds | | Dense | 90-100% | Nearly full — remaining gaps are intentional public space | | Compressed | 100% | Full. No gaps. Every cell is built. | **Reference calibration (real-world sanity check):** | Real settlement | Pop | Real area | Real density | Model class | Model area | Match quality | |----------------|-----|-----------|-------------|-------------|------------|---------------| | Wadenoijen, NL | 695 | 0.10 km² | 6,950/km² | Dense | 0.12 km² | Close | | Kerk-Avezaath, NL | 1,250 | 0.40 km² | 3,125/km² | Established | 0.42 km² | Close | | Bisbee, AZ | 5,000 | 1.50 km² | 3,333/km² | Established | 1.67 km² | Close | | Kiruna, SE | 17,000 | 11.40 km² | 1,491/km² | Settled | 11.33 km² | Near-exact | | Périgueux, FR | 30,000 | 9.82 km² | 3,055/km² | Established | 10.00 km² | Near-exact | | Tiel, NL | 39,000 | 13.00 km² | 3,000/km² | Established | 13.00 km² | Exact | | Kanazawa DID, JP | 466,000 | 59.00 km² | 7,898/km² | Dense | 77.67 km² | Reasonable | | Paris proper, FR | 2,040,000 | 105.00 km² | 19,429/km² | Compressed | 136.00 km² | Reasonable | The model is calibrated to real-world settlements. Discrepancies come from the square-grid assumption (real settlements elongate along rivers, coasts, and roads) and from the discrete density class boundaries (continuous real densities mapped to five bins). **Interaction with other D-records:** - **D-194 (district mix):** District *types* are still determined by the three-component mix. Density determines how much *space* those districts occupy and how they're spaced. - **D-197 (prosperity_baseline):** Disambiguates the wealth curve. Same low density reads as luxury (high prosperity) or absence (low prosperity). - **D-201 (spatial hierarchy):** Tile scale amended from 0.5m to 1m with 0.5m subtile. Chunk real-world size doubles from 32m to 64m. All higher tiers unchanged. - **D-215 (spatial arrangement patterns):** The five archetype patterns operate within the footprint that density determines. CompanyTown spine is longer at Settled density, compressed vertically at Dense density. - **D-216 (BlockIrregularity):** Irregularity affects street grid geometry within a district. Density affects building height and spacing within that geometry. Orthogonal — a Dense district can be Organic (irregular streets, tall buildings) or Grid (regular streets, tall buildings). - **Rationale:** Population alone cannot determine settlement character. A village of 2,000 at medieval European density (Kerk-Avezaath, 3,200/km²) and a homestead spread of 2,000 at frontier density (400/km²) produce a 10× footprint difference. In the Reach, utility cost on virgin worlds is the dominant constraint, making Settled (~1,500/km²) the natural floor. The density scalar unifies footprint calculation, vertical pressure, and inter-building spacing into a single derivable parameter. The tile/subtile split preserves 0.5m visual fidelity for character movement and furniture placement without inflating the generation pipeline's spatial grid. - **Ticket:** (pending — implementation tickets to be created when cascade Tier 3/4 work begins) - **Raised by:** Jeroen + Claude, design session 2026-05-03. Grounded against real-world settlement data (Wadenoijen, Kerk-Avezaath, Uzès, Tiel, Périgueux, Kiruna, Kanazawa, Paris, Bisbee, Alice Springs, Brasília). - **Cross-reference:** D-194 (district mix), D-197 (prosperity_baseline), D-201 (spatial hierarchy — tile scale amendment), D-215 (spatial arrangement patterns), D-216 (BlockIrregularity) - **Dissent:** None --- ### D-222: Spatial hierarchy and naming — Subtile to District, lore vs code - **Date:** 2026-05-22 - **Decision:** Canonical naming and sizes for the sub-settlement spatial hierarchy. This record is the single source of truth for the ladder; it supersedes the dimensions/naming scattered across D-094 (hierarchy), D-201 (tier table), D-220 (tile scale), and D-066 (dual-scale grid), which are amended to point here. **The ladder (each tier nests cleanly in the one above):** | Unit | Side | Composition | Real-world analogue | Used for | |------|------|-------------|---------------------|----------| | Subtile | 0.5m | — | a footstep | server simulation, entity positioning, render detail | | Tile | 1m | 2×2 subtiles | a doorway | **the unit all sizes are quoted in**; generation cascade, pathfinding, grids | | Chunk | 64m | 64×64 tiles | half a block | streaming / serialization unit (technical) | | Block | 128m | 2×2 chunks | a city block (~100m real) | generator planning unit | | Quarter | 512m | 4×4 blocks (16) | a superblock (≈ Barcelona *superilla*, 0.262 km²) | the cell a settlement footprint is tiled into | | District | 2048m | 4×4 quarters (256 blocks) | a real urban district (4.19 km²) | large urban division | | *(above)* | *fluid* | — | borough / sector / city | **not a fixed tier** — settlements vary too much; groupings above District are settlement-specific | Nesting rhythm: Chunk→Block is 2×2 (Chunk is a technical sub-block streaming unit); Block→Quarter→District is a uniform 4×4 (the legible "human" ladder). **Lore ≠ code (load-bearing):** the tier names above are **code/generation units** — fixed grid cells. The *same words* used in lore, narrative, and UI ("Sova Transit District", "the market quarter") are **free-form region labels** with no obligation to map to a code tier. A narrative "district" may span several code Quarters, sit inside one, or ignore the grid entirely. **Reviewers and the clerk must not reconcile a lore name to a code tier** — they are different registers that happen to share vocabulary. **Rename from prior records:** the 512m cell that D-094 / D-201 / D-220 called a **District** is now the **Quarter**. "District" is promoted to the 2048m tier (4×4 quarters), matching the real-world scale of a named urban district. The tile scale (Tile = 1m, Subtile = 0.5m) and the doubled footprints established by D-220 are unchanged. Code that uses "District" for the 512m skeleton unit (e.g. `DistrictSkeleton`) is now misnamed → see follow-up ticket. - **Rationale:** The old hierarchy named only Chunk/Block/District and jumped from a 512m "District" straight to the 1–30km Settlement, leaving the 512m cell wearing the name of a unit two rungs too large — a real district is kilometres and many neighborhoods, not 16 blocks. Grounding against real sizes (city block ~100m; Barcelona superblock 400m / 9 blocks; real district several km) puts Block at 128m (correct), the 512m cell at Quarter/superblock, and a true District at 2048m (4.19 km²). Leaving everything above District fluid matches reality — boroughs/sectors/cities vary too much to force a fixed generation tier, and the Reach's settlements range from frontier homesteads to compressed capitals. Separating the lore register from the code register lets narrative use "district" naturally without dragging the generator's grid into it. - **Raised by:** Jeroen + Claude, design session 2026-05-22. Grounded against real-world block/superblock/district scales. - **Cross-reference:** [D-094](#d-094) (spatial hierarchy — renamed/superseded), [D-201](#d-201) (tier table — superseded for the sub-settlement tiers), [D-220](#d-220) (tile scale — Tile/Subtile retained; its "district" footprint cell is the Quarter), [D-066](#d-066-dual-scale-grid--05m-simulation-1m-visual-2x-retina-factor) (dual-scale — Subtile is the 0.5m grid), [D-093](content.md#d-093-sova-transit-district--spatial-layout-and-district-topology) (Sova Transit District — a *lore* district, not bound to the code tier) - **Dissent:** None --- ### D-223: Authored content as a flavored name pool — markers names-only, hand-authored templates removed - **Date:** 2026-05-22 - **Decision:** Per-planet authored content feeds the deterministic generator as a **flavored name pool only** — never as authored geometry or pinned positions. This supersedes the topographic-geometry retention and the `reserved=1` pinning of [D-207](#d-207-fully-generative-placement--markersjson-stripped-to-topographic-features), and the hand-authored-template references in [D-191](#d-191) §8. **markers.json → names only.** A body's `markers.json` carries a flavored pool of names (river names, mountain names, settlement names) and **no positions or geometry**. Positions are not authored anywhere; they are derived or generated: - **Rivers** — courses come from the deterministic D8 drainage (D-208) on the fixed heightmap; the **largest rivers take names from the pool**. - **Mountains** — positions are a given from the **fixed heightmap**; they take names from the pool. - **Settlements** — the economic sim runs, the **largest population areas** are found, the capital and cities are picked by the placement pipeline (D-211), and they **take names from the pool**. **The 6 hand-authored templates are removed.** Lendel, Edict, Vuurkloof, Røros, Cairnside, Estrade were a superseded hand-authored-showcase direction that kept resurfacing and complicated the landscape. Their hand-authored marker positioning, the `reserved=1` pinning, and the "6 templates" framing/special-casing are all removed. **The bodies themselves remain real places** — their wiki pages, lore, corporations (e.g. comptoir-lendel), and names stay and feed the name pool; deleting them would only force regenerating equivalent content. Only the authored *machinery* is tossed. **After this there is no distinguishable category of "template" left** — the six are processed by the exact same pipeline and data model (fixed heightmap + names-only markers) as every other body, with no special code path, schema flag, or pinning. They differ only in *content richness* (more developed lore/names/economic profile), which is a difference of degree shared by any notable body, not a difference of kind. The concept dissolves; that is why it can be removed wholesale rather than carefully migrated. **No pinned positions anywhere.** Determinism comes from fixed heightmaps + deterministic drainage + deterministic economic sim + seeded placement — not from authored anchors. The lore-vs-code split (D-222) governs how pool names attach to generated features: names are free-form labels on generated geometry, not bound to grid cells. Sub-city authored set-pieces (e.g. D-093 Sova Transit District, station interiors) follow the same principle when Phase 4 reaches that scale — generated geometry, lore names/roles attached, nothing pinned — to be applied (and prior authored layouts superseded) at that point. - **Rationale:** Authored positions created a hand/procedural split that (per D-207's own rationale) was impossible to query, diff, or validate. Reducing authored content to a name pool removes that split entirely while preserving the flavor that makes places feel hand-made — names follow culture and region, geometry follows terrain and economics. The templates were leftover scaffolding from before fully-generative placement; removing them simplifies the pipeline without losing any canonical place. - **Implementation:** tracked under Phase 4 (epic T-750) — strip markers to names, remove template machinery + `reserved` pinning, update the atlas pipeline/schema, regen. Historical archives (sprints, workshops, audits, CHANGELOG, atlas proposals) are left untouched. - **Implementation status (T-951, 2026-05-22): done.** All 2,398 `markers.json` reduced to a names-only pool (25,264 names; 349 city names across 271 bodies). The Python atlas **geometry generator** (`generate_atlas.py`) and the **LLM naming cluster** (`gemma_naming.py`, `naming_core.py`, `apply_name_fixes.py`, their tests/QA, the `fix_fewshot_bleed`/`prune_atlas_features` geometry tools, the redundant `import_city_names.py`, and the `run-atlas-naming.sh` runner) were retired — the procedural server cascade (Phase 4) supersedes them. The Gemma prompting methodology is preserved in [docs/gemma-naming-methodology.md](../docs/gemma-naming-methodology.md). Shared atlas-DB utilities moved to `tooling/planet-gen/atlas_common.py`; **`import_economics.py` is now the sole regen-db generator that owns the atlas index** — it loads the names pool into `atlas_city_names` and empties the 8 geometry tables (`atlas_cities/roads/railroads/pois/rivers/oceans/mountain_ranges/body_grids`), which the cascade fills. ~~`population`/`kind`/`settlement_class` on `atlas_city_names` are deferred to placement (T-955)~~ **SUPERSEDED (D-242, T-1075):** both are now install-baked at import time (a documented rank-size derivation from `bodies.population`; `settlement_class` defaults `PopulationBudget` with a small `NameLocked` override list) — see D-242. ~~The `reserved=1` corp-HQ cross-reference stays (corp HQ names are real places)~~ **SUPERSEDED (D-242, T-1074):** that insert path produced duplicate co-named "cities" (no `UNIQUE(body_id, name)`) and is removed; the corp↔settlement relationship now lives on `corporations`, not as a city-pool row — see D-242. A latent duplicate-accumulation bug in name population (no clear + no unique constraint) was fixed with a deterministic rebuild. **Sol (system `GJ 0`) is permanently exempt from the normal generators:** it uses real Earth/Mars/Luna geography via the offline `sol_import.py` (left in place for future scripted integration), so its bodies keep geometry-bearing `markers.json` as preserved positional config and are skipped by the names-pool importer — Sol names will come from its own integration, not the cascade. `make regen-db` green. - **Raised by:** Jeroen, 2026-05-22 — resolving the open question on merging preconfigured content into the deterministic cascade. - **Cross-reference:** [D-207](#d-207-fully-generative-placement--markersjson-stripped-to-topographic-features) (superseded — names-only, no reserved pinning), [D-191](#d-191) §8 (markers format — names-only), [D-208](#d-208) (drainage → river courses), [D-211](#d-211) (settlement placement), [D-199](#d-199) (economic read set), [D-222](#d-222) (lore≠code names on generated geometry), [D-242](#d-242) (corp-HQ cross-reference removed + population/settlement_class baking — both supersede notes above) - **Dissent:** None --- ### D-224: SeedChain — deterministic seed-derivation contract - **Date:** 2026-05-23 - **Decision:** All deterministic generation — the Phase-4 world cascade now, and NPC / storyteller / economic generation later — descends from a single master world seed through one shared type, **`SeedChain`**, living at **`server/src/seed.rs`** (top-level, because seeds are broader than the atlas). It is the **only** sanctioned way to derive a child seed; the ad-hoc pre-mixing (`seed.wrapping_add(C)`) that was in `skeleton_gen.rs` / `district_mix.rs` is removed, and the doc-comment references to a "SeedChain" in those files become real. **Mixing primitive.** `splitmix64` — already used by `EntityRng::from_seed_and_id` (`simulation/rng.rs`), chosen there specifically to avoid the `wrapping_add` collision class where `(seed=0,id=N) == (seed=1,id=N-1)` — is promoted to a shared `pub(crate)` function in `server/src/seed.rs` and reused. One canonical *mixer* for the whole codebase. (`EntityRng` keeps its own domainless combine — `splitmix64(world_seed) ^ splitmix64(stable_id)` — for now; re-expressing it as `derive(Npc, stable_id)` would be a deliberate, **stream-changing** migration, not a free cleanup, so it is left as a follow-up.) **Contract:** ```rust pub struct SeedChain(u64); // Copy pub enum SeedDomain { Body, Layer1Topography, Layer3Settlement, Layer4Quarter, Block, Npc /* … */ } // u64 tags impl SeedChain { pub fn root(world_seed: u64) -> Self; // top of the chain pub fn for_body(world_seed: u64, body_id: &str) -> Self; // root + derive(Body, fnv1a_64(id)) pub fn derive(self, domain: SeedDomain, id: u64) -> Self; pub fn atlas_rng(self) -> AtlasRng; // integer-only LCG stream (D-010) pub fn seed(self) -> u64; // raw — for SimRng/ChaCha or further derive() } ``` **Derivation (load-bearing — pinned, because changing it changes every generated world):** `derive(domain, id) = splitmix64(self.0 ^ splitmix64(domain as u64)) ^ splitmix64(id)`. Properties: deterministic; domain-separated (distinct `SeedDomain` tags never share a stream); full avalanche (splitmix64 on each input); integer-only (D-010 T-4); chainable (`root → Body → Layer3Settlement → Quarter → Block`). The output is well-distributed, so `AtlasRng::new` is fed the derived seed directly — no `| 1` or golden-ratio pre-mix guard. **Body identity (the `Body` domain id).** Bodies are identified by a *string* `body_id`, not a numeric StableId, so `SeedDomain::Body` is keyed by **FNV-1a (64-bit) of `body_id`** — the repo's standard deterministic `&str → u64` convention (matching `TemplateId`/`TriangleId`). `SeedChain::for_body(world_seed, body_id)` (`root(world_seed).derive(Body, fnv1a_64(body_id))`) is the single sanctioned path; callers must use it rather than inventing their own string→u64 hash, or they would silently derive divergent worlds from the same seed — the very class of nondeterminism this record exists to kill, one level up. **Stability guards.** `SeedDomain` carries explicit `#[repr(u64)]` discriminants and is append-only; the unit test `seed_domain_discriminants_are_pinned` fails CI if any is renumbered (which would re-roll every world). `AttractorType` likewise carries explicit `#[repr(u8)]` discriminants because it is cast `as u8` as a sort key (`features.rs`); reordering it would change attractor ordering and flip the cascade golden. **Scope of effect (verified 2026-05-23):** SeedChain changes only the RNG-*using* layers — the existing `skeleton_gen.rs` (Layer 4 block placement) and the future Layer-3 settlement placement (T-955). It does **not** affect Layer 0 heightmaps (produced by the Python `planet_simulation` pipeline, seeded separately via `--seed`, committed as `heightmap.png` files) nor Layer 1 (`drainage`/`features`/`subbiome` are RNG-free — pure functions of the heightmap). The T-952 Layer 0→1 golden fixtures are therefore SeedChain-independent and can be captured in any order relative to the SeedChain work. - **Rationale:** Three seeding paths had drifted apart — `AtlasRng` (LCG, "callers pre-mix"), `EntityRng` (correct splitmix64 mixing), and ad-hoc `wrapping_add` in atlas callers — while the code already *named* a SeedChain that didn't exist. A single typed derivation chain with domain separation makes every sub-stream reproducible from one world seed, eliminates the `(seed,id)` collision class `wrapping_add` invites, and gives the determinism harness (T-952) a stable contract to verify against. Promoting one mixer prevents two divergent implementations. - **Implementation:** T-952 (Phase 4, epic T-750) — `server/src/seed.rs` (incl. `for_body`/`fnv1a_64`), `SeedChain` threaded through the atlas RNG callers, an extensible cascade harness (`run_cascade`/`CascadeSnapshot`/`CascadeLayer`), and a golden-seed regression test (SHA-256 of `heightmap.png` + JSON-serialized `Layer1Output`, run at 256×128 so the river network is non-empty — JSON not msgpack, to match the diffable `golden_suite.rs` convention). Pre-Phase-5: no savegames exist, so the seed-stream change needs no migration; D-202's `schema_version` lineage covers future changes once saves exist. - **Raised by:** Jeroen + Claude, `/whats-next` refinement of T-952, 2026-05-23. - **Cross-reference:** [D-010](#d-010) (determinism — integer-only, seed→identical output), [D-200](#d-200) (three-tier execution model), [D-208](#d-208) (RNG-free drainage), [D-223](#d-223) (names-only pool — placement uses seeded RNG), `simulation/rng.rs` (EntityRng / splitmix64 precedent) - **Dissent:** None --- ### D-225: Atlas layer-stream proxy — compute-on-demand, mod-first (resolves Q-098) - **Date:** 2026-05-23 - **Decision:** How per-body generation-cascade layer data reaches the Godot client for the Phase-4 Atlas progress viewer (T-960). **Resolves Q-098.** Derived layer data is **never baked** into the install — that would both bloat the install (~100MB+ for ~267 bodies) and make modded bodies second-class. Instead a server-side **layer-stream proxy** computes on demand from moddable source files and streams to the client: **(1) Transport — existing IPC stream + additive message tag.** Not a second socket, not a per-frame envelope rewrite. The bridge today carries no message-type discriminator (server→client is always `ObserverSnapshot`, client→server always `Vec`). The atlas request/response ride the *same* TCP stream as new message types, disambiguated **structurally** in v1 (a snapshot has `entities`/`tick`; the atlas messages do not). A full `BridgeMessage` envelope-everywhere migration is deferred — it would be a needless wire break, and client+server co-ship (D-005/D-192) so it can be done later as cleanup. **(2) Single framed MessagePack response, not chunked.** `Layer1Output` is ~hundreds of KB worst case (512×256), well under the 16 MB frame cap. The **raster is not streamed** — the client already loads `reliefmap.png` from disk; the proxy streams only the *computed* `Layer1Output` (rivers, basins, attractors + sub-biome). MessagePack matches the rest of the bridge. **(3) Mod-first source resolution.** A new `BodySourceResolver` with an ordered search: mod dirs (override) → base install (`wiki/...`, the floor). It reads the body's relative `terrain_reference` from `systems.db` (read-only) and returns the first existing `/`. First-party and mod bodies flow through the **identical** resolve→`run_cascade`→stream path — the `heightmap.png` is the sole source of truth, no baked layer data. v1 wires the base root only; the resolver type + search order exist and are tested with a synthetic mod root, so the seam is mod-first from day one. **(4) Cache via background queue on miss (D-203/D-206).** Request → `BodyWorldStateCache` lookup. Hit → serialize and return synchronously (a serialize, no compute). Miss → enqueue an `Immediate` `AnalyzeBody` on the `GenerationQueue` (D-206 background tier) and reply `Pending`; push the layer response when the completion lands. **The ~45 ms cascade must never run on the tick thread** (D-203: no blocking CPU/DB for heightmap data on main). The client shows the existing "TERRAIN DATA PENDING" panel until the response arrives. Eviction → recompute (always valid — determinism guaranteed). **(5) Whole `Layer1Output` per response; client composites additive overlays.** The layers are produced together in one drainage pass, so per-layer requests save no compute and only add round-trips. Overlay toggles (heightmap + rivers + attractors + sub-biome shown *together*) are a pure client-side render concern (the existing `_overlay_visibility` pattern). The request carries `body_id` + `up_to_layer` (a forward-compat seam; v1 honors `Topography`). - **Critical-path dependency:** the proxy is inert until `gen_queue.rs::run_work_item`'s `AnalyzeBody` actually runs `run_cascade` → builds `BodyWorldState` → populates the cache (today a documented stub, deferred from T-142), and `GenCompletion::BodyAnalyzed` carries the computed state, not just `body_id`. This activation is the long pole and is tracked as its own ticket blocking T-960. - **Rationale:** Baking privileges first-party content (a mod body cannot ship baked artifacts it cannot produce) and adds install bloat. Computing from the moddable heightmap on demand — the cascade is deterministic and ~45 ms, and D-200/D-203/D-206 already provide the background-compute + LRU tiers — keeps mods first-class, adds zero storage, and uses the architecture as intended. Reusing the existing IPC stream (vs a second socket) avoids a parallel connection lifecycle for an occasional, user-initiated, latest-wins-irrelevant request. - **Deferred / spun off:** the full `BridgeMessage` envelope-everywhere migration (later cleanup, not this ticket); the mod **content catalog** — mods adding *new* bodies need body rows + `terrain_reference` discoverable, but `systems.db` is binary / source-canonical (D-189) and mods cannot append to it → **Q-099**. D-225 resolves mod *file* resolution only; base-install resolution is enough to ship T-960. - **Implementation:** T-960 (client viewer + proxy) plus the `AnalyzeBody` activation ticket. New surface: `server/src/atlas/source_resolver.rs`, `server/src/atlas/layer_proxy.rs`; `gen_queue.rs` `run_work_item` activation + `GenCompletion` payload; bridge receive/send branching; client `protocol.gd` / `sim_bridge.gd` / `atlas_viewer.gd`. - **Raised by:** Jeroen (mod-first directive) + Tyre (design pass) + Claude, `/whats-next` refinement of T-960, 2026-05-23. - **Amendment (2026-06-12, extension constraint — fable-ous.md S-10):** `decode_inbound` (`bridge/mod.rs:38-74`) distinguishes the two inbound message types purely by MessagePack shape (array = `Vec`, map = `AtlasLayerRequest`) and serde ignores unknown map fields — so a future second map-shaped inbound type with overlapping required fields would silently mis-decode as an atlas request. Constraint: **the next inbound message type must introduce a tagged envelope** (or a required marker field) rather than extending shape-based detection. - **Amendment (2026-07-23, body-map-viewer workshop — deferred constraint DISCHARGED; see [D-255](#d-255)):** the 2026-06-12 constraint above ("the next inbound message type must introduce a tagged envelope / a required marker field") is now **executed** by [D-255](#d-255). The step-canvas payload cannot ride the existing `district_window` windowed carrier (measured 21×–563× over the ~30 KB windowed ceiling across the three canvas sizes), so it introduces a **required marker field** (`step_canvas: bool` on a new `StepCanvasRequest` inbound variant + a dedicated `StepCanvasResponse` outbound message), extending the same `star_map`/`city_names`/`browse` discriminated-shape pattern. This is the sanctioned tagged-envelope path D-225 deferred, now built — no note change beyond recording that the constraint is discharged there. - **Cross-reference:** Q-098 (resolved by this), Q-099 (mod content catalog — spun off), [D-191](#d-191) (Atlas viewer), [D-166](#d-166) (per-layer Atlas progress viewer), [D-200](#d-200) / [D-203](#d-203) (three-tier execution, LRU cache), [D-005](#d-005) / [D-192](#d-192) (client+server co-ship — no version handshake), [D-224](#d-224) (SeedChain — feeds the cascade the proxy runs), [D-255](#d-255) (body-map-viewer render architecture — discharges the tagged-envelope deferral) - **Dissent:** None --- ### D-226: Live-pause inspection harness — agent-navigable, real-UI debug/review - **Date:** 2026-05-24 - **Decision:** A multi-layer harness that reuses the **real client UI** so both a human *and* an automated agent can review/debug deterministic server-computed state, by attaching to the live (auto-pausing) server. It supersedes the offline file-dump idea floated during D-225: the review tool *is* the production tool (zero divergence), inspecting live data through the real path rather than stale dumped artifacts. The stack, built bottom-up: **(1) Live-pause substrate.** The client attaches to the running server; entering a fullscreen implant app (the Atlas) **auto-pauses the sim**. Pause freezes the world-advancing tick phases (Movement / Simulation / Economy / Storyteller / Knowledge / TickAdvance) but **keeps PreInput (the gen-drain), Input (to receive resume), Snapshot and PostSnapshot (the bridge) alive** — so the UI keeps fetching data while the world is frozen. Built on the existing `TickRate::Paused` + the `input.rs` paused-allowlist seam, triggered by `HudGroups::gameplay_occluded` (D-170). For static Layer-1 geography pause is only a compute-saver; for *dynamic* state (economics) a stable frozen snapshot is essential. **(2) Data — layer-stream proxy (D-225).** Streams `Layer1Output` (later economics state, save state) from server to client on demand, mod-first, no bake. **(3) Human-visual viewer (T-960).** The Atlas renders cascade layers as **additive, toggleable overlays** (relief base + rivers + drainage basins + attractors; *shape* encodes attractor type, *color* encodes sub-biome), extending the existing `OVERLAY_DEFS` / `AtlasOverlayBar` with a `generation` overlay group + a left-side legend panel. Buttons start `pending`/locked and unlock as each layer's data arrives ("grows as each layer lands", D-166). **(4) Agent-navigable channel.** A client-side `AtlasAgentInterface` exposing JSON `observe` (current data state + a walkable **UI affordance tree**) and `act` (named **semantic intents** — `select_body`, `open_regional`, `set_overlay`, `back`, … — backed by the same handlers a click calls, **not** pixel coordinates; the map uses `_gui_input`). Runs headless. Turns human-eyeball review into an agent-automatable QA sweep across the whole Reach (and later economics/saves). **(5) Capture — reuse, don't reinvent.** Interactive screenshots reuse the **existing `tests/run-visual` capture primitive** (frame-delayed `save_png`, proven by `character_creation.gd`'s standalone capture). The agent channel adds *interactive* explore-and-capture; the existing scenario/flow **visual-golden regression** role stays distinct and unchanged. Clear hover state before capture. **Consumers** (build geography now; the others adopt the pattern in their own phases — do **not** build them now): Layer-1 geography (Phase 4) → economics (dynamic; pause essential) → save-game inspection (load → pause → inspect; `save_state.rs` substrate). **Amended 2026-07-13 (T-960 rescope — Si audit ratifications):** - **(a) Overlay mechanism simplified.** The per-button `pending`/locked + `notify_gen_layer_ready(id)` unlock mechanism in item (3) was never built; the shipped pattern — plain always-toggleable buttons plus a single per-body "generating" pending indicator — has carried two layer deliveries (the L1 triad, then T-1046's district grid) and is ratified as the standard. Stable `gen_*` button ids remain the contract. - **(b) `gen_l0_heightmap` dropped.** Relief already renders via the always-on `terrain` overlay (D-191 §7); a second generation-labeled L0 toggle would duplicate it. - **(c) Legend panel stands.** The left-side legend (shape/color key for attractor types and sub-biomes) remains an unshipped, in-scope deliverable of item (3)/T-960. - **(d) No tile-level Atlas map.** The planetary Atlas maps generation layers down to settlement/quarter-skeleton granularity only; chunk/tile/voxel fill (L5) is verified by the believability/derivation harnesses and inspected **in-world in Phase 5** — never as a planetary map layer (at most aggregate stats). Ratifies T-1046's implementation precedent; matching D-191 amendment + CLAUDE.md Phase-4 wording updated the same day. **Amended 2026-07-16 (T-1112 — coarse quarter-footprint Atlas layer, the L4 skeleton on the planetary map):** the fourth generation-cascade layer to land on the Atlas, sitting between T-1046's `district_grid` (D-239 coarse morphology) and item (d)'s hard ceiling. Design only — implementation is a follow-up ticket (touch points named in §4 below, none built here). - **(1) `QuarterFootprintLayer` data shape.** Source of truth is `BodyWorldState.quarters: BTreeMap` (`QuarterWorldState { skeleton: QuarterSkeleton, block_tags }`), where `QuarterSkeleton.blocks: [[BlockSkeleton; 4]; 4]` carries `zoning: ZoningType`, `district_type: DistrictType`, `density_pct: u8`, `landmark: Option` per block, plus `corridors: Vec` at the quarter level. Critically, **`QuarterWorldState` carries no independent spatial position** — `QuarterId` is a content-addressable hash (`SeedChain::for_body(world_seed, body_id).derive(SeedDomain::Layer4Quarter, city_id).seed()`, `plugin.rs::build_skeleton_work_item`), not a coordinate. The only spatial anchor a quarter has is the `city_id` it was generated for — which `SettlementLayer`/`CityPlacement` (T-960 §2, D-211) already carries as `(city_id, position)`. So the layer is **keyed by `city_id`, joined to a quarter by recomputing the same deterministic `quarter_id` derivation** the dispatch path already uses (a pure function of already-public inputs — no new field needed anywhere) and looking it up in `state.quarters`. This also means **no city-outline geometry is derived or served** — at planetary-map projection a 512 m quarter is roughly 1/80th of a heightmap pixel (a district ≈ 2048 m is already sub-pixel at ~40–78 km/px, D-243), so there is no real silhouette to trace; the layer is aggregate stats anchored at the existing L3 settlement position, mirroring `RoadGraphLayer`/`SettlementLayer`'s established "trim the internal struct to what an overlay needs" pattern rather than inventing outline geometry with no data behind it. Per settlement-with-quarters, the served aggregate is: ```rust pub struct QuarterFootprintEntry { pub city_id: u64, pub density_avg_pct: u8, // basis-point mean of BlockSkeleton.density_pct, 16 blocks pub dominant_district_type: DistrictType, // mode across 16 blocks; ties → lowest declaration-order variant pub dominant_zoning: ZoningType, // mode across 16 blocks; same tie rule pub landmark_count: u8, // count of Some(LandmarkSlot) across 16 blocks (max 16) pub corridor_count: u8, // QuarterSkeleton.corridors.len(), clamped to u8 } pub struct QuarterFootprintLayer { pub entries: BTreeMap, // keyed by city_id, D-010 determinism } ``` Five fields earn their place: `density_avg_pct` and the two dominant-mode fields are Araminta's color/shape encoding inputs (§3); `landmark_count`/`corridor_count` are inspection-only (the D-226(d) ceiling forbids them as a map-visible channel — they surface in the existing city-click sidebar instead, an `ImplantDataRow` addition, not a new draw call). Rejected from the set: per-block detail (violates the ceiling outright), `reservations`/`social_sites` counts (no consumer identified — Araminta's encoding doesn't need them and nothing else asked), and a float density (D-010 integer discipline — `density_pct` is already `u8` basis-point-flavored on the source struct, so the mean stays `u8`, no `f32` anywhere on the wire). `dominant_district_type`/`dominant_zoning` serialize as their named enum variant (serde default), following the `RoadGraphLayer`/`SettlementLayer` precedent (`RoadNodeKind`, `MaintenanceAuthority` — neither `repr(u8)`-pinned, serialized as names) rather than `district_grid`'s `as u8` byte-packing, which was specific to a dense `cols×rows` array where `MorphologyZone` is deliberately `repr(u8)`-pinned for that purpose; a handful of per-settlement aggregate fields have no such packing need. `BTreeMap` throughout for D-010 determinism, matching `block_tags`' own `BTreeMap<(u8,u8), _>` precedent on the source struct. - **(2) Hard ceiling (binding).** This layer is the concrete instance of item (d)'s "at most aggregate stats" clause: five scalar fields per settlement, quantized `u8`, no per-block zoning/street/tag detail ever reaches the wire, and no chunk/tile/voxel data is touched (this layer reads only `QuarterSkeleton`/`BlockSkeleton`, never `FillChunk` output — a different generation phase entirely, D-230). If a future ticket wants finer planetary-scale detail than this, the answer is "no" per item (d), not "extend this struct" — the settlement/quarter-skeleton granularity ceiling applies to this layer by construction, not by restraint that could erode. - **(3) Overlay encoding (Araminta).** New overlay id `gen_l4_quarters` (label `QTR`, `group: "toggle"`, matching the `gen_*` convention in `OVERLAY_DEFS`). **No outline is drawn** (per §1) — the layer is a **density-scaled glyph anchored at the existing L3 settlement position**, drawn in the same pass immediately after `_draw_gen_settlements` so it reads as "on top of" the city dot it annotates. Square side scales off `density_avg_pct` (e.g. `4.0 + density_avg * 6.0` px at 1.0 zoom, clamped `[4.0, 12.0]`) — dense build reads as a bigger block, sparse as smaller, without pretending to show real shape. **Shape carries identity, color carries intensity** (the same convention as `_sub_biome_color`/`MORPHOLOGY_COLORS`): shape = `dominant_district_type`, a small corner-notch glyph family on the filled square (plain = mixed/no clear dominant, corner tab top-right = commercial, corner tab bottom-right = industrial, small diamond cutout center = civic/landmark — capped at 3–4 variants, a coarse skeleton read, not a legend of every `DistrictType`); color = `density_avg_pct` on a single-hue intensity ramp within the existing settlement-gold family (`COLOR_SETTLEMENT` → `COLOR_SETTLEMENT_CAPITAL`-adjacent bright gold at high density), so the new layer reads as *part of* the settlement-marker family rather than a competing hue, since it always co-renders beside `gen_l3_settlements`. `landmark_count`/`corridor_count` are **not** a visual channel (§2's ceiling) — they surface as `ImplantDataRow`s in the existing city-click sidebar panel. Zoom gating reuses `SETTLEMENT_LABEL_MIN_ZOOM = 2.0` (no new threshold): below it, glyph draws at minimum size with color only (the notch is illegible at a few px anyway); at/above it, full size with the dominant-type notch visible. Legend entry (`GENERATION_LEGEND`): ```gdscript { "overlay_id": "gen_l4_quarters", "title": "QUARTER FOOTPRINT — L4 (color = density, shape = dominant type)", "rows": [ {"glyph": "▪", "color": Color(0.55, 0.48, 0.30, 0.6), "label": "low density"}, {"glyph": "▪", "color": Color(0.94, 0.82, 0.38, 1.0), "label": "high density"}, {"glyph": "◪", "color": Color.TRANSPARENT, "label": "dominant type (corner tab)"}, ], } ``` - **(4) Six wiring touch points (follow-up ticket, not built here).** `AtlasLayerResponse` gains `quarter_footprints: Option` + a `build_quarter_footprint_layer(state, placements)` function mirroring `build_district_grid`'s "empty source → `None`" contract (`server/src/atlas/layer_proxy.rs`); `ZoningType` gains `PartialOrd, Ord` derives mirroring `DistrictType`'s T-994 precedent (`server/src/simulation/generator.rs` — declaration order is not a stability-pinned wire format on either enum, so the additive derive is safe), which is what makes §1's lowest-declaration-order tie-break computable for `dominant_zoning` (PR #179 review: `DistrictType` already carries the derives, `ZoningType` does not — the tie rule itself is unchanged); `protocol.gd` passthrough for the new field (mirrors the existing `district_grid`/`road_graph`/`settlements` fields); a `gen_l4_quarters` entry in `OVERLAY_DEFS` (`client/ui/implant/apps/atlas/atlas_viewer.gd`); a `_draw_gen_l4_quarters()` function imitating `_draw_gen_district`'s "read `viewer.get_generation_quarter_footprints()`, guard on `Dictionary`, draw" shape (`atlas_marker_overlay.gd`); and the `GENERATION_LEGEND` entry above (`atlas_legend_panel.gd`). `layer_proxy.rs` was mid-concurrent-edit for T-1113's `region_grid` addition at design time — read-only pass, no conflict expected (both land as new sibling `Option` fields on `AtlasLayerResponse`, following the same one-field-per-layer pattern the growth-ceiling note on that struct already anticipates naming T-1112 and T-1113 as the last two candidates). **Amended 2026-07-18 (T-1124 — windowed district-resolution regional map, the jet-plane altitude between the planetary Atlas and the never-mapped chunk/voxel world):** a new Atlas *viewing mode*, deliberately not counted alongside item (3)/T-1112/T-1113's whole-body-layer sequence, because it is a different kind of thing from all of them — those are **whole-body layers** (computed once, cached per body, one screenful, `up_to`-gated but otherwise request-independent); this is a **windowed viewport query** (parameterized by a client-chosen rect, re-issued on every pan, never a whole-body snapshot). District resolution (2,048 m/cell, D-243) sits strictly between the D-226(d) settlement/quarter-skeleton ceiling and the never-Atlas-mapped chunk/voxel tier — in remit, and the first time the D-227 "invented deterministically" terrain (everything finer than the ~40–78 km/px heightmap) has ever been surfaced to a screen rather than a debug probe. Design only — implementation is a follow-up ticket (T-1124 report; touch points named below). - **(1) Request — extend `AtlasLayerRequest`, do not add a sixth inbound shape.** `bridge/mod.rs`'s `Inbound` demux doc names `BrowseRequest` (T-1131, PR #184) the **fifth and last** map-shape probe this hand-rolled scheme should ever carry; a sixth top-level request shape is explicitly forbidden without migrating the whole channel to the tagged-envelope framing D-225 deferred. The window parameters therefore ride on the **existing** `AtlasLayerRequest{body_id, up_to}` as new `#[serde(default)]` fields, absent = whole-body (today's behavior, byte-unchanged for every existing caller): ```rust pub struct AtlasLayerRequest { pub body_id: String, pub up_to: CascadeLayer, /// District-window centre (T-1124). `None` = no window requested /// (whole-body layers only, today's behavior). #[serde(default)] pub window_center: Option, /// Window side length in districts. Ignored when `window_center` is /// `None`. Clamped server-side to `[1, DISTRICT_WINDOW_MAX_N]` (§4) — /// never trusted from the wire. #[serde(default)] 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 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 { // ...existing fields unchanged... pub region_grid: Option, /// The requested district window (T-1124), or `None` when the request /// carried no `window_center` / no window data is cached yet for a /// pending body. Distinct from the five layers above: keyed on the /// REQUEST (body, center, n), not on the body alone — see §2. 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. **`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, pub n: u32, // window side length in districts (n × n cells) pub morphology: Vec, // MorphologyZone discriminant, 17-entry frozen vocab (D-239 §6) pub elev_q: Vec, // 0-100, matches DistrictGridLayer.elev_q encoding pub temp_dc: Vec, // deci-°C, REGION_TEMP_NONE_DC sentinel — same scheme as RegionGridLayer.mean_temp_dc, deliberately NOT a separate district-tier quantization (see rationale below) pub moisture_q: Vec, // 0-100, matches DistrictGridLayer precedent pub vegetation: Vec, // VegetationClass discriminant, 0-6 incl. Marine (T-1126) pub glaciation: Vec, // GlaciationGrade discriminant, 0-4 (T-1127, accepted — see §3) } ``` All six arrays are dense row-major `n × n`, same indexing convention as `DistrictGridLayer`/`RegionGridLayer` (`i = row * n + col`), built by iterating `derive_district` over `[center.0 - n/2, center.0 + n/2) × [center.1 - n/2, center.1 + n/2)` exactly as `aliveness_probe --render`'s `render_window_panels` already does — this design promotes that probe's window loop from a debug binary to a served layer, unchanged in mechanism. **Temperature stays `i16` deci-°C with the existing `REGION_TEMP_NONE_DC` sentinel**, not a new `u8` band-relative scheme (a live design-round proposal, overruled here): the district window and the region climate overlay (item T-1113) must share one temperature colorizer on the client, and D-243's edge-fuzz discipline ("climate does not change on a line") argues against two independently-chosen quantizations that could paint a visible ramp discontinuity at the region/district zoom-swap threshold — a rendering seam standing in for a data seam that D-243 explicitly rules out. The 1 extra byte/cell this costs over `u8` is immaterial at the window sizes in §4. - **(3) Field-list dispositions (mandatory per the T-1124 refinement).** Six fields ship, all already-derived `DistrictProfile` members with no new derivation logic: - **`glaciation_grade`: ACCEPT.** T-1127 (done, PR merged) explicitly deferred this field's wire half to this design pass and flagged the render pattern already works — the per-pixel ice tint on the morphology panel (`aliveness_probe::apply_ice_tint`) is production-proven, and `glaciation_grade` is already a first-class `DistrictProfile` field with a stable 0–4 discriminant. Shipping it as its own array (rather than baking the tint server-side into `morphology`) matches Araminta's encoding needs: the client can choose to tint, use a dedicated glaciation overlay, or ignore it, exactly as the probe renderer offers a sixth dedicated panel alongside the tinted morphology panel. - **`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. 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. **Corrected per PR #187 C1:** those per-window figures assume the body's `TerrainAnalysis` is warm — the **first** window on a body additionally pays one ~45 ms `run_layer1` derive (the analysis is deliberately transient post-cascade, D-203), memoized in a **lazy per-body LRU on the `GenerationQueue`** (capacity 8, ~2 MB/entry, browsed-bodies-only, true recency eviction; an eviction re-pays the ~45 ms on that body's next window). Every subsequent window on the same body — any `(center, n)`, not just exact repeats — hits the LRU and costs only the per-window pack. **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**: 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 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 >= Moderate` (alpha scaling with grade; `None`/`Light` draw no tint — `Light` is erosion signatures, not visible ice, per `apply_ice_tint`'s own gate, which this corrected prose now matches — PR #187 C4) 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. **Amended 2026-07-21 (T-1124 §5 entry revision — Jeroen, first companion hands-on):** the regional-map **entry mechanic changes from zoom-threshold LOD swap to explicit click-through**. Jeroen's ruling after using `make atlas`: the planetary pixel-scaling pan/zoom "is only messing with the pixels of the map and the interaction is weird" — (a) the **planetary heightmap view becomes FIXED** (no drag-pan / wheel-zoom of the planetary canvas; the current pan/zoom ships until T-1138 replaces it, then is removed *in the same change* as the replacement so close inspection is never stranded); (b) **entry is a click-through**: hovering the planetary heightmap shows a **rectangle cursor** representing the regional-mode bounds, and clicking descends into the regional map centered on the click point's derived `DistrictPos` — §5's float-on-center/first-window rules carry over with "pan center" read as "click point". `DISTRICT_WINDOW_MIN_ZOOM` is retired before ever being built (the T-1138 zoom-headroom note is moot); the §5 cross-reference reading of D-013 ("the zoom gesture owns spatial descent") is superseded **for this seam only** — click owns descent. Everything *inside* the regional mode stands unchanged (§4 pan-only refetch, debounce, float-on-center, D-227 cache, border-fade). A **morphing transition** between map modes is explicitly deferred (Jeroen: nice, too ambitious for now) — the descent may cut. **Open at T-1138:** the rectangle cursor is an affordance, not to scale — an n=64 window (~131 km) is a few pixels on a planetary canvas; the screen design must resolve the honest representation (rectangle at true extent with a zoom-in cut on click, or a not-to-scale reticle with the real extent labeled beside it) without implying the regional view covers more planet than it does. **Amended 2026-07-21 (T-1145 — Jeroen, second companion hands-on, KALLAST window):** three regional-window presentation fixes, all client-only. **Cover-fit supersedes contain:** `fit_window_view()`'s zoom now derives from the LARGER viewport dimension with no margin factor (`max(viewport.x, viewport.y) / composite_native`, not the old `0.9 * min(...)`), so the square district-window composite fills a wide/tall viewport edge to edge instead of leaving side margins, with the shorter axis' data extending into pan-space (the same "cover" concept as CSS `object-fit: cover`) — the existing §4 pan-edge refetch is unaffected (it keys off the screen-center-to-DistrictPos mapping, which any fit already centers on `_held_center` by construction, so no refetch churn at rest). **WASD + edge-scroll supersedes drag-pan:** LMB-drag panning is removed entirely (Jeroen's ruling — drag conflicts with click semantics for the map objects, e.g. settlements, this window will host later); panning is now held WASD/arrow keys (continuous, frame-rate-independent, `_process`-polled, physical-keycode reads to stay independent of the project's existing `move_north`/etc. gameplay-movement InputMap actions bound to the same keys) plus edge-scrolling (cursor within ~24px of a viewport edge, suppressed over UI and while the OS window lacks focus); wheel zoom is unchanged; pole-wall (§5 amendment above) and east-west wrap (T-1142) semantics are preserved unchanged under the new input source. **Smoothing is an interim presentation, pending T-1143:** the composite renders as an `n`×`n` `Image`/`ImageTexture` (one pixel per district, the identical existing per-cell color pipeline) drawn scaled with linear filtering — the same treatment the planetary heightmap already gets — instead of `n`×`n` flat rects, so GPU bilinear sampling reads as a terrain gradient rather than hard blocks; the original crisp per-cell path survives behind a compile-time const specifically so T-1143's design pass can compare both directly, and this smoothing is **not** T-1143's answer to district-tier legibility, only a stopgap ahead of it. **Amended 2026-07-21 (T-1143 design-pass rulings — Jeroen, after the zoom-ladder design pass, `docs/architecture/atlas-zoom-ladder-t1143.md`):** three rulings on the pass's reserved decisions. **(1) The item-(d) ceiling is opened for the Atlas ladder** — Jeroen: *"we set a new BHAG so old restrictions are up for debate."* The D-166 2026-07-21 zoom-ladder condition ("down to tile scale") is read **literally**: the Atlas windowed viewport may descend below quarter (512 m) toward block/tile granularity. This is an explicit ruling, not erosion — exactly the deliberate revisit the T-1112 §2 anti-erosion clause was hardened to force into the open. Item (d)'s substance survives in narrowed form: chunk/tile/voxel output still never appears as a **whole-body planetary map layer**, and the below-quarter rungs are implementation-gated on their own **measurement pass** (costs/wire for block and tile rungs are unmeasured — design pass §2/§7); the harness-verification path for L5 fill remains primary until that pass lands. **(2) Planetary rung wire carrier: progressive capped-density tiling** riding the generalized `district_window` carrier (granularity parameter, §3 of the design pass) — no new dense-raster wire shape, no forced tagged-envelope migration. **(3) The §5 entry click-through cut is superseded by continuous cursor-anchored zoom**: wheel-zoom carries the view from the orbital frame down through regional granularities continuously, anchored at the cursor, **with the condition that a full zoom-out resets to the original canonical planetary frame and location** (the fixed orbital framing is the ladder's top rest state, not a drifted pan state). The click-through descent and rectangle reticle are retired as the *sole* entry (T-1138's shipped mechanic stands until the continuous ladder replaces it in the same change — close inspection is never stranded, same discipline as the §5 fixed-view transition). D-013's "the zoom gesture owns spatial descent" reading is **restored** for this seam. **Wire-contract note (T-1150, PR #191 review — Tyre):** the `window_granularity` field that ruling (2) rides on expresses **finer-than-district integer multiples only** (1 = district, 4 = quarter today; each new rung is a deliberate widening of `resolve_window_granularity`'s whitelist — the single widening point; unknown values fall back to district, never trusted from the wire). Coarser-than-district reuse (the region/orbital rungs of ruling (2)'s progressive tiling) requires the design pass's R5 signed/log-scale-or-enum redesign of the field — a new magic value is not the path. Recorded here so the type's limit is contract, not only a design-doc risk row. **Refinement-semantics note (T-1153, PR #192 review — Tyre):** the ladder's progressive cross-rung refinement (hold the coarse composite, fetch the finer rung, swap in place on arrival; per-tile arrival in the orbital mosaic) **extends** the T-1124 §4 float-on-center/debounce async contract — it does not supersede it. §4 still governs the per-request mechanics unchanged (`district_window: None`-until-derived polling, the 150 ms debounce, float-on-center refetch); rung crossings add a second request class on top, per the design pass §3's progressive-refinement model. This record is the one that governs the swap-on-arrival behavior. The legacy `window_granularity: u32` wire field is now fully shadowed by `window_granularity_v2` (the server always echoes both); it is **scheduled for retirement** once pre-T-1152 wire-compat is confirmed unneeded (single-repo client/server pair — no external clients exist today; ticketed). **Pending-shape protocol note (T-1163, PR #193 review — Tyre):** `AtlasLayerResponse` has **two legal wire shapes for one logical "still deriving, client must re-poll" state**, and both are contract: whole-response `status: Pending` (whole-body cache cold — nothing about this body computed yet) and `status: Ready` with `district_window: null` (body warm, this window still in the derive queue). `Ready` is set **only** by the whole-body cache-hit branch, independent of the window's own derivation. Any `AtlasLayerResponse` consumer must treat BOTH shapes as retry-with-backoff and only `NotFound`/`Error` as terminal — the T-1163 cold-launch starvation (every first launch black) was precisely a client reading `status != Ready` as ignorable. A server refactor that "cleans up" the Pending/Ready-null asymmetry must migrate every consumer in the same change. **Filter-axis note (T-1161, PR #194 review — Tyre):** COMPOSITE_SMOOTH is retained as the compile-time *pipeline* axis (texture vs. per-cell rects, crisp path kept for debug/compare); the ladder's crispness-at-sparse-rungs requirement is met by the per-rung *sampling-filter* policy (`_filter_for_granularity_v2`: Region/orbital-mosaic NEAREST, District/Quarter LINEAR, unknown falls back LINEAR), **not** by deleting the const. The design pass §8 step 6 "retire COMPOSITE_SMOOTH" is errata'd accordingly; T-1155's retirement framing is cancelled, superseded by T-1161. **Wave-1 nature-overlay carrier note (T-1156, 2026-07-23 — Tyre):** river skeletons (rivers/basins/attractors) ride the Atlas zoom ladder on the **existing whole-body `layer1` field** (`RiverNetwork`/`drainage_basins`/`attractors`, already serialized on every `AtlasLayerResponse`), **not** on the windowed `district_window` carrier — so the windowed-family ceiling (§2, [HARD], exactly one windowed field) is untouched and no tagged-envelope migration is triggered. Rationale: the skeleton is discrete vector geometry ((u16,u16) cell chains, boundary polylines, point attractors), computed once per body in the Layer-1 pass and cached "valid forever" (D-227) — categorically the whole-body family, not a per-pan viewport query. Rasterizing rivers into `district_window`'s per-cell arrays is rejected: a 1-cell-wide thalweg is sub-cell at every ladder rung (512 m/cell and coarser), so a presence-byte either over-fattens (the Lendel failure the ladder mandate kills) or drops the river on the sampling grid. **Per-rung refinement is client-side** (trunk at Region → +tributaries at District → +streams at Quarter), a filter on a **new quantized `river_class` per cell added to `RiverNetwork`** (derived from the flow-accumulation `drainage.rs` already computes; additive, `#[serde(default)]`-safe, no ceiling impact) — NOT server-side per-rung re-transmission. This is distinct from T-1162's server-side `min_wavelength_m` cutoff: that truncates a continuous per-metre field at the rung's Nyquist limit; the skeleton is a fixed finite graph with nothing to truncate. **General rule established:** discrete map features (linear + point geometry) ride the whole-body overlay family, filtered per-rung client-side; only continuous per-metre fields ride the windowed per-cell arrays — Wave 2's roads/rail/settlements (already whole-body fields) inherit this carrier unchanged. **Consistency scope:** the river skeleton is upstream of and independent from T-1162's perturbed `moisture_q` (drainage runs on elevation, never samples moisture), so the two cannot disagree; the vegetation layer's riparian response to rivers (`near_perennial_water`) is a **named forward contract, deferred to T-1168** — not fixed in Wave 1. Until it lands, the river overlay draws over terrain whose vegetation layer does not yet respond to it (an accepted nature-layer-first gap). **Visibility-direction note (same PR, Araminta):** the per-rung visibility DIRECTION is Araminta's presentation ruling on the 76 km skeleton-resolution evidence — **fade-down** (Region shows the full skeleton, District trunk-only de-emphasized, Quarter off), consciously inverting the provisional add-as-you-descend mapping the ticket brief carried. This posture is explicitly **pre-T-1170**: it is revisited (in `RIVER_CLASS_VISIBLE_BY_RUNG`, the single client-side revisit point) when course invention gives finer rungs real geometry to reveal. **Two-waterline note (T-1172, 2026-07-23 — Tyre):** the river skeleton is extracted against the **raw heightmap sea level** (`drainage.rs`), a rung-independent graph; the drawn coast is the **derived morphology verdict** — which is **rung-dependent by construction** (the coast-warp crinkle, `coast_invention.rs`, adds octaves at finer rungs, so the drawn coastline is a *family* of curves indexed by rung, not a single curve). There is therefore **no single authoritative server-side waterline** to reconcile the skeleton against — a server-side classification would bake one rung's coast into the wire and be wrong at every other rung. Reconciliation is a **presentation-frame** operation: the draw site clips river dots/confluences/mouths against the arrived composite's per-cell water verdict at the rung being painted (drop-in-water, no snap). The skeleton stays rung-independent (its correct nature per the carrier note); the clip is retired into T-1170 when course invention terminates courses at the invented coast with continuous geometry. **Course-invention carrier note (T-1170, 2026-07-23 — Tyre, full ruling in docs/architecture/river-courses-t1170.md):** river course geometry below D8 resolution is **invention, not skeleton**, and rides the **windowed payload** as a vector field inside `DistrictWindowLayer` (`courses`, additive) — invented server-side per window on the background queue (T-1137 discipline) at the window's rung, terminated server-side against the same rung's morphology water verdict the window's cells carry. This does **not** count against the windowed-family ceiling (§2 [HARD]): the ceiling counts windowed *query* fields on `AtlasLayerResponse`; courses are content of the single existing windowed payload, same echo key, same staleness semantics. The wave-1 carrier rule is hereby refined three-way: **(i)** rung-independent discrete features (skeletons, graphs, markers — computed once, valid forever) ride the whole-body family, filtered per-rung client-side; **(ii)** continuous per-metre fields ride the windowed per-cell arrays; **(iii)** rung-indexed invented detail rides the windowed payload regardless of geometric kind, because rung is a request parameter and only the windowed query carries one — the coast crinkle has always implicitly been (iii); courses are its vector sibling. The two-waterline note's reconciliation resolves per-rung server-side for courses (the terminus is windowed content at a known rung); the Region-rung skeleton path retains the presentation-frame clip until Region itself goes windowed (T-1143 ruling 2), which retires the last clip site. The skeleton itself gains `river_downstream` (additive per-river-cell D8 pointer with `MOUTH`/`EDGE_DRAIN`/reserved-`TERMINAL` sentinels) on `RiverNetwork` — whole-body, rung-independent, per rule (i). Pole-edge drains are grid artifacts, not mouths — excluded from `mouths` at extraction. **Implementation notes (PR #197 review):** (a) Godot's line rasterizer floors stroke widths below ~1.0 canvas units to a 1-px hairline; the client floors compensated stroke widths accordingly (`zoom_compensated_stroke_width`), with the consequence that Ruling 5c's **per-class width differentiation is inert at every shipping fit zoom** — course classes are distinguished by opacity alone until T-1175's per-vertex tapering (Polygon2D strips) supersedes stroke-width rendering; the 'trunk widest' promise is design intent, not current pixels. (b) The pole-row edge-drain branch is **structurally unreachable** (`flow_direction` bounds-checks before assignment); the real EDGE_DRAIN mechanism is the interior `k<0` no-valid-downstream case — discovered by revert-verification, test fixture exercises the reachable path. **Amended 2026-07-23 (body-map-viewer workshop — stepped render architecture; full record [D-255](#d-255)):** the T-1143 design-pass rulings above (2026-07-21) are revised by the body-map-viewer workshop, which relocated content determination to the server, made the client a map-art function, and made zoom stepped. Six consolidated revisions to this record's amendment chain: - **Ceiling re-scope (§2 windowed-family ceiling, [HARD]) — tagged-envelope migration TRIGGERED, superseding T-1143 ruling 2.** T-1143 ruling 2's "progressive capped-density tiling... no forced tagged-envelope migration" is superseded: measurement (T-1179) shows the step-canvas payload at **21×–563× the ~30 KB windowed-payload reference** across the three canvas sizes — a cell-count gap of two-to-three orders of magnitude no encoding closes, so it cannot ride the `district_window` carrier. The ceiling's **purpose survives** (no uncorrelated concurrent windowed *queries*); its **mechanism is re-scoped** to the **legacy `district_window` carrier only**. The step canvas rides a **new tagged-envelope carrier** (D-225's deferred migration, now executed — [D-255](#d-255) §b) that the "exactly one windowed field" rule does not apply to by construction (it is the new shape, not a second field on the old). `district_window` survives byte-unchanged for its existing consumer until that consumer is replaced, then goes cold. - **Rung selector superseded (T-1143 §6 `select_rung` / `MAX_COVERAGE_M` coverage-walk).** The coverage-ceiling walk (Quarter→District→Region→tile-mode mosaic; `compute_tile_grid`) is replaced by a **discrete step index** into the stepped gridunit ladder ([D-255](#d-255) §c). Rungs-as-derivation-granularity survive; the *selector* is "which step the viewport is on," not a coverage walk. `resolve_window_granularity`'s whitelist-validation discipline (T-1150) is the right shape for the step index's own validation. - **Zoom transport superseded (T-1143 ruling 3).** Continuous cursor-anchored zoom → **stepped** (one server-canvas fetch per crossed step boundary). Survives unchanged: cursor-anchored centering, edge-scroll pan, and the [HARD] full-zoom-out reset (now to the canonical **Global rung-0 body-surface frame**, per [D-255](#d-255)). - **Item (d) restated per-request + accumulation cap.** The item-(d) whole-body prohibition is stated as a **per-request / per-derivation** constraint, not aggregate-storage: no single `StepCanvasRequest` derives whole-body coverage at sub-Region spacing. The client-side persistent-cache *accumulation* path (a systematic exhaustive pan assembling the forbidden artifact as N cached files — most plausibly the item-(4) `AtlasAgentInterface` QA sweep) is closed structurally by a **per-body deep-rung client-cache retention cap** ([D-255](#d-255) cache section), not by practical improbability. The rule's purpose is about information content, not file count. - **Item (d) floor partial-restore at chunk (Jeroen, interview 2 — narrows T-1143 ruling 1).** T-1143 ruling 1 opened the item-(d) floor "toward block/tile"; interview 2 **scopes that opening — it does not reverse it.** Jeroen: *"the actual tile level rung seems unusable. maybe replace with 64?"* The Atlas ladder bottoms out at **chunk (64 m)**; **tile/voxel (1 m) returns to never-Atlas-mapped** (a 10-px-per-tile full-screen view is ~192×108 m of ground — Phase-5 in-world content, not a map). Block (128 m) and chunk (64 m) are legal Atlas rungs, riding the viewport-sized carve-out; tile/voxel L5 fill stays harness-verified, shown in-world in Phase 5, exactly as item (d) originally required. A deliberate scoping of the BHAG, not a walk-back. - **Courses carrier gloss + cliff sparse-list sibling (river-courses-t1170.md rule (iii)).** The T-1170 course-invention carrier rule survives; "the windowed payload" is repointed to "the per-step data-canvas payload (formerly `district_window`, now the tagged step-canvas envelope)." Rule (iii) (rung-indexed invented detail rides the windowed payload regardless of geometric kind) gains a **second vector member alongside `courses`: `cliffs: Vec`** — carved-gorge geometry (dominant `elevation` + per-segment `channel_depth` + `cliff_edge`, direct solver-output carry), a **sparse list, zero-length when nothing is carved** (T-1177's population survey: **zero carved cells across all 267 real committed bodies** — structurally rare, `#[serde(default)]`). **Phase-4 Atlas scope** (a map showing a smooth shoreline where the settled solver computed a carved channel would misrepresent the "settled hydrology" the workshop premised). The cliffs list is NOT the endorheic-lake cue (0/267 carve — see [D-227](#d-227)'s lake amendment). - **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`. - **Implementation:** Phase 4 (epic T-750), built bottom-up — auto-pause substrate, T-969 proxy (D-225), T-960 viewer, agent channel, agent capture. Geography is the first consumer. - **Raised by:** Jeroen + Claude (design), with Tyre (channel/pause/headless architecture) + Araminta (overlay encoding + affordance UX), 2026-05-24. - **Cross-reference:** [D-225](#d-225) (layer-stream proxy — the data path), [D-166](#d-166) (per-layer Atlas progress viewer), [D-191](#d-191) (Atlas viewer), [D-169](#d-169) / [D-170](#d-170) (implant components / HUD occlusion — `gameplay_occluded` trigger), [D-200](#d-200) / [D-203](#d-203) (execution tiers / LRU cache), Q-099 (mod content catalog), `tests/run-visual` (capture primitive), `save_state.rs` (save-inspection consumer). **T-1112 amendment additionally:** [D-222](#d-222) (Quarter terminology — the 512m unit this layer surfaces), [D-234](#d-234) (footprint geometry — the block-subdivision source the aggregates summarize), [D-243](#d-243) (quarter = 512m rung, and the containment ladder that makes a quarter sub-pixel at planetary projection — Araminta's no-outline rationale), [D-010](#d-010) (determinism — integer-only aggregates, `BTreeMap` keying). **T-1124 amendment additionally:** [D-227](#d-227) (derive-don't-store + the "invented deterministically" clause this design is the first to surface on a screen; determinism is what makes the echoed-center staleness guard and the client-side window cache both sound), [D-243](#d-243) (district = 2,048m rung — this is precisely the D-226(d) ceiling's floor, the regional altitude above quarter-skeleton and below the never-mapped chunk/voxel tier; edge-fuzz discipline — the temperature quantization-consistency rationale), [D-239](#d-239) §6 (the frozen 17-zone `MorphologyZone` vocabulary this layer serves unchanged) / §8 (vegetation climate law, amended by T-1126's `Marine`), D-225 extension / T-1131 / PR #184 (the five-map-shape demux ceiling — why the window rides on `AtlasLayerRequest` rather than a new inbound shape), [D-010](#d-010) (wire-integer discipline — all six per-cell fields are integer/quantized, no `f32` on the wire). §5 (screen) additionally leans on [D-191](#d-191) (the `AtlasViewer` whose `_view_zoom` LOD vocabulary and `SETTLEMENT_LABEL_MIN_ZOOM` threshold the district window extends in place), [D-169](#d-169) / [D-170](#d-170) (implant chrome / theme accent-role discipline — map palettes stay out of `ACCENT_ACTIVE` gold the settlement marker owns), and [D-013](#d-013) (diegetic navigation — the zoom gesture owns spatial descent, so it is not overloaded onto the city-click sidebar). Tickets: T-1123 (the `derive_district` window-render precedent this design promotes to a served layer), T-1127 (glaciation_grade derivation + probe render pattern, wire half deferred here and now accepted), T-1126 (`VegetationClass::Marine`), T-1118 (region-grid climate overlay — §5's temperature/moisture overlays reuse its ramp so one colorizer spans both zoom levels), T-1119 (concurrent `quarter_footprints` wiring — the sibling whole-body layer this design's §2 distinguishes itself from). - **Dissent:** None - **Amendment (2026-07-25, T-971 / PR #209 — agent-channel vocabulary reconciled to the stepped Atlas):** the layer-4 `AtlasAgentInterface` ships with its intent vocabulary re-derived against the post-[D-255](#d-255) stepped Atlas, superseding this record's original list. **Dropped:** `select_city` and `open_regional` — post-D-255, settlements are `settlement_id` canvas cell values with no hit-test affordance (no server get-settlement-by-id, no client hit-test; a settlement-selection intent becomes its own ticket when a real consumer exists), and Region→Chunk is one screen (descent is `scroll_rung`, not a screen push — `open_body` replaces `open_regional`). **Added:** `open_atlas` (a driver must be able to start the session), and `jump_to_center` — the fixed-center revisit pattern proven by the T-1157 eyeball drivers, made first-class via `StepCanvasViewer.jump_to` (same request tail, extent cap, and cache keys as cursor navigation, so an agent cannot request a state a player couldn't). `get_layer_data_summary` reports the real `EncodedStepCanvas` fields (courses-by-class, cliffs, draw-matched settlement counts) — the original attractor/river/basin-count phrasing was retired-AtlasViewer vocabulary. **Transport narrowed:** in-process consumers only (gdUnit, `-s` drivers, the T-1157 capture harness — the committed reference driver replaces the scratch eyeball drivers); the original curl/terminal remote transport is a follow-up when a remote consumer exists. Full handler-by-handler mapping on T-971. --- ### D-227: Deterministic-rebuild world model — derive-don't-store, cache, tile mutators, volumetric - **Date:** 2026-05-25 - **Decision:** The walkable world is a **pure function of `seed + atlas`**, materialised on demand and cached, never persisted as a tile grid. - **Derive-don't-store.** `subtile(x,y,z) = derive(seed, atlas, position)` — deterministic (D-010), recomputed on demand, held in a transient by-chunk/region cache for performance, evictable (eviction → recompute, always valid). No generated tile/voxel is ever saved. - **Volumetric, in voxels.** Unit vocabulary, fixed here: a **voxel** is the 1 m cube (the 3-D framing of D-222's tile); a **subvoxel** is its 0.5 m subdivision — eight per voxel, the finest derivation/dig granularity, in all three axes. Derivation is 3-D — surface *and* subsurface — over this grid. The heightmap/atlas sets the top; below it a deterministic geology model (strata by depth → bedrock; per-region lithology; ore / aquifer / cave as 3-D noise) fills downward. Digging reveals `derive(...)` for newly-exposed subvoxels — never "generated on dig"; the hole merely stops hiding seed-math. - **Floors are semantic and variable-height, not a fixed voxel count.** A *floor* — a walkable story, the z-level of D-049 / D-110 — groups voxels and **defaults to 3 voxels (3 m)** but is **not locked**: a cathedral nave or a hangar is one ~10 m floor, and that must be expressible. The voxel/subvoxel substrate is uniform and continuous in z; a floor exists wherever the volume is **walkable void**, and that void can be **built** (buildings), **excavated** (basements, mines), or **natural** (caves, lava tubes, caverns — derived in the geology model as 3-D voids). A cave is as much a floor as a basement; only truly solid rock is floorless. z-level addressing indexes stories whose real height in voxels varies. - **Vertical extent is physical, not a floor count.** The old "±50 floors" framing was a *stored-grid* artifact (each z-level was a filled, stored layer with memory/fill cost). Derive-don't-store eliminates that cost, so: **down** is bound by the body's own geology (crust → bedrock → pressure/heat-impassable interior, a per-body depth tied to radius / `max_elevation`); **up** is bound by *max built-structure height* (towers are built/authored or player-placed, not natural-derived) over free-to-derive air. This **supersedes any ±50 cap** — none was ever recorded as a D-record; it survived only as a 50-floor-skyscraper *example* in the generator-architecture workshop. - **Persistence has two parts, and only one is mutators.** (a) **Tile mutators** capture *direct physical gameplay* — the player or sim physically altering the world (pave a road, build a wall, dig a strip-mine, rocket-launcher the bank, fell a tree). Save = `seed + sparse mutator log`; load = re-derive base + replay. (b) **Live simulation state** (the rolling economy now; NPC / storyteller state later) is *history*, not seed-derivable, so it **is saved — but as sim-state, not as mutators.** The *derived* economic base is handled by **cache expiration** — recomputed on expiry, and **force-evicted on an event** (e.g. the player tanks a system's economy) — while the rolling state persists alongside the save. The physical world stays re-derivable; the living state is the small saved delta on top. (Mutator op schema → Q-103.) - **Determinism reclassified safety-critical.** Because mutators reference derived state, any derivation drift (a non-deterministic algorithm, an f32 comparison/ordering, `HashMap` iteration) desyncs the whole save — not merely a cosmetic difference. D-010's integer-only + ordered-collection discipline is load-bearing for *saves*, not just for golden tests. - **Rationale:** A world that stores its tiles cannot scale to body-sized 3-D volumes and bloats saves; a pure-function world with a transient cache + a sparse mutator log scales to any size, makes saves trivially small, and is the only model under which "dig anywhere, to any depth" is free (the subsurface was always computable — digging just reveals it). It also forces the determinism discipline the whole cascade needs anyway. The downward floor cap fell because it was solving a problem — per-layer storage cost — that derive-don't-store eliminates. - **Open sub-questions:** the geology-model fidelity (simple depth-horizon stack vs tectonic-grade folding/faults) and how far `FloorMaterial` is derived now vs deferred to the city layers (both tracked in D-228 / Q-101); the mutator op schema (Q-103). - **Implementation:** Phase 4+ (epic T-750). The caching substrate exists at the atlas level (`BodyWorldStateCache`, D-203/D-225); the tile/voxel cache, mutator overlay, and geology derivation land as the cascade reaches the tile layers. No migration needed pre-save (D-202 `schema_version` lineage covers future drift once saves exist). - **Amended 2026-07-17 (T-1125 — the invention carries geographic content into district classification; two-tier driver model):** the "invented deterministically (interpolation + domain warp + detail-scatter)" clause is now implemented *with character* at the district tier, closing the T-1123 finding that classification consumed only the raw bilinear envelope (pixel-smooth coasts; scatter amplitude slaved to coarse heightmap slope ≈ 0 exactly on low-relief coasts). Mechanism (`atlas/coast_invention.rs` + `district_profile::invent_primitives`, shared by the on-demand `derive_district` AND batch `derive_district_profile` paths so they can never silently diverge): **(a) coastline domain-warp** — every envelope field (elevation, slope, ocean mask) is bilinearly sampled at the same warp-displaced position (C¹ multi-octave value noise, ≈16–262 km band, sub-pixel amplitude cap 0.75 px, distinct salted hash stream — never correlated with the terrain scatter or climate edge-fuzz), inventing bays/capes/fjord inlets while the heightmap stays the truth at its own scale; **(b) slope-independent scatter floor** — the detail-scatter envelope gains a character-driven floor (invention no longer collapses on flat coasts; the old "flat envelope → zero invention" reading is superseded — the envelope rule survives as a *ceiling*: gentle bounded relief, never mountains on an authored plain); **(c) shoreline carving** — ridged character contributes real slope in shoreline patches so the steep coastal families (Fjord/CliffCoast) can fire where glacially/tectonically justified. **Crinkle varies (Jeroen's ruling): two driver tiers, zero new authored data.** Tier 1 (body personality envelope): `planet_class` (via `TectonicClass`) + `hydrosphere` + `atmosphere` + `body_radius_km` (via the pixel⇄metre seam) only — **D-240 stands: no orbital/tilt inputs**; erosion-proneness is *derived* (more ocean → wetter/rainier → higher erosion → smoother mature coasts; dry/thin-atmosphere → sharp young coasts). Tier 2 (position): latitude, driver-tier `GlaciationGrade` (fjordy high-latitude glaciated coasts), local wetness, and a seeded ~100–400 km heterogeneity field so stretches of the same coast differ; longitude participates via absolute world-metre noise keying. **Circularity ruling:** the driver-tier climate (glaciation/moisture) reads the *unwarped* raw-bilinear primitives — one-step-stale by design, documented at the call site. All pure `(seed, body, position)` (D-010); character never steps on a district/region line (D-243 edge-fuzz discipline). - **Amended 2026-07-23 (T-1170 — river courses join the invention family; full ruling in docs/architecture/river-courses-t1170.md):** the "invented deterministically" clause now covers linear features: river courses between D8 cells are pure `(seed, body, edge, rung)` functions — a rung-independent valley-seeking coarse path (bilinear `TerrainAnalysis` proxy, never full re-derivation) plus rung-indexed perpendicular warp octaves under the `min_wavelength_m` truncation discipline, distinct salted stream, amplitude tapered to zero at cell-centre anchors (confluence continuity), stations at global arc-length positions (window-independent). Round-1 relief reconciliation is course-follows-terrain (valley preference); terrain-carves-for-course is the deferred converse, targeted at block/tile rungs. - **Amended 2026-07-24 (body-map-viewer workshop — four cache/derive additions; full record [D-255](#d-255)):** the derive-don't-store model gains four additive amendments from the body-map-viewer render architecture. All four leave derive-don't-store's core intact; none makes any derived value a function of mutable state. - **(1) Map-time TTL-split + staleness-vs-storage as distinct eviction axes (Jeroen).** The Atlas map shows current state via a **TTL-split**: static geometry (morphology/elevation/moisture/vegetation/glaciation/height — everything this record covers) is cached **indefinitely-fresh** (determinism → re-derivation is byte-identical, so "stale" does not apply); **sim-state planes** (frozen/flooded, [D-253](#d-253)-driven) are separately-cached **short-TTL** planes re-requested as sim time advances. Two distinct eviction axes (Jeroen, verbatim: *"we still may also want to evict non global level geometry based on time to save storage for planets the player visits but never goes back to"*): **staleness-eviction** applies only to sim-state planes (they genuinely go stale); **storage-eviction** applies to all sub-global cache entries including geometry — evicted on time-since-last-visit purely as a **storage-budget policy**, not because the data is wrong (a re-derive on next visit is byte-identical and cheap). The **global tier alone (the rung-0 Global body-surface canvas, [D-255](#d-255) §c) is keep-always**, exempt from both axes. - **(2) Persistent client-cache schema/version tag (the one place [D-192](#d-192)'s co-ship guarantee does not reach).** [D-255](#d-255)'s disk-backed client cache (`user://atlas_cache/`) survives a game update — a cache file written by version N read back by N+1 crosses a version boundary D-192 assumes away for the live wire. **Requirement:** every persistent cache entry carries a **schema/version tag** (the game's `project.yaml` version or a `generator_sha`-style stamp), checked at read time — a **mismatch is treated as a cache miss, re-fetched, never decoded**. Consistent with this record's "cache never truth, evict → recompute always valid": a version-mismatched entry is just another eviction case. - **(3) Seed-chaining as a "cache-accelerated pure function" (Jeroen ratified — [D-255](#d-255) §a.10).** A finer step may **consume a coarser step's resolved output** (the outline's "serves as seed information for the deeper cascade") **without weakening derive-don't-store**: the DEFINITION stays pure (`derive(seed, position)`, byte-identical every time); the IMPLEMENTATION may read a **resident coarser canvas as an OPTIMIZATION** with a **derive-fresh fallback**. This is an optimization, not a semantic dependency, because the coarser canvas is itself evictable derived data — a pure function of the same seed — so correctness never depends on the cache being warm (evict → derive fresh → **byte-identical** input, this record's own "eviction → recompute, always valid" test). What is consumed is the coarser rung's **continuous primitive baseline** (the shipped district-reads-region pattern), **never** its resolved categorical classification (that would violate the per-rung dominant-mode re-derivation discipline). A **mandatory determinism test** (cache-hit path == cache-miss path, byte-exact) is the correctness gate; the ①②③ benched costs are the **cache-cold worst-case ceiling** (the fallback path), so the shipped system is never slower than measured. No chain-reaction on eviction: each rung's fresh-derive fallback is self-contained (`derive(seed, position)` takes nothing but those two inputs). - **(4) Lakes sourced from settled hydrology (data-source fix, no wire/vocabulary change — [D-255](#d-255) §a.11).** `MorphologyZone::Lake` (discriminant 1, already in [D-239](#d-239) §6's frozen 17-zone set — **not** a new zone) is sourced from `HydrologyResult`'s (T-1177) settled-equilibrium basins instead of the crude `ocean_fraction_q >= 60` heightmap heuristic, falling through to that heuristic where no basin exists. Water derives from the **continuous `filled_scaled` field sampled bilinearly per rung** (mechanism B above — the same way `sea_level` is sampled), **not** projected basin-cell membership (which would give a blocky, non-refining lake edge — the D-166-corollary magnified-composite artifact); so lake edges *refine* with zoom. Lake basin geometry is **static** (settled equilibrium, cached indefinitely-fresh), distinct from the short-TTL flooded sim-state plane of (1). **Endorheic-vs-overflow reads via OUTFLOW-COURSE PRESENCE** — an overflow basin's outlet edge appears in `courses` (T-1170), an endorheic basin's does not — **no wire bit, no 18th zone** (proportionality: 4.63% endorheic = 1,030/22,270 basins does not justify permanently widening the frozen vocabulary; the outlet-wiring is required regardless for hydrologic honesty; and the inference is exact — every `Overflow` basin has a non-empty `outlet_path`, every `Endorheic` none). The cliffs list is NOT the cue (T-1177 survey: zero carved cells across all 267 real bodies). **Honest sequencing:** until the basin-outlet→D8 wiring ticket ships (additive, pre-cleared by T-1170 Ruling 7b's reserved `TERMINAL` sentinel), the map shows lakes but not the drains-vs-closed distinction. - **Raised by:** Jeroen (derive-don't-store, volumetric, drop-the-floor-cap directives) + Claude, atlas-derivation workshop, 2026-05-25. - **Cross-reference:** [D-010](#d-010) (determinism — now save-critical), [D-222](#d-222) (subtile/tile/chunk hierarchy), [D-110](#d-110) (signed z-levels), [D-225](#d-225) (layer-stream proxy + cache pattern), [D-203](#d-203) (LRU cache tier), [D-224](#d-224) (SeedChain — feeds `derive`), [D-228](#d-228) (composite tile schema — the derived value type), [D-253](#d-253) (transient sim-state — the short-TTL plane the map-time split carries), [D-255](#d-255) (body-map-viewer render architecture — the four amendments above), [Q-101](../questions/architecture.md#q-101) (refinement contract), [Q-103](../questions/architecture.md#q-103) (mutator op schema), [Q-104](../questions/architecture.md#q-104) (floor↔voxel-z mapping) - **Dissent:** None --- ### D-228: Composite tile schema — orthogonal axes, derived shape, region-level morphology - **Date:** 2026-05-25 - **Decision:** A tile's "type" is **not a flat enum** but a small bundle of **orthogonal axes** (all *derived* per D-227 — this is the cache's value type, not stored state). Named types ("fjord wall", "mountain pass", "river bank") are **derived display labels, never stored**: a fjord wall and a sea cliff are identical to the simulation (`Cliff + Rock + DeepWater`), differing only in name. - **Per-subtile axes:** `TerrainMaterial` (**permanent** natural ground — Soil / Sand / Gravel / Rock / Wetland / Lava) · `FloorMaterial` (built surface over the ground — None / Concrete / Pavement / Carpet / Metal / …) · `Vegetation` (ground cover — Barren / Grass / Scrub / Thicket / Forest / Crop / Cleared / …) · `Water` (local depth *state* — Dry / Shallow / Deep) · `elevation` (scalar, metres). **`Snow` and `Ice` are not `TerrainMaterial`** — they are seasonally dependent, so they live in the seasonal **cover** overlay (Q-105), present only when the region's seasonal state puts them there (permanent only where climate never melts them — poles, glaciers). The `FloorMaterial` / `Vegetation` vocabularies stay open, extended by the layers that own them. - **`FloorMaterial` and `Vegetation` are override stacks, not single derivations.** Each resolves in precedence **wild/natural (biome + climate) → economic (managed: farmland, plantation, paving, clearance — from the settlement layer) → user (mutators: chopped, planted, built, demolished)**; the topmost present layer wins — the same derive-don't-store + mutator pattern (D-227) as the rest of the world. `Vegetation` carries cover/concealment, the movement sound profile, *and* economic yield (timber / crops) on one axis — and managed crops follow the region seasonal cycle (sown → growing → ripe → harvested → fallow), so farmland visibly turns over with the year (Q-105). - **`Water` is a dynamic depth state, plus derived flow.** The axis is depth/wetness only; the water *feature semantics* (river / ocean / lake / sea / tidal-flat / delta) are the **region morphology zone** (below), not a per-tile value, and **flow direction** derives from the D8 network where moving water applies. Depth is **time-varying** via a deliberately cheap, deterministic, **clock-bound** water-height — a **seasonal** term phased by hemisphere (latitude sign) and a **tidal** term that exists only if the body has a moon (no satellite → no tide), amplitudes as body parameters. The water-height is a **region property computed once per phase** (not per tile, not per frame); per-tile flood state is then just `region-water-height vs local elevation`. So **floodplain / tidal-flat / seasonal-river *emerge*** rather than being placed — a low `alluvial-plain` floods at high water and recedes at low. The static world stays static; only this region water-height carries the clock overlay (sim-time as the derivation input), recomputed on phase change — the dynamic-state path D-226 anticipates. Passability derives from `Water` + `elevation` + `material` together. (Model → Q-105.) - **Derived, not stored:** **shape / geometry** (flat / angled / drop / cliff) = `f(elevation-step × material)` — sand slumps to an angle of repose, rock breaks to a vertical face on the same 1 m step; drives the rendered tile geometry. **Tactical form** (crest / hollow / channel / bank — cover, sightline, concealment) read from elevation curvature + water adjacency on demand. Storing either would mean updating 8 neighbours per elevation change, and both are clean functions of data the tile already holds. - **Region-level (shared by all tiles in a region, not stored per-tile):** **morphology zone** — fjord / delta / meander-reach / **alluvial-plain** / open-ocean / lake / sea / … — the whole-shape decision a single tile cannot see; it (a) governs which tile-tags get laid down, (b) carries affordances a tile can't (this inlet is a sheltered harbour), and (c) **owns the water-feature semantics** (river vs ocean vs lake vs sea) the per-tile `Water` axis deliberately omits. **Morphology resolves top-down (region → tile), never bottom-up** — a tile cannot know it is in a fjord from its neighbours; deriving it per-tile would produce the squaring the model exists to avoid. **Sub-biome, province, and the region's seasonal/clock state are likewise region properties** — the season is computed *once* per region per phase and inherited by its tiles. That single cheap seasonal state drives **water-height (flooding / tides), seasonal snow cover, weather, and the farmland crop cycle** alike — one calculation, many dynamic overlays (→ Q-105). - **Separate axes:** biome (climate / sub-biome — region-level, authority unresolved, Q-100; distinct from, but informs, the per-tile `Vegetation` cover), resource hints (bitmask: fertile / mineral / timber / groundwater). - **Cohesion matrix (anti-squaring at the material layer):** intra-region material + sub-biome scatter (a dirt patch in grass, a lone rock in beach sand) comes from a **global, position-keyed continuous noise field — never per-chunk** — so transitions never reveal chunk/grid seams. Straight lines appear **only when authored** (roads, plazas, field edges); a straight line must always have a placed cause, never be a generation-grid artifact. (Algorithm → Q-102.) - **Amended 2026-07-02 ([D-246](#d-246) — intra-class micro-habitat mosaic):** the `Vegetation` and `TerrainMaterial` vocabularies (left above "open, extended by the layers that own them") gain the values the sub-chunk micro-mosaic distributes over — `Vegetation::Meadow` (forb-rich herbaceous), `Vegetation::Deadfall` (dead woody debris — obstruction + fuel, no canopy concealment), `Vegetation::Lichen` (crustose/pioneer biological crust on rock/lava); `TerrainMaterial::Hardpan` (compacted flat crust — hard footing, does not slump like sand), `TerrainMaterial::Scree` (loose angular rock debris — unstable footing; the derived name is "talus" at a cliff foot, "scree" on an open slope). These are **new values on existing axes, not a new axis** — the composite schema is unchanged. They are front-loaded in one amendment (not grown incrementally) because each value is a distinct *selectable* outcome in the mosaic's weighted palette, so adding one later re-rolls the deterministic realization of every affected voxel. The full per-class vocab→axis table and the palette-key rule live in [D-246](#d-246). - **Rationale:** A flat enum explodes combinatorially (≈5×5×8 ≈ 200 mostly-incoherent variants), can't be queried by axis ("all water-adjacent tiles"), and grows a new variant for every new mechanic. Orthogonal axes are set independently by different cascade passes (material from sub-biome, water from drainage, shape from elevation), independently queryable, cheap to serialize, and compose without explosion. Names are visual/narrative and belong downstream of the simulation. - **Open sub-questions:** how far the `FloorMaterial` / `Vegetation` vocabularies are enumerated now vs deferred to the settlement layers; the exact morphology-zone vocabulary; the dynamic water-height model (→ Q-105). - **Implementation:** Phase 4+ (epic T-750), as the cascade reaches the tile layers. Today's `TileKind` (D-049 render stack) is the seed of the per-subtile axes; `SubBiomeVariant` (D-210) is the region biome axis. - **Raised by:** Jeroen (FloorMaterial, shape-from-material, cohesion-matrix directives) + Claude, with the atlas-derivation workshop four — Tyre (composite data-structure), Gestalt (tag model + tactical form), Nigel (morphology-as-character), Burnelli (economic distinctions), 2026-05-25. - **Cross-reference:** [D-227](#d-227) (derive-don't-store — these axes are the derived value type), [D-010](#d-010), [D-210](#d-210) (sub-biome — region biome axis), [D-222](#d-222) (subtile/tile), [D-049](#d-049) (z-stack render — `TileKind` seed), [D-208](#d-208) (D8 — flow-direction source), [D-226](#d-226) (dynamic-state inspection — the water overlay), [Q-100](../questions/architecture.md#q-100) (biome authority), [Q-101](../questions/architecture.md#q-101) (refinement contract), [Q-102](../questions/architecture.md#q-102) (cohesion-matrix algorithm), [Q-105](../questions/architecture.md#q-105) (dynamic water-height) - **Dissent:** None --- ### D-229: Building-property-tag schema — the step-3 fill output - **Date:** 2026-05-25 - **Decision:** Define `BuildingPropertyTag`, the typed step-3 output that replaces the `String` stubs (`era`, `society_profile`, `zone_palette`, `chunk_layout`) on the skeleton. **One tag per building footprint** placed in a block; **written once** at plan time (inside the extended `GenerateSkeleton`, D-230), **read-only thereafter** by three consumers — FillChunk (D-230), the guarantee audit (D-097), and the Phase-6 interior generator (D-231). Fields: - `zone_type_id: ZoneTypeId` (`Box` matching a RON `id`) — *what the building is*; the wire to the 31 D-142 zone-type RON files. Refined from `district_mix.rs`'s per-block `ZoningType` via a generator lookup `(ZoningType + economic_role + seed) → ZoneTypeId`. - `footprint: TileRect` — integer tile-space rect within the 128-tile block (D-010 integer-only). - `extent: FloorExtent` — floor/basement extent; **resolves Q-104** (below). - `entry_class: BuildingEntryClass` — physical access character: `Public | Commercial | Restricted | BreachOnly`. **Named `BuildingEntryClass`, not "access tier"**, to avoid colliding with D-028's relational dialogue layers. Derived from `zone_type × layout_mode (D-096) × prosperity (D-197)`: Commission-Grid → formal/logged/corporate-or-authority credentials; Organic → social/reputation/unlogged credentials. U-curve degrade: low prosperity on a normally-Commercial zone → `BreachOnly` (derelict). - `flavor_ref: ArchitectureFlavorRef` — which trait template characterizes this building (D-232); no rolling-economy read. *(Amended 2026-07-08, T-994/T-1003: now an enum, not a bare index — `InVocabulary(u8)` (index into the body's closed `trait_selection`, resolved via the phase-2 district-dominant pick) `| Swerve(tag)` (the rare out-of-vocabulary deviation draw, or the sparsity escape hatch). The original `(seed + zone_type) → index` mechanism was the pre-three-phase-draw stopgap.)* - `era: ConstructionEra` (`Founding | Established | Modern | Derelict`) + `era_cause: EraCause` — feeds ZonePalette modifier axis C (D-101) and sets the D-217 condition floor. Derived from `founding_age_years + prosperity_baseline + seed`; a body carries mixed-era buildings (founding period anchors the distribution; seed scatters outliers). - `initial_condition: TileCondition` — frozen-amber snapshot from `prosperity_baseline` (D-197/D-217). The rolling condition overlay (D-198) paints *over* this; it never mutates the tag. - **`FloorExtent { base_floor: i8, floor_count: u8, heights: FloorHeightProfile }`** where `FloorHeightProfile = Uniform(u8) | Variable(Vec)`. **Q-104 resolution (the D-110 ↔ D-227 bridge):** two pure functions — `floor_at_voxel_z(z) -> Option` and `voxel_range_for_floor(f) -> Option<(i32,i32)>` — map D-110 floor-index addressing onto D-227 physical voxel-z. Default `Uniform(3)` (3 voxels ≈ 3 m/floor, per Jeroen); a cathedral/hangar is `Uniform(10)`; a mixed-use stack is `Variable([5,3,3,3,3])`. The `Variable` branch carries per-floor memory only when floors actually differ. - **Rationale:** A typed tag is the single contract that lets the atlas render a building, the guarantee audit validate a district, and Phase 6 seed an interior — all from one frozen object. String stubs cannot carry any of that. Orthogonal fields (what / where / how-tall / who-may-enter / cultural / era / condition) compose without a combinatorial enum explosion, matching D-228's axis discipline at the building scale. - **Implementation:** Phase 4+ (epic forthcoming). Replaces the stub fields on `BlockSkeleton`/`QuarterSkeleton` in `server/src/simulation/generator.rs`. - **Amended 2026-06-05 (T-957 — authored zone-type selection table):** the `(ZoningType + economic_role + seed) → ZoneTypeId` lookup is made concrete for the **planetary** cascade. A third input, **`setting` (SettingType)**, is added as a *tweaker* (not a surface/station switch — station & orbital bodies run a separate cascade per [Q-109](../questions/architecture.md#q-109) and own the station-only ids `residential_station`/`extraction_space`/`port_space`/`rural_orbital`, which this table never selects). `zone_type_for(zoning, role, setting, seed)` builds a candidate slice from the base table below, applies the setting tweaker, then deterministically seed-picks one id. Every base cell is non-empty (no empty slices — mirrors D-195's no-zero rule); an unknown role falls back to the ZoningType default. - **Base table (planetary variants):** Commercial → `[commercial_market, entertainment_hospitality]` (financial→`[diplomatic_elite, commercial_market]`; transit_hub→`[commercial_transit, commercial_market]`; service_mixed→ +`entertainment_venue`). Residential → `[residential_surface]` (agricultural→`[rural_agricultural, rural_pastoral]`; extraction→`[residential_dispersed, residential_surface]`). Industrial → `[industrial_manufacturing, industrial_freight]` (manufacturing→`[industrial_manufacturing, industrial_processing]`; extraction→`[extraction_surface, industrial_processing]`; agricultural→`[industrial_processing]`). Administrative → `[administrative_civil]` (institutional→ +`administrative_judicial, diplomatic_elite`; research→`[research_station, administrative_civil]`; service_mixed→ +`medical_facility`). Transit → `[port_surface]` (transit_hub→`[commercial_transit, port_surface]`; manufacturing|extraction→`[industrial_freight, port_surface]`). Recreational → `[entertainment_venue, entertainment_hospitality]` (research|institutional→ +`archaeological_site`). Restricted → `[security_checkpoint]` (military→`[military_garrison, security_checkpoint, detention_facility]`; research→`[research_station, security_checkpoint]`; institutional→`[detention_facility, security_checkpoint]`). Mixed → `[residential_surface, commercial_market, administrative_civil]`. - **`setting` tweaker (post-pass):** Maritime/Water → `port_surface`⇒`port_maritime` (+`port_fishing` for Transit), `rural_*`⇒`rural_aquaculture`, `extraction_surface`⇒`extraction_platform`. Agricultural → bias `rural_agricultural`/`rural_pastoral` into Residential/Mixed. Wilderness → surface `wilderness_frontier`/`residential_dispersed` at the frontier. Urban/other → base unchanged. - **Entry-class U-curve threshold (gap fill):** the "low prosperity on a Commercial zone → BreachOnly" degrade uses the D-217 bands — `prosperity_baseline_bps < 2300` (the D-217 Broken band) → `BreachOnly`. **Era** follows this record as written (`founding_age_years + prosperity_baseline + seed`); the round-3 "distance-to-origin" note is dropped (not in the data model). **FloorExtent** uses the D-220 density-class floor midpoints with seed-jitter within the class range. **Doors** are **not** populated here — `doors: Vec::new()`; door derivation (D-231) is T-979. - **Raised by:** Tyre (schema + FloorExtent/Q-104), Gestalt (BuildingEntryClass, derivation), Miri (flavor_ref), economic-built-world workshop round 2, 2026-05-25. - **Cross-reference:** [D-142](content.md#d-142) (zone-type taxonomy), [D-096](#d-096) (layout mode), [D-197](#d-197) (prosperity baseline), [D-217](#d-217) (tile condition), [D-110](#d-110) (signed z-levels), [D-227](#d-227) (voxel substrate), [D-101](#d-101) (ZonePalette), [D-028](#d-028) (dialogue access — name disambiguated), [D-230](#d-230), [D-231](#d-231), [D-232](#d-232), [Q-104](../questions/architecture.md#q-104) (resolved here) - **Dissent:** None --- ### D-230: FillChunk two-phase execution model — background-plan / on-demand-derive - **Date:** 2026-05-25 - **Decision:** Building fill runs in **two phases**, split by latency budget. **Plan phase (background, no budget):** building-property tags + footprints are produced *inside* the extended `GenerateSkeleton` Rayon task — it already holds the full `CityGenerationContext`, so footprint subdivision + tag assignment stay contiguous with the skeleton work. Output: `DistrictWorldState { skeleton: DistrictSkeleton, block_tags: BTreeMap<(u8,u8), Vec> }`, stored in `BodyWorldState.districts: BTreeMap` — `BTreeMap` everywhere for D-010 determinism. **Derive phase (`FillChunk`, on-demand, <5 ms):** pure geometric shell derivation — read the cached `DistrictWorldState`, and for each tag `shell_derive(seed, footprint, extent, z) → {Void | Wall | FloorSlab | Roof}` per voxel; fill interstitial tiles (street / open space) from `density_pct` + the D-215 pattern; apply `initial_condition`. Cost estimate: ~40 k voxels/chunk × ~10–15 ns = **0.4–0.6 ms**, well under budget even for tall skyscrapers. **Pre-condition:** `FillChunk` is only dispatched after `GenCompletion::SkeletonGenerated` for that district has been processed; if `block_tags` is absent, re-enqueue at `High` and warn — never a blocking read on the main thread. - **Rationale:** The expensive, variable planning work (district mix, tag assignment, footprint layout) has no place under a per-chunk latency budget; doing it once in the background and reducing `FillChunk` to cache-read + rectangle-containment + z-range lookup is what makes on-demand fill trivially fast and re-derivable (D-227). - **Code-gap flagged (→ implementation ticket, independent of this workshop):** `GenCompletion::SkeletonGenerated` currently returns only `city_id: u64`; it must carry the `DistrictWorldState` back so the main thread can insert it into `BodyWorldState.districts`. The completion routing is incomplete today regardless. - **Implementation:** Phase 4+. Amends D-200 (extends the runtime-background execution tier). `FillChunk`/`ChunkGenWorker` are no-op stubs today (`gen_queue.rs`, `workers/stubs.rs`). - **Raised by:** Tyre, economic-built-world workshop round 2, 2026-05-25. - **Cross-reference:** [D-200](#d-200) (three-tier execution — amended), [D-203](#d-203) (LRU cache), [D-225](#d-225) (layer-stream proxy), [D-227](#d-227) (derive-don't-store), [D-215](#d-215) (arrangement patterns), [D-220](#d-220) (density), [D-010](#d-010) (determinism), [D-229](#d-229) - **Dissent:** None --- ### D-231: DoorSpec and InteriorDescriptor — the step3→step4 boundary and the Phase-6 seed - **Date:** 2026-05-25 - **Decision:** The **door is the boundary descriptor** between the generated exterior (step 3) and the lazy interior (step 4, Phase 6) — *not yet an interactive object* (that is Phase 5). Each building tag carries `doors: SmallVec<[DoorSpec; 4]>` (≥ 1). `DoorSpec { facing: CardinalDirection, door_class: Main|Service|Emergency|Hidden, entry_class: BuildingEntryClass, initial_state: Open|Closed|Locked|Sealed, credential: None|TemporalWindow(hours)|Corporate(corp)|Resident(block)|Authority|Social(f32), connects_to: Street(id)|AdjacentBuilding(block)|Interstitial, interior_descriptor: InteriorDescriptor }`. **Door-count derivation:** 1 `Main` minimum; `Service` if the zone has a logistical function; `Emergency` if `floor_extent.above_ground ≥ 2`; `Hidden` seeded by zone_type (research ~60% / admin ~20% / residential ~5%) — `Hidden` carries the D-106 Rooftop-Bar discovery layer. **Initial-state derivation:** Public → Open/Closed, Commercial → `TemporalWindow(zone hours)`, Restricted → Locked + credential, BreachOnly → Sealed. **Mutation semantics (D-227):** initial state is frozen-amber derived; runtime changes (faction lockdown, a picked lock) are **tile mutators over the frozen base**, never edits to the tag — the base is always re-derivable. **`InteriorDescriptor { zone_type_id, entry_class, floor_extent, era, flavor_ref, prosperity, layout_mode }` is the complete Phase-6 seed:** a future interior generator produces a deterministic floor plan from this descriptor + `SeedChain` with **no other system queried**. The voxel at a door position is a conditionally-passable solid — solid until Phase 6 activates the interior, then void. - **Rationale:** Phase 6 (D-166) defers interiors, but its seed must be fixed *now* or step 3 cannot guarantee a generable interior later. Carrying the descriptor on the door (the exact threshold where generation will fire) keeps the lazy-interior contract local and self-sufficient, matching the planet-down-cascade "descriptor + catalog behind the door" rule. - **Implementation:** `DoorSpec`/`InteriorDescriptor` structs land in Phase 4 (planned on the tag); the door-open *fill* is Phase 6. `ChunkMutations`/`TileOverride` already exist as the mutator layer. - **Raised by:** Gestalt, economic-built-world workshop round 2, 2026-05-25. - **Cross-reference:** [D-229](#d-229), [D-097](#d-097) (guarantee audit reads connectivity), [D-106](#d-106) (rooftop clause / hidden door), [D-110](#d-110), [D-227](#d-227) (mutators), [D-142](content.md#d-142), [D-217](#d-217), [D-166](scope.md#d-166) (Phase-6 deferral) - **Dissent:** None --- ### D-232: Architecture-flavor — trait-template catalog (economically gated, seed-drawn, wiki-biased) - **Date:** 2026-05-25 (round 3 — supersedes the round-2 per-body-Gemma-array draft of this record) - **Decision:** The **architecture-domain completion of D-167** (which retired the 7 abstract heritage roots and made the wiki the cultural register but never built the wiki→generator mapping). Cultural flavor is **economically founded, seed-randomized from a shared trait-template catalog, with the wiki biasing the draw for hero bodies** (Jeroen's model). This *replaces* both the round-2 per-body Gemma arrays and the rejected dimensional per-axis draw. - **The catalog (holistic templates, not per-axis traits).** A single shared `architecture_trait_catalog.toml` → `trait_templates` table. Each template is a **coherent bundle** — a template defines a *relationship* between its axes, so it is never decomposed: `{ tag, label, cultural_description, allow: [ObjectTag], block: [ObjectTag], zone_affinity: {DistrictType→weight}, era_scope, eligibility (two-tier, below), base_weight, visual_bundle (→ D-235) }`. Per-axis mixing is forbidden (it produces incoherent grammar, e.g. stone walls + flat roof). - **Two-tier eligibility — and role is per-layer, not global.** *Hard gates* (`bulk_class`, `prosperity`, `production_ubiquity`) exclude a template from the pool when failed. *Weight modifiers* (`economic_role`, `dominant_faction`, `founding_age`, `geographic_sector`, `morphology_zone`) multiply `base_weight` but never exclude. **Key principle: the same input plays different roles at different layers.** `morphology_zone` is a *hard gate* on street geometry (D-234 — a fjord cannot have radial streets) yet only a *soft weight* on cultural-template eligibility. Economics *founds and hard-gates* what a building **is** (D-233) but only *weights* how it is **characterized**. **`geographic_sector` (corridor) is always a soft weight, never a gate** — "corridors are tendencies, not borders"; a hard corridor gate collapses the pool to ~7 templates/corridor → ~100 % within-corridor collision. - **All eligibility numbers are integer basis-points** (`min_prosperity_bps` etc.), never `f32` — D-010 determinism, now save-critical under D-227. - **The draw (three phases).** (1) *Body vocabulary* (background, at skeleton time): hard-gate filter → weight (mods × wiki-bias) → `SeedChain`-seeded weighted draw of **K** templates → stored as `trait_selection: Vec` on the skeleton. **K is locked to `complexity_tier`: Full = 5, Moderate = 3, Minimal = 1, Empty = 0** (Nigel's birthday math: K=4 fails within-corridor uniqueness at a 35-template catalog; K is *not* a range and *not* keyed to prosperity). The draw is **coverage-aware** — it must cover the body's actual district-type mix, not draw 5 templates that all starve the civic district. (2) *District-dominant* (at fill): each district draws **one** dominant template from the body's vocabulary, weighted by `zone_affinity`, applied **whole** to that district (Araminta's composition rule); secondaries surface on outbuildings/secondary streets and in their own high-affinity districts. The body vocabulary is **closed by default** (a mining town's civic hall looks like *that town's* civic — a feature). (3) *Within-template* variation: the fill seed picks within each template's `allow`-lists per axis. - **Deviation system — closed by default, rare tasty swerves.** On top of the closed vocabulary, a **rare per-building wildcard** can draw a *coherent whole* template from outside the body's vocabulary (the Shinto temple in Amsterdam — a complete foreign building, never an axis-scramble). The swerve is **cultural only**; the building's function still passes the normal economic hard gates. **Three sources, two opposed active drivers:** *foreign import* (another corridor's grammar) driven **up** by cosmopolitanism / centrality / transit / Epicenter tier; *heritage callback* (the body's own corridor **heritage pool**) driven **up** by remoteness / isolation / conservatism (the cut-off Latin world that reaches back to haciendas); and the passive *past-vogue holdover* (just old, see era below). The **sparsity escape hatch is the same mechanism** triggered by necessity rather than dice — when the closed vocabulary genuinely cannot serve a district, it reaches the full catalog. - **Corridor = a two-part pool.** Each corridor is authored as a **baseline** character (its default cohesive look — e.g. East-Asian: cyberpunk density + utilitarian industry) **plus a heritage sub-pool** (deep-history callbacks — East-Asian: fishing-village/temple enclaves; Iberian/Latin: haciendas, colonial-revival) tagged so the remoteness dial draws specifically from it. A **shared cross-corridor pool** feeds the foreign-import swerves. - **Era = maintenance/wear, NOT a material-tech ladder.** The Reach is post-space-travel throughout; there is no stone→concrete→glass progression — fashion cycles fast and arbitrarily. A building's construction era (D-229, from distance to the founding origin) reads primarily as **age/wear**, realized through the condition layer (D-217 / D-198 — maintenance modulates how much wear shows). The "different-looking old building" is just the **past-vogue holdover** — the *temporal* sibling of the spatial swerve, one deviation system with sources in *elsewhere* and *elsewhen*. (This retires the round-2 era-band material-progression and the `era_fallback`-as-tech-ladder; `era_fallback` survives only as the asset-resolution fallback chain, below.) - **Logical catalog vs visual-asset layer (the incremental-content split).** The **logical catalog** — tags, eligibility, `allow`/`block`, token *references* — is cheap data, **authored complete and frozen at launch**; the draw reads only this, so derivation is deterministic and stable forever **with no catalog versioning needed**. The **visual-asset layer** is a **fallback hierarchy**: every specific texture/material token declares a generic parent it degrades to (`temple_wall_wood` → generic `wood_wall` placeholder until the specific art ships, then it **upgrades in place**). Themes/textures are patched in incrementally behind stable tokens — a shipped world's *structure* is frozen at birth (the temple was always a temple), only its render *fidelity* sharpens. Versioning re-enters only if a genuinely **new logical template** is added post-launch (minimized by authoring the catalog generously up front). - **Wiki bias (hero bodies only, ~30–40).** A sparse `atlas_body_trait_bias` row: `pin` (mandatory, **counts toward K**), `boost` (≤ 3× weight), `suppress` (≥ 0.33×, never 0 — preserves second-playthrough surprise). Bias is **per-body, never per-corridor**. Non-hero bodies (the ~240 remainder) run the identical algorithm with no bias — economics + corridor weight + seed. - **CI guardrails (Nigel, build-time validation):** ≥ 5 templates eligible after hard gates per economic class; no single template > 60 % of pool weight after modifiers; bias is per-body; the catalog grows ≥ 1 template/dimension per new corridor or archetype. - **Catalog population:** ~25–35 templates at floor, 40–45 target. Core hand-curated (Miri authors cultural meaning + eligibility; Araminta authors the matching `visual_bundle`), then a *bounded* Gemma corpus-distillation pass (one read of all wiki, propose new templates, human-gated) — not per-body generation. - **Channel separation from D-233 (held):** D-233 decides **what** a building is (vocabulary, coverage); D-232 decides **how** it is characterized. Same inputs (e.g. `bulk_class`) used non-conflictingly — D-233 as hard function gate, D-232 as soft cultural weight. They compose at fill. - **Amended 2026-05-31 ([D-237](#d-237) — authored specialization layer):** the template draw now reads an authored `cultural_specialization` (new column on `system_economy`) that **selects/biases the template pool** when a system's cultural character diverges from its corridor baseline. The field carries two value sub-types in one column — *activity/character* values (`agrarian`, `industrial_heritage`, `institutional`, `scholarly`, etc.) and *heritage* values (`scottish`, `vietnamese`, `zulu`, `french_provencal`, etc.); heritage values take precedence where present. This is a Phase-4 correctness fix, not just flavor: without it, a system whose founding heritage diverges from the corridor (e.g. Vietnamese-founded Dài Lộ in the east_reach Korean/Japanese corridor) draws the wrong cultural templates. `NULL` = use the existing corridor-pool algorithm unchanged. Consistent with the held channel separation — `cultural_specialization` is a D-232 cultural-weight input, never a D-233 function gate. Singular landmarks (e.g. Groombridge's GSH within a `financial_hub` district) are expressed via D-222 multi-block reservation + an `atlas_body_trait_bias` hero pin, not the system-level field. - **Amended 2026-07-08 (T-1003 — deviation/swerve system implemented, driver inputs pinned):** the two opposed drivers now map to real fields (`server/src/atlas/trait_swerve.rs`): *foreign-import* scaled up by `WorldTier::Epicenter`/`Passage` (transit), `dominant_faction = "mixed"` (cosmopolitanism), and road/rail-graph node degree (centrality); *heritage-callback* scaled up by road-graph isolation (degree ≤ 1), remote tier (Waypoint/Backwater), and `founding_age_years` bands (conservatism). Rates are integer bps: base **100 bps/building** per driver, hard cap **300 bps** — placeholder constants pending Nigel/Burnelli calibration; a `dist_ly`-percentile remoteness input is deferred until the read-set carries it. The wildcard result is recorded as `ArchitectureFlavorRef::Swerve(tag)` (out-of-vocabulary by construction); pools are hard-gate-eligible only (cultural-only rule held). The sparsity escape hatch is implemented as the same mechanism, necessity-triggered at the phase-2 district-dominant pick (deterministic max-weight, no dice). The passive past-vogue holdover stays on the D-217 condition layer, as decided. - **Amended 2026-07-08 (T-994 — phase-2 "district" pinned to the D-243 tier):** the word "district" in the phase-2 *district-dominant* draw means the **D-243 2 048 m District cell** (4 quarters), **not** the 512 m Quarter — this record was written after D-222 renamed the 512 m unit to Quarter, but the pin was never made explicit and the shipped code had no District-tier representation at all (caught in the 2026-07-07 /whats-next refinement; the "one template per district, applied whole" composition rule reads at 2 048 m). Implementation (T-994): the dominant template per `(DistrictType)` is pre-resolved at L3→L4 dispatch time, seeded by `(SeedChain::for_body, district-cell position, district type)` — so any settlements whose quarters share a District cell independently derive the identical dominant template with no cross-settlement coordination, and `assign_block_tags` is a pure lookup. Phase 1's body K-draw is likewise seeded from `SeedChain::for_body` (never the per-settlement chain), preserving the closed-vocabulary invariant. - **Supersedes (architecture/generator domain only):** **D-104** (`HeritageGrammarOverlay` + per-root data → the catalog + `allow`/`block`), **D-105** (heritage-root→informal-zone lookup → flavor-filtered selection; the three zone *types* survive), **D-101 modifier axis A** (`HeritageRoot` → catalog draw; axes B/C + faction/climate/condition/season unchanged), **D-107** (per-root trauma decay → per-template/condition; the "trauma intensifies culture" principle survives). Also retires the round-2 `atlas_body_culture` / `atlas_body_culture_era` tables. - **Storage:** `trait_templates` (the catalog) + `atlas_body_trait_bias` (sparse, hero bodies). The per-body draw result lives as `trait_selection: Vec` on the skeleton — **re-derivable** from catalog + economics + bias + `SeedChain`, no Gemma in the hot path. `CityGenerationContext` carries `trait_selection` + `morphology_zone` (replacing the round-2 `flavor_profile`; D-199 amend). - **Source-location deferred:** the human-authored *source* home (catalog file + `bias.json`) rides on **Q-107** (wiki → Atlas content-set consolidation). The generator-facing tables are invariant to it, so the fill seam is unblocked regardless. - **Implementation:** Phase 4+ (mechanism + sparse catalog); visual themes fill in across Phase 5+ and post-launch behind the fallback chain. `ObjectTag`/material vocabulary is Miri + Araminta co-maintained. - **Raised by:** Jeroen (the trait-template / economically-founded / wiki-biased model + the swerve, corridor-pool, era-as-maintenance, and logical/asset-split refinements), with Miri (lead synthesis), Burnelli (eligibility), Nigel (draw + variety math + guardrails), Araminta (visual bundle + composition), economic-built-world workshop rounds 2–3, 2026-05-25. - **Cross-reference:** [D-167](content.md#d-167) (corridor cultural system — completed here), [D-223](content.md#d-223) (Gemma naming pipeline + corridor mixing), [D-142](content.md#d-142) (zone types — the function baseline filtered by templates), [D-228](#d-228) (morphology zone), [D-233](#d-233) (economic channel — what vs how), [D-234](#d-234) (morphology→street — the hard-gate layer), [D-235](#d-235) (visual bundle + fallback hierarchy), [D-217](#d-217)/[D-198](#d-198) (condition — where era's wear lands), [D-224](#d-224) (SeedChain — the draw PRNG), [D-199](#d-199) (read-set — extended), [D-237](#d-237) (authored `cultural_specialization` — template-pool selection), [D-101](#d-101)/[D-104](content.md#d-104)/[D-105](content.md#d-105)/[D-107](content.md#d-107) (superseded in part), [Q-106](../questions/architecture.md#q-106) (era reframed), [Q-107](../questions/architecture.md#q-107) (source location) - **Dissent:** None --- ### D-233: Economic signal → block-fill vocabulary — `BulkClass` × `ProductionUbiquity` - **Date:** 2026-05-25 - **Decision:** Within a block of a given district type (D-194), the building vocabulary, coverage density, and interstitial character derive from two commodity signals for the settlement's **dominant commodity**, both read from the `systems.db` commodity catalog (D-184) at build time — **seed-independent, no D-199 tier split, no rolling-economy touch:** - `BulkClass = BulkSolid | BulkLiquid | PrecisionDense | Perishable | NonPhysical` - `ProductionUbiquity = Ubiquitous | Common | Specialist | MonopolySource` - **Coverage:** `BulkClass` sets a roofed-coverage fraction (NonPhysical 0.85–0.95 → PrecisionDense 0.75–0.90 → Perishable 0.50–0.65 → BulkSolid 0.25–0.40 → BulkLiquid 0.20–0.35), scaled by `density_class` (D-220) and `prosperity_baseline` (D-197). For bulk industries the non-roofed remainder is **operations surface** (haul roads, ore pads, conveyor runs, tank berms) — tagged *built economic infrastructure*, **not** interstitial open space. - **Vocabulary:** each `(BulkClass × production_tier)` draws from a **frozen** building-vocabulary pool (e.g. `mine_head`/`conveyor_run`/`tailings_area`; `tank_farm`/`flare_stack`; `cleanroom_facility`/`qc_lab`; `field_shed`/`cold_store`; `office_tower`/`civic_hall`). A mine block places mine buildings, full stop. - **Concentration/dispersion rule:** `ProductionUbiquity` sets the **spatial spread** of zone-type blocks, not per-block weight — `Ubiquitous` scatters small instances through mixed-use (the settlement doesn't read as "a water town"); `MonopolySource` concentrates contiguous block groups (the mine *is* the settlement, everything else is support). - **Residential follow-on:** every non-residential production block emits a labor-demand signal sizing adjacent residential blocks (BulkSolid extraction ~3.0× → BulkLiquid ~2.5× → Perishable ~2.5× → PrecisionDense ~2.0× → NonPhysical ~1.5×); housing character from `prosperity_baseline` (>0.7 market-rate → 0.4–0.7 standard worker → <0.4 company barracks/squatter). - **Frozen-amber:** the vocabulary is t=0-derived and immutable; the condition overlay (D-198) paints Maintained/Worn/Abandoned but **cannot change a `BuildingTag`** — a mine processing plant becomes an *abandoned* mine plant, never an office. - **Amends D-199** (adds `dominant_bulk_class`, `dominant_production_ubiquity`, `morphology_zone` to `CityGenerationContext`) and **D-184** (back-fills the `bulk_class` enum values it named but left open). Enforcement: `fill_chunk(ctx, seed)` holds no `PressureState` reference. - **Refined 2026-05-26 (batch refinement):** `BulkClass`'s 5 values are **built-form archetypes** — a projection of the 8 commodity cargo-types in `commodities.toml` (`bulk`/`standard`/`compact`/`oversized` → `BulkSolid` [size is orthogonal to bulk-form; a future axis if it earns visual payoff], `precision` → `PrecisionDense`, `liquid` → `BulkLiquid`, `perishable` → `Perishable`, `non_physical` → `NonPhysical`). The 8 stay on commodities for the economic model — no fidelity is lost; the 5 are only the built-form driver. The settlement's **dominant commodity is *derived*** from its highest-output location-bound production chain (D-178), **not** an authored role→commodity table — so the built form reflects the already-authored production/trade data (where sector specialization lives) and varies body-to-body for free. - **Rationale:** Floor-area-to-output ratio is the capital structure of an industry, not a style choice — services maximize floor, bulk extraction is mostly open operations surface. Driving coverage and vocabulary from `BulkClass` makes settlements that share an economic *role* look physically distinct by *what they make*, and the ubiquity rule gives the settlement its identity (a MonopolySource town vs background infrastructure). - **Implementation:** Phase 4+. `bulk_class`/`production_ubiquity` lookups added to the economics import; consumed in the plan phase (D-230). - **Raised by:** Burnelli, economic-built-world workshop round 2, 2026-05-25. - **Cross-reference:** [D-194](#d-194) (district mix — upstream), [D-184](content.md#d-184) (commodity catalog — amended), [D-199](#d-199) (read-set — amended), [D-220](#d-220) (density), [D-197](#d-197) (prosperity), [D-142](content.md#d-142) (zone types — downstream), [D-176](#d-176), [D-180](#d-180) (EconEvent — overlay trigger), [D-198](#d-198) (condition overlay — amended), [D-229](#d-229) - **Dissent:** None - **Re-amended 2026-05-31 (D-237 — authored specialization layer):** The settlement's `dominant_bulk_class` and `dominant_production_ubiquity` are now sourced from the authored specialization layer (D-237) rather than derived directly from the highest-output production chain. Derivation chain: (1) If `system_economy.economic_specialization` is authored → resolve via `specialization_vocabulary(economic_specialization)` → `(commodity_id, production_ubiquity_override)` → `(BulkClass, ProductionUbiquity)`. Use the vocabulary-specified `ProductionUbiquity` (which may override the catalog default per the equal-or-higher rule). (2) If `economic_specialization` is NULL → run the deterministic-varied heuristic (D-237 §fallback: seed-hashed weighted draw over the vocabulary using `economic_role` + corp HQ signals + noise term) → same projection. In both cases the resolved `(BulkClass, ProductionUbiquity)` populates `CityGenerationContext.dominant_bulk_class` and `CityGenerationContext.dominant_production_ubiquity`. Frozen-amber constraint unchanged: `fill_chunk(ctx, seed)` holds no `PressureState` reference. Within-system body variation (weighted draw over body-level candidates anchored to system commodity) remains seed-hashed. **Groombridge clarification:** The vocabulary expresses district-level character. Singular MonopolySource-class landmarks within a Specialist district (e.g. GSH within Groombridge's `financial_hub` cluster) are expressed via D-222 multi-block reservation + D-232 hero-element pin, not via composite vocabulary values. --- ### D-234: Morphology zone → street and footprint constraints - **Date:** 2026-05-25 - **Decision:** The D-228 region **morphology zone** constrains street geometry and footprint subdivision so terrain shapes settlement form — the variety guardrail that stops the generator placing a radial-core city in a fjord. Two rules: - **(a) Permitted street patterns by morphology** — a lookup that gates the D-215 archetype arrangement patterns: fjord/canyon → ribbon or hub-and-spoke only (streets linear along the terrain axis, radial impossible); delta/braided → hub-and-spoke following channels (bridges as forced nodes); alluvial-plain → any pattern; island → hub-and-spoke (perimeter access priority); mountain-pass → ribbon only (elevation steps as block boundaries). - **(b) Waterfront footprint rule** — any block adjacent to a water morphology feature uses **pier/quay geometry** on the water-facing edge (no standard setback, dock-orthogonal subdivision, access priority toward the water) and standard street frontage on inland edges. Applies uniformly wherever a block touches a water feature — body-independent, zone-independent. This is what makes port towns present to the quay differently than to the street. - Street geometry obeys the ±45° pathfinding cap (D-096) — no curves; straight lines appear only where authored (the D-228 cohesion rule). Fills the Step-1 `chunk_layout` / `corridors` / `access_points` stubs. - **Refined 2026-05-26 (street-network algorithm — two layers):** streets generate in **two layers**, mirroring real cities and the Civ road lineage. **Arterials** (`corridors`) = a **minimum-spanning / least-cost graph** over the district's key nodes (access-points, reservations, landmarks) — emergent trunk topology (Civ6-style, "roads where traffic wants to go"), ±45°-snapped; **this is the node/edge graph the guarantee audit (D-097) reads** (a chokepoint *is* an arterial bottleneck; an encounter-corridor *is* an arterial through-route). Ribbon = MST on linearly-constrained nodes; hub-and-spoke = MST with a forced centroid hub. **Local streets** (`chunk_layout`) = a ±45° **grid lattice** within each block (Civ4-style), modulated by the D-096 Grid/Organic mode (offsets + rotation). **Voronoi was rejected** — its arbitrary-angle edges violate the ±45° cap and its irregular cells break the axis-aligned `TileRect` footprint fast-path (D-229), degrading per-chunk fill from rectangle-containment to polygon rasterization. (Civ1–4's "road on every tile" is the anti-pattern this avoids; the arterial/local split is the city-scale fix.) - **Rationale:** Morphology zone and `architecture_flavor` were added to the context (D-199) as *inputs without consumers* — two economically-similar bodies otherwise produce topologically identical street networks and identical block subdivision (Nigel's same-y-grid failure). These two rules give morphology structural teeth so the fjord port and the delta port are categorically different cities, not reskins. - **Implementation:** Phase 4+. Consumed in the plan phase (D-230) when laying streets and subdividing blocks. - **Implemented 2026-06-06 (T-957):** the street network + footprint subdivision land in `skeleton_gen.rs` (absorbing T-976 into T-957 — one coherent walkable-quarter deliverable). `AccessPoint`/`CorridorSpine`/`ChunkLayout` are now typed structs (were `String` stubs): access nodes from road-entry octants + reservation gates; arterial corridors as a morphology-gated graph (Ribbon for fjord/canyon/mountain-pass, HubSpoke for delta/island/enclosed water, Prim-MST mesh otherwise), ±45°-snapped (a); per-block local lattice modulated by D-096 Grid/Organic. Footprint subdivision is a density-scaled BSP into axis-aligned `TileRect`s with D-233 BulkClass roofed-coverage. **Waterfront rule (b):** the water-facing quarter edge (from the settlement's `Coastal` founding orientation, D-213) drops its street setback to 0 so buildings present flush to the quay. The water bearing itself is now extracted in Layer 1 (`TerrainAnalysis::water_bearing`, 8-octant integer) and fed into D-213 founding orientation (T-956's `0` stub is gone for coastal/river settlements). **Pending dependency:** the waterfront rule reads `CityGenerationContext.founding_orientation`, which `city_context_reader` still stubs to `Cardinal` — real per-settlement orientation only reaches quarter generation once the **Layer-3 placement → Layer-4 `GenerateSkeleton` dispatch** is wired (it must copy the placement's founding orientation into the context). That cross-layer dispatch is the remaining integration; the rule is correct and tested, awaiting its input pipeline. - **Raised by:** Nigel, economic-built-world workshop round 2, 2026-05-25. - **Cross-reference:** [D-228](#d-228) (morphology zone), [D-215](#d-215) (arrangement patterns — gated), [D-096](#d-096) (layout mode / ±45° cap), [D-220](#d-220) (density), [D-229](#d-229), [D-213](#d-213) (founding orientation — water bearing), [Q-106](../questions/architecture.md#q-106), [Q-109](../questions/architecture.md#q-109) - **Dissent:** None --- ### D-235: Building exterior visual grammar and material vocabulary - **Date:** 2026-05-25 - **Decision:** The visible-form layer over the tags. `(template × zone_type × density) → BuildingExteriorTag { wall_material, roof_form, setback_tier, facade_rhythm, color_range }`, where `template` is the district's dominant trait template (D-232), derived in three steps: **(1)** the template's `allow`/`block` filters the available material + roof token set (a template is a *coherent bundle* — its axes are chosen together, never mixed across templates); **(2)** `zone_type` biases within the filtered set (probabilistic — the template can override, e.g. an `allow: [stone_base]` industrial block stays stone, not corrugated metal); **(3)** `density` sets `setback_tier` (Dense → `zero_lot` → … → Frontier → `campus`), which drives **interstitial type** (`void / court / garden / plaza / dock_slip / market_pad / open_lawn`). The fill seed then picks within each axis's allow-list. **Era is NOT a material filter here** (D-232 reframe): the Reach is post-space-travel throughout, with no stone→concrete→glass tech ladder — material choice comes from the template (fashion), and era reads as **maintenance/wear** via the condition layer (D-217), with the occasional out-of-vogue building handled as a deviation (D-232's past-vogue holdover), not a material rule. - **Exterior vocabulary** (extends D-228's FloorMaterial axis): `WallMaterial` (stone_cut/stone_rough/fired_brick/clay_render/heavy_timber/pile_timber/timber_frame/reinforced_concrete/corrugated_metal/steel_panel/composite_panel/precision_glass/smart_facade), `RoofForm` (pitched_steep/pitched_shallow/flat/composite_curved/corrugate_shed/dome), `FacadeRhythm` (bay_window/grid_panel/solid_punched/arcade/open_front/blind_wall), `StreetSurface` (cobble/packed_earth/poured_slab/elevated_boardwalk/dock_plank/rail_embedded — derives from district + density + template, the same filter as buildings, for a consistent world). **Color** is template-bounded (cultural palette cue), seed-selected within range — always within the template's register. - **Fallback hierarchy (the incremental-content mechanism, D-232).** Every specific texture/material token declares a **generic parent it degrades to**: `temple_wall_wood` → generic `wood_wall` placeholder until the specific asset ships, then it **upgrades in place**. The *logical* token a building uses is fixed at generation (deterministic, frozen); only its rendered *fidelity* sharpens as themes/textures are patched in. This is where incremental content lives — an asset-resolution concern, not a generation one — so no catalog versioning is needed. (`era_fallback` from the round-2 draft survives only in this generalized form — a fallback chain, not a tech ladder.) - Worked example: a fjord port (template `fjord_maritime` → stone base + steep roof + zero-lot + solid-punched + cobble) and a delta port (template `river_delta` → pile-timber + shallow roof + arcade + boardwalk) share density and zone types yet read as completely different cities. - **Amended 2026-07-07 (T-995 — ObjectTag vocabulary ratified, resolves Q-049):** the `WallMaterial`/`RoofForm`/`FacadeRhythm`/`StreetSurface` example token lists above never shipped. The canonical ObjectTag vocabulary is the shipped 28-template `architecture_trait_catalog.toml` (T-1005) material palette, now formalized as a machine-readable registry at `wiki/economics/object_tag_vocabulary.toml`, importer-validated by `economy_import/traits.py` (V-TT-03 existence, V-TT-04 fallback-graph). The ratified tags, by axis: **wall** (10) — `concrete_wall stone_wall brick_wall rendered_wall stucco_wall timber_wall rammed_earth_wall steel_frame glass_curtain_wall composite_panel`; **roof** (7) — `flat_roof pitched_roof corrugated_roof clay_tile_roof terraced_roof vaulted_roof green_roof`; **facade** (8) — `regular_facade ornamental_facade industrial_glazing arcade_facade shuttered_facade screen_facade colonnade lattice_screen`; **street** (7) — `paved cobble packed_earth canal_way elevated_walkway heavy_haul boardwalk`; plus the four fallback-terminal **generic** placeholders — `generic_wall generic_roof generic_facade generic_street`. Every specific tag's registry entry declares its generic fallback parent directly (the per-template `fallback` maps in the catalog remain illustrative/non-exhaustive documentation, not the validated source). - **Formally retires** Araminta's generator-architecture Round-4 `D-READY-9` ten-root heritage-modifier TOML system — superseded by the D-232 trait-template catalog (body-specific draw, allow/block-bounded, no root taxonomy). - **Rationale:** The template is the differentiator, but it needs a concrete visual vocabulary to act on, filtered consistently across walls, roofs, facades, *and* streets or the world reads incoherent. The "old quarter vs new development" texture comes from **wear + occasional past-vogue holdover** (D-232), not from a material-technology ladder — because in a post-space-travel setting there is no such ladder. The fallback hierarchy lets the logical world be complete and frozen at launch while the art catches up over patches. - **Implementation:** Phase 4+ (the token logic + Atlas-level data); textured render + the bulk of the theme library are Phase 5+ and post-launch, behind the fallback chain. `ObjectTag`/material vocabulary is Miri + Araminta co-maintained. - **Raised by:** Araminta, economic-built-world workshop round 2, 2026-05-25. - **Cross-reference:** [D-228](#d-228) (composite tile / FloorMaterial — extended), [D-232](#d-232) (flavor — the filter), [D-217](#d-217) (condition / heritage markers), [D-142](content.md#d-142) (zone types), [D-220](#d-220) (density → setback), [D-106](#d-106) (height tiers) - **Dissent:** None --- ### D-237: Authored per-system specialisation layer — `economic_specialization` + `cultural_specialization` + `dominant_faction` - **Date:** 2026-05-31 - **Decision:** Each inhabited star system carries three authored per-system identity fields, statically compiled from `wiki/economics/system_specialization.toml` at build time via `import_economics.py`. These fields constitute the lore-authored identity layer above the heuristic and are the primary source for D-233/D-232/D-214 generator inputs for named systems. All three fields are static (t=0), deterministic (D-010), and NULL-safe (NULL = heuristic fallback runs). **(1) `system_economy.economic_specialization TEXT` (new column).** A 27-value curated vocabulary compiled from `wiki/economics/specialization_vocabulary.toml` into a `specialization_vocabulary` DB table. Each value maps to `(commodity_id, production_ubiquity_override_or_null)` → `(BulkClass, ProductionUbiquity)` for D-233. Scale is encoded in the value (no separate scale column): `breadbasket` pins `ProductionUbiquity = Specialist` where the catalog default would be `Ubiquitous`; `terroir_agriculture` pins `MonopolySource`. Equal-or-higher rule: a `production_ubiquity_override` may only be equal or higher concentration than the commodity's global default (CI hard error V-SES-03). Authored for ~80–100 named systems; deterministic-varied heuristic fallback for ~200 unnamed systems (seed-hashed weighted draw over the vocabulary: W_ROLE_PRIMARY=8,000 bps, W_CORP=5,000 bps, W_NOISE=400 bps to every vocabulary value; same seed+system_id always produces the same value; the noise term ensures ~5–10% of unnamed systems draw an off-primary value, preventing all unnamed agricultural worlds from being identical). **Relationship to `economic_base_primary` (the seam — clarified 2026-06-01):** `system_economy.economic_base_primary`/`_secondary` is pre-existing free-text prose (e.g. `"fusion_fuel, financial_services"`) rendered into the wiki index.md "Industries/Exports" infobox by `wiki_sync.py`. `economic_specialization` is a **deliberate parallel representation at higher fidelity**, NOT a duplicate: the prose is uncomputable GTTR flavor for human readers; the enum is the machine-actionable projection the D-233 generator consumes. They are the same *concept* (what the system produces) at two resolutions. The two must stay mutually consistent — `economic_specialization` should never contradict the `economic_base_primary` prose for the same system (the T-1016 content pass authors the enum from the same lore the prose describes, and CI soft-warning W-SES-03 flags prose/enum divergence). Unification onto one source was considered and rejected: the prose carries multi-sector nuance and narrative voice the enum cannot, and the enum carries the deterministic (BulkClass × ProductionUbiquity) projection the prose cannot — collapsing either direction loses information. systems.db is canonical; index.md infobox is a read-only DB projection; only the index.md *prose sections* (Supply Dependency / Faction Notes / …, round-tripped by `wiki_sync` `PROSE_SECTIONS`) are authored in-place. **(2) `system_economy.cultural_specialization TEXT` (new column).** Directs D-232's template pool when a system's cultural character diverges from corridor baseline. Single field carrying two value sub-types: *activity/character values* (`agrarian`, `industrial_heritage`, `institutional`, `scholarly`, `artistic`, `financial_technocratic`, `cosmopolitan`, `compact_cooperative`, etc.) and *heritage-type values* (`scottish`, `vietnamese`, `zulu`, `afrikaans_cape`, `tagalog`, `chinese`, `italian_northern`, `french_provencal`, `norse_compact`, etc.). Heritage values take precedence when both apply. `NULL` = use corridor default (D-232 existing algorithm). Authored for ~60–100 systems where the corridor default would produce wrong D-232 architectural draws in Phase 4 (not a thin Phase 6 concern: a Vietnamese-founded system in the east_reach Korean/Japanese corridor draws the wrong templates without an explicit pin). Does NOT replace `system_culture.cultural_register` (prose NPC/dialogue voice); consumed by the physical generator only. **(3) `system_factions.dominant_faction TEXT` — populated, not added.** Column already exists (D-199 field 4); VALUES for named systems are authored in this pass via `system_specialization.toml`. 8-value vocabulary: `concord_assembly | compact | compact_sympathetic | syndic_dominant | veil_institute | independent | disputed | mixed`. `lattice_commission` is dropped — the Commission is a regulatory body under Assembly authority, not a governing faction; ACB and Bastion are `concord_assembly`. Authored for ~40–60 named systems; existing Commission-presence + currency-zone derivation serves as fallback. **Groombridge landmark mechanism:** Where a system's authored identity includes a singular MonopolySource-class institution within a Specialist-scale district (Groombridge: `financial_hub`/Specialist + GSH clearing house), the district character is encoded as Specialist in the vocabulary. The singular landmark is expressed via D-222 multi-block reservation + D-232 `atlas_body_trait_bias` hero pin — not via a composite vocabulary value. The vocabulary expresses district-level character; landmark-level identity is the D-232/D-222 mechanism. **CI guardrails:** 6 hard errors (vocabulary FK integrity, inhabitance coverage, production_ubiquity monotonicity, vocabulary constraint for all three fields); 8 soft warnings (MonopolySource review list, distribution balance, prose/field divergence, D-172 faction/currency alignment, Compact monoeconomy flag, authoring gap lists). Full CI spec in `docs/workshops/system-economic-specialization/workshop-outcomes.md §8`. **Source:** `wiki/economics/system_specialization.toml` (per-system entries) + `wiki/economics/specialization_vocabulary.toml` (vocabulary definition). Both added to `IMPORT_ECONOMICS_SOURCES` for meta-stamp tracking. `system_economy` gains two new columns via `COLUMN_MIGRATIONS` in `import_economics.py`; `specialization_vocabulary` is a new read-only reference table. - **Rationale:** The pre-existing D-233 heuristic (`economic_role` + corp HQ + `planet_class` → dominant commodity) fails predictably for: (1) output-vs-input confusions (Cygni B: heuristic sees ore inputs, not freight_hauler outputs); (2) political mandates (Groombridge: financial infrastructure is an institutional choice, not geographic necessity); (3) terroir-locked production (D-177 systems: catalog defaults are globally ubiquitous, not locally monopolistic); (4) service economies (Prometheus, Keid: no heuristic path from available signals to the correct identity). The three-field authored layer resolves all four failure modes. Three fields are the minimum that changes how a system reads without redundancy: economic → D-233 block fill, cultural → D-232 template selection, faction → D-214 political archetype. `cultural_specialization` is a Phase 4 correctness requirement, not Phase 6 enrichment. - **Raised by:** System economic-specialization workshop, 2026-05-31. Burnelli-Sheldon (schema, vocabulary, CI, fallback algorithm), Miri (must-pin list, scale gap, D-177 terroir pins, cultural heritage vocabulary), Paula (faction vocabulary, Compact rules, heritage-divergence cultural scope, D-172 compliance), human checkpoint (dual-axis design, scale-in-vocabulary, faction authored, Groombridge landmark mechanism, deterministic-varied fallback). - **Dissent:** None. - **Cross-reference:** [D-233](#d-233) (re-amended — economic block-fill now sourced from this layer), [D-184](content.md#d-184) (commodity catalog), [D-199](#d-199) (CityGenerationContext read-set — extended), [D-232](#d-232) (architecture-flavor template draw — consumes `cultural_specialization`), [D-214](#d-214) (PoliticalArchetype — consumes `dominant_faction`), [D-172](content.md#d-172) (currency zone alignment — `dominant_faction = compact` validates), [D-174](#d-174) (shadow economy intensity — `compact` elevates), [D-177](content.md#d-177) (productivity constraints — informs monopolistic pins), [D-220](#d-220) (density), [D-197](#d-197) (prosperity), [D-222](#d-222) (multi-block reservation — Groombridge landmark mechanism), [D-178](#d-178) (rolling economy — not touched; this is t=0 authored layer only) --- ### D-239: Tile derivation contract — coarse→fine refinement chain (resolves Q-101) - **Date:** 2026-06-07 - **Resolves:** [Q-101](../questions/architecture.md#q-101) - **Decision:** The walkable tile is materialised by a **three-carrier refinement chain**, each stage a pure deterministic function of `(seed, atlas, body-params, position)` per [D-227](#d-227): `RegionProfile` (~1 km) → `ChunkContext` (64 m) → `VoxelColumn` (1 m). Each scale boundary is its own derivation with its own failure modes. **(1) Determinism — no authoring at the derivation layers, ever.** L1–L5 derivation has **zero per-body override hooks**. All authorial control lives **upstream** at the body-parameter / atlas / system-specialization layer; all gating params (`RIVER_THRESHOLD` — a derived per-body-class value `(hydrosphere, tectonic_activity, precipitation_class) → threshold`, not the global `200`; `tectonic_class`; `GlaciationGrade`; `precipitation_class`) are **derived from body params** (stellar type / orbit / hydrosphere / lithology / temperature history), never authored. Lore-anchored bodies (Kallast = plains, Velen = coast, Cygni B = volcanic) are honoured by **setting params** and stand as **validation cases** — if a body reads wrong, fix its params, never patch the derivation. *Validation caveat:* derived terrain can only honour a lore body if its params permit it (e.g. tidal flats require a moon param for the D-228 tidal term — verify Velen's params before treating its coast as a contract). **(2) Climate — district temperature is the primitive; moisture is a separate primitive.** A simple scalar **district temperature in °C, resolved per 2×2 km district, nullable**, derived from: the **sun** (luminosity + insolation), the **planet** (the district's latitude + elevation lapse rate + orbital/axial phase = the season term, *and* day-phase = the diurnal term), and the **atmosphere** (greenhouse → the base, and heat-retention → the **diurnal-swing amplitude**; thick air = small day/night swing, thin = large). **No atmosphere → temperature is `null`**, and the entire climate/vegetation/weather branch is simply absent (an airless body's surface ice is *geology* per [D-227](#d-227), not climate). Tiles inherit their district's temperature. **Moisture** is the second primitive (water availability, from `hydrosphere`). **Everything climatic derives from temperature (+ moisture):** precipitation = f(temp, moisture); vegetation/treeline = temperature bands × elevation; long-term / seasonal-minimum temperature → `GlaciationGrade`; the cheap region seasonal/clock state ([Q-105](../questions/architecture.md#q-105)) is literally temperature(time). This formalises [D-210](#d-210)'s temperature proxy into the keystone scalar. No authored climate inputs anywhere. **(3) Freeze & snow — a scattered phase transition, transient in the marginal band.** Ice/snow is not a hard `temp < 0` contour but a band with **deterministic, spatially-coherent scatter** (seed-noise → a ragged, natural freeze line + microclimate; clustered patches, never per-tile dice), branched by surface type: - **Fresh / "sweet" water (lakes, rivers):** scatter band **+5 °C → −10 °C (district mean)** — the upper +5 is night-frost (the diurnal swing) reaching the coldest / most-exposed tiles even when the mean is above zero; −10 = frozen across the whole day/night cycle. - **Salt water (oceans):** its own **lower, wider band** (seawater onset ≈ −2 °C) with a distinct **pack-ice spatial pattern** (sheets + leads, not a lake skinning over). - **Snow (land):** the land parallel, **gated on moisture** (cold + wet → snow accumulates; cold + dry → bare frozen ground), same scattered band. - **Transient:** because temperature is clock-bound (season + day/night), in the marginal band ice/snow **forms in the cold phase and melts in the warm phase** — dawn frost burns off, a stream iced at dawn is crossable by noon. Passability is therefore time-of-day dynamic. (Forward contract to [Q-105](../questions/architecture.md#q-105).) **(4) Anti-"squaring" (warp).** A global position-keyed **domain-warp field** suppresses grid/seam artifacts — a **stateless, hash-based** pure function of `(seed, body_id, position)` (no lookup table / thread-local cache, so order-independent across threads/platforms). It keeps **f64 sub-metre precision through to the final voxel coordinate, then quantises by truncation to the integer voxel address** (a cast, not a comparison → IEEE-754-deterministic across targets). The ±8 m warp range makes FMA-contraction ULP variance (~1e-15 m) unable to shift the rounded voxel, so no platform guards are needed. The warp is *position math*, not a structural decision — [D-010](#d-010) integer discipline on all material/morphology decisions is preserved. **(5) Morphology — 8 families** via a strict **decision tree over integer `RegionProfile` inputs**: LavaField · FjordWall · CliffCoast · BraidedDelta · DuneStrand · IncisedGorge · MeanderReach · AlluvialPlain (fallback). Adds `tectonic_class` to `RegionProfile`. MountainPass is a zone label sharing IncisedGorge geometry. **Hard gates (boolean, pre-selection, in tree order):** fjord requires `GlaciationGrade ≥ 2`; LavaField requires `tectonic_class = Volcanic`; lithology bounds slope/form (see laws). **(6) Frozen 17-zone `MorphologyZone` display vocabulary** (derived labels per [D-228](#d-228), enum→label map frozen). The **canonical freeze point is the Rust `MorphologyZone` enum**; adding/renaming/removing a zone **requires a D-record amendment** (reviewer-enforced; classifier tuning that merely re-classifies a region is *not* a vocabulary change and stays free). The 17: open ocean, lake, tidal flat, dune strand, cliff coast, fjord, delta, estuarine, alluvial plain, river bank, meander reach, braided plain, valley floor, mountain pass, alpine, volcanic, wetland. **Zones ≠ families:** four (tidal flat, estuarine, alpine, wetland) are derived sub-classifications from family + elevation/water-height, not distinct generator families. **(7) Zone seams — prevent incompatible, allow valid (never patch).** The boolean gates are discontinuities, so "continuous inputs → continuous classification" is *not* the mechanism. The mechanism is **decision-tree gate ordering + a build-time compatibility-matrix invariant**: incompatible family pairs (e.g. MeanderReach↔Volcanic) cannot be adjacent classifier outputs (a build-time test, never a runtime override). Valid geomorphic seams (cliff↔fjord at the glaciation threshold, lithology faults) are **permitted sharp transitions** — kept sharp (real geology is sharp), made non-grid/organic by the warp; no modulation feathering. The warp also prevents degenerate slivers at chunk scale. **(8) Believability laws (binding):** drainage monotonicity (respect the D8 thalweg, [D-208](#d-208); tributaries join upstream; mouths at sea level); lithology→landform (Rock → vertical faces; Sand → ≤~32° angle of repose, dunes not cliffs; Gravel → braided channels/fans not single-thread meander; Soil → rolling/floodplain; Wetland → ≤5° flats; Lava → sheets/shield slopes + tubes, immature drainage); glaciation→form (fjord ≥2, U-valleys ≥1, moraines ≥1, cirques ≥2; grade 0 = V-ridges, never glacial U); climate→vegetation (treeline Forest→Scrub→Barren, no skip; riparian Thicket/Scrub 1–3 tiles along perennial waterways) — all flowing from the temperature primitive. **(9) Game-feel constraints:** every chunk carries ≥1 tactical decision point; cover ≠ concealment; high-ground asymmetric-but-not-dominant; chokepoints narrow enough to matter (river crossings 3–15 m, gorge floors 2–8 m); seasonal/tidal state produces **real passability changes, not cosmetic** — BraidedDelta/MeanderReach `ElevationDelta` calibrated so channels fall below and levees above the [Q-105](../questions/architecture.md#q-105) high-water threshold. **(10) Mechanics:** no per-tile `flow_direction[64×64]` in `ChunkContext` (D8 ≈ 152 m/cell, coarser than a chunk) → one basin-direction + global meander-curve params; features with wavelength > 64 m seed from **Region-or-higher**, not the chunk seed. `RegionProfile` is stored in `BodyWorldState` ([D-203](#d-203), ~6k/body, populated in the [D-206](#d-206) background pass) so the Atlas reads zone labels without triggering voxel derivation; voxel derivation is on-demand + cached, never persisted ([D-227](#d-227)). Budget ~2.2–4.2 ms/chunk (validate per-family in Phase 4 — FjordWall/IncisedGorge are far costlier than the AlluvialPlain fallback). - **Rationale:** [D-227](#d-227)/[D-228](#d-228) fixed *what* a tile is (derive-don't-store voxels; orthogonal derived axes); D-239 fixes *how* the finished upper cascade (L0–L4) becomes actual tiles — the one unbuilt layer. The district-temperature primitive collapses the scattered climate inputs (precipitation, glaciation, season, snow/ice, vegetation) onto one derived scalar + moisture, keeping the whole climate branch deterministic and null-cutting airless bodies cleanly. Prevent-at-source seams + the warp give coherent, organic terrain without a runtime patch. The frozen vocabulary protects authored Atlas/wiki content while letting the classifier evolve. - **Raised by:** tile-derivation-contract workshop (Tyre — refinement chain, warp, determinism; Gestalt — 8 families, game-feel; Troblum — feasibility, warp precision, scale corrections; Miri — believability laws, vocabulary, lore reconciliation), lead-interviewed decisions + an adversarial verification pass, 2026-06-07. - **Dissent:** Tyre's initial cross-family elevation-blend was resolved against (prevent-at-source). Early-integer-truncation of the warp (raised against Gestalt's `ElevationDelta` ranges and by Tyre) was resolved against in favour of f64-to-voxel. - **Implementation note (T-1024, 2026-06-07):** §1 per-body `RIVER_THRESHOLD` is now a derived field (`derive_river_threshold`) on `RegionProfile`. §2 district temperature is a nullable `f32` on `RegionProfile` sampled from a per-region climate derivation (`derive_temperature_c`); moisture is a separate integer primitive (`derive_moisture_q`, 0–100). Both populate the `RegionProfile` carrier during the L4 cascade run. Climate inputs use a **hybrid strategy**: stellar luminosity and spectral class are sourced from the import pipeline (`bodies.axial_tilt_deg`, `star_systems.spectral_class`); greenhouse offsets and diurnal amplitudes are tunable at runtime via `server/data/climate_constants.toml` (source-canonical TOML, not hardcoded). - **Implementation note (T-1025/T-1027, 2026-06-08):** §6's frozen 17-zone `MorphologyZone` enum is realised in `server/src/simulation/generator.rs` (`repr(u8)`, discriminant-pinned). The §5 8-family gated classifier + §7 compatibility-matrix invariants live in `derive_morphology_zone` (`region_profile.rs`). **16 of the 17 zones are reachable at RegionProfile scale; `BraidedPlain` is the exception** — distinguishing it from `Delta` needs a lithology signal (§8 Gravel→braided) that `RegionProfile` does not carry, so `BraidedPlain` is **deferred to ChunkContext** sub-classification. The §7 compatibility invariant is enforced as a **classifier-gate-ordering** property (a build-time test that a single region cannot yield a forbidden pair), per §7's "build-time test" language; genuine cross-region sharp seams (cliff↔fjord, lithology faults) remain permitted. §2 climate-derived fields (`precipitation_class`, `glaciation_grade`, `vegetation_class`) all derive from the temperature(+moisture) primitive (T-1025). - **Amendment (T-1082, 2026-06-28):** §5's family set grows from **8 to 9** — a `WaterBody` generator family is added for the water zones **OpenOcean / Lake / TidalFlat**, which previously fell through to the dry-land `AlluvialPlain` fallback (the D-245 believability bug: oceans rendered as dry forested land — `water=Dry` + `Forest` on the sea). `WaterBody` lays `Water::Deep`/`Shallow` (a Shallow shoal band on the district-anchored coast line `coast_anchor_m`, Deep beyond; TidalFlat is all-Shallow), a seabed `TerrainMaterial` (`Rock` on steep districts / `Sand` on gentle / `Wetland` for tidal mud), `Vegetation::Barren`, the water surface at `elevation_m = 0` (the §8 mouths-at-sea-level convention), and seasonal `Ice` via `derive_cover` (frozen seas). This **refines §6**: open ocean / lake / tidal flat are still derived zone *labels*, but their *generator* is now `WaterBody`, not `AlluvialPlain` — so §6's "tidal flat … not [a] distinct generator famil[y]" is superseded for the dispatch of those three zones. `Wetland` stays a land zone on `AlluvialPlain` (a marsh — saturated ground, not open water). Lives in `server/src/atlas/voxel.rs` (`MorphologyFamily::WaterBody` + `generate_water_body`, dispatched by `zone_to_family`); verified by the T-1083 believability harness (water-renders-wet flips PASS for Arbour + Edict). The §5 8-family decision-tree classifier (`derive_morphology_zone`) is unchanged — this is a voxel-tier *generator* family, selected by zone, not a new RegionProfile classifier branch. - **Amendment (T-1080, 2026-06-28):** §2's **moisture** primitive is now a **body ceiling × per-district spatial gradient**, not a single body constant. The hydrosphere+atmosphere value (`derive_moisture_q`) is the *wettest* a district on the body can be; per-district **latitude** (equator wet → pole dry), **elevation** (orographic / rain-shadow), and **continentality** (`100 − ocean_fraction_q`, coast wet → interior dry) subtract from it. Integer (D-010); coefficients in `ClimateConstants` / `climate_constants.toml` `[moisture_gradient]` (provisional, Q-123 calibrates). This mirrors D-240's latitude-graded *temperature* model and fixes the T-1080 believability bug (`moisture_q = 80` for all 2 048 districts → uniform vegetation/terrain — "nothing to fuzz"). It **propagates**: `precipitation_class`, `vegetation_class`, and the `morphology_zone` Wetland gate all read `moisture_q`, so those diversify for free. Lives in `district_profile.rs::derive_moisture_q`; verified by the T-1083 believability harness (moisture-gradient flips PASS, `distinct` 1 → ~49 on Arbour). The body-scale ceiling keeps each world's character; the gradient varies it within. - **Amendment (T-1126, 2026-07-17):** §8's climate→vegetation law predates a water class — `derive_vegetation` was ocean-blind, so district-resolution maps painted Forest across open ocean (T-1123 finding; the voxel tier masked it in-world via the T-1082 `WaterBody` generator, but the district carrier itself disagreed with its own morphology). `VegetationClass` gains **`Marine`, appended at discriminant 6** (after `RiparianThicket = 5`; the load-bearing discriminant-pin test is extended). The verdict is **morphology-derived**: `build_district_profile` derives `morphology_zone` first and passes `open_water = (OpenOcean | Lake)` into the vegetation call — vegetation and morphology can never disagree by construction, and **no fourth ocean threshold exists**. Precedence: airless (`temperature_c == None`) still wins — an airless body has no climate/vegetation branch at all (§2), so its seas read `Absent`, not `Marine`. The `Ord` position of `Marine` is **non-semantic**: the §8 Forest→Scrub→Barren density-ladder reading does not extend to it (discriminants are append-only serialisation order). `near_perennial_water` (the riparian river-proximity signal) is unrelated and unchanged. - **Amendment (T-1081, 2026-06-28):** the voxel tier now carries **mid-scale relief** (D-243 §2's invented terrain). Before, the family generators set `elevation_m` from `elev_q` at a compressed scale plus only ±4 m per-voxel micro-scatter — the walkable surface read flat (the D-245 "0–3 m, no hills to navigate by" bug). A `voxel_relief` pass (`detail_scatter.rs`, the `terrain_detail` fBm at a **0.13–1 km sub-district octave band** — all finer than the 2 km district, so it never competes with `elev_q`'s district-scale role — rather than the district 4–40 km band; body-global `SeedDomain::VoxelRelief` seed, position-keyed; the `f64` perturbation truncated to integer metres before assignment — D-010) is added post-dispatch in `derive_voxel_column`. Two load-bearing choices: **(a) the relief envelope is `slope_q·3 + elev_q`, not slope alone** — the coarse heightmap (~40–78 km/px) yields `slope_q ≈ 0` even on high ground (observed max 13 on Arbour), so gating on slope would invent nothing; folding elevation in makes high terrain read rugged and coastal flats stay gentle. **(b) Only the flat families** (AlluvialPlain / LavaField / BraidedDelta / DuneStrand / MeanderReach) take it — CliffCoast / FjordWall / IncisedGorge already generate strong internal relief and a position-varying field would warp those features (e.g. drown a gorge's wall-to-floor drop); WaterBody stays at sea level. The relief **span** (`VOXEL_RELIEF_SPAN_M = 100`) and the believability voxel-relief threshold (≥ 8 m) are provisional (Q-123 calibrates, like the moisture coefficients). The span is held modest deliberately: relief sits on the compressed `elev_q/N` base (max ~50 m) and `elevation_m` clamps at 0 (sea level), so an oversized span clamps away on low ground (drowning the relief, biasing it positive) — a bigger span belongs with the deferred absolute-elevation model. Verified by the T-1083 harness — a new `voxel_relief` contrast metric (mean within-district elevation range across a district-spanning transect, since a single 64 m sample chunk is narrower than the relief band) flips PASS (≈22 m on Arbour + Edict). **Deferred:** the *absolute* elevation span is still the compressed `elev_q/N` base — a per-body hypsometric relief model (so a body's true max relief sets the ceiling AND gives the relief headroom to grow without clamping) is a later refinement; the mid-scale relief gives navigable hills now without it. - **Cross-reference:** [D-227](#d-227) (derive-don't-store voxel model), [D-228](#d-228) (composite tile axes / cohesion / seasonal state), [D-210](#d-210) (temperature proxy — formalised), [D-203](#d-203) (BodyWorldState cache), [D-206](#d-206) (background analysis pass), [D-208](#d-208) (drainage / D8), [D-010](#d-010) (determinism), [D-234](#d-234) (street/footprint geometry — consumes morphology), [D-142](content.md#d-142) (zone types), [D-217](#d-217) (tile condition), [Q-102](../questions/architecture.md#q-102) (cohesion = the warp), [Q-103](../questions/architecture.md#q-103) (mutator schema — open), [Q-105](../questions/architecture.md#q-105) (seasonal/clock state — temperature/ElevationDelta forward contract) --- ### D-240: Registry orbit/star data is non-canonical — climate derives from a `planet_class` temperature envelope - **Decision:** A body's registry **orbital position and stellar assignment** (`orbital_period_days`, `spectral_class`, `star_type`, and any derived `distance_au`/luminosity) are **non-canonical placeholder data** — bodies were dropped into orbit and stars taken as they sat in the registry, with no authored physical intent. **No derivation layer may consume them as physical inputs.** Climate derives **only** from the authored, intent-bearing fields: `planet_class` (sets the temperature envelope), `atmosphere`, `hydrosphere`, plus the intrinsic latitude/elevation of the generated surface — with per-world variety from the deterministic body **seed** ([D-010](#d-010)). Temperature is a **`planet_class` → (min, max) °C envelope** (source-canonical in `server/data/climate_constants.toml`, tunable): latitude maps the region across the envelope (pole → cold end, equator → warm end); atmosphere / hydrosphere / elevation modulate **within** the bounds; the seed adds a small bounded nudge for inter-world character. A body can never derive outside its class band by construction. This **supersedes the sun-driven (Stefan-Boltzmann luminosity + distance) portion of [D-239](#d-239) §2**. - **Rationale:** Deriving physics from arbitrary registry values dresses placeholder data as authoritative — *worse* than an obvious error, because it looks real. The sun-driven path produced physically impossible results (a `temperate`/liquid-water world deriving to **+356 °C**, because a NULL `spectral_class` defaulted to a G-type Sun on an M-dwarf-dominated Gliese catalog — a 25× luminosity error). The 2026-06-08 audit found the blast radius is exactly one function (`derive_temperature_c`); every other reference merely plumbs the columns, so once temperature is re-based the orbit/star columns can be dropped from the derivation inputs entirely. Anchoring to `planet_class` — which already encodes the authored intent (`frozen`, `tropical`, `hot_arid`, …) — keeps every world inside its band and makes the T-1031 believability harness's class-consistency check true by construction. - **Raised by:** Jeroen (2026-06-08), reviewing T-1024 climate output across 25 non-Kallast bodies. - **Cross-reference:** [D-239](#d-239) §2 (temperature primitive — the sun-driven portion is superseded here), [D-010](#d-010) (determinism — seed-based variety), [D-227](#d-227) (derive-don't-store), [D-228](#d-228) (tile axes). --- ### D-242: Corp HQ settlement model — HQs are not cities; separate from the name pool, specialization-keyed, install-baked - **Date:** 2026-06-14 - **Decision:** A corporate **headquarters is not a city** and does not belong in the `atlas_city_names` flavored name pool. - **Remove the corp-HQ cross-reference into the city pool.** The [D-207](#d-207-fully-generative-placement--markersjson-stripped-to-topographic-features)/[D-223](#d-223) `reserved=1` step (`populate_atlas_city_names_corps`, `tooling/economy-db/economy_import/atlas.py`) inserts **one `atlas_city_names` row per corp HQ** whose name isn't already pooled. With no `UNIQUE(body_id, name)` it produces **duplicate co-named "cities"** — e.g. **10 `Groombridge` rows on GJ380c**, one per corp — which every downstream spatial layer (placement, roads, hinterland) then renders as separate settlements scattered across the map. This insert path is removed; the HQ relationship lives with the corp (`corporations.headquarters_system`, plus a resolved `headquarters_body`), not in the city pool. This **supersedes [D-223](#d-223)'s** "the `reserved=1` corp-HQ cross-reference stays" note. - **HQ placement is a baked, specialization-keyed preference.** Each corp's HQ is `CityTenant` or `Standalone`, chosen by a **source-canonical `specialization → HQ-placement` map** (`corp_type` is degenerate — only `corporation`/`combine`, 158/7 — so the key is `corporations.specialization`, optionally `supply_chain_role`): - **CityTenant** (finance, media, retail, services …): the HQ is a **tenant of an existing city** — a corp→city link, **no new settlement**. Many corps may tenant one city (supplemental, non-competing HQs realistically cluster — a bank and a newspaper share a downtown). - **Standalone** (extraction/mining, heavy industry, shipyards …): the HQ **is its own settlement**, emitted into the cascade settlement list with its own body + position, so the road graph and downstream layers treat it as a place. - The preference depends only on corp specialization — **not on seed or install** — so it is authored once and baked into `systems.db` at import (a difference of corp *type*, not of run). - **`population` and `settlement_class` are install-baked, not seed-derived.** Body-level `population` is already baked (273/3 240 bodies > 0); the **per-settlement spread is carried down into the city/settlement source data**, so every settlement starts with a fixed `population` + `settlement_class`. **Only position remains seed-derived** (the [D-211](#d-211) placement pipeline). This **supersedes [D-223](#d-223)'s** "`population`/`settlement_class` deferred to placement (T-955)" and **retires the never-built "assign population from the seed" step** — `match_cities` only ever *read* those fields (all `0`/NULL) and nothing assigned them, so faking population in the seed layer buys nothing over baking it. - **The cascade settlement input** = the city name pool (cities, baked pop/class) **+** standalone-HQ settlements, merged. The Layer-2 road graph ([D-211](#d-211), T-1038) routes over that merged list unchanged; "hubs" are the significant cities, standalone HQs are typically minor nodes. - **Rationale:** Modelling HQs as city rows conflated two different things and made corp-dense worlds nonsensical — ten "Groombridge" dots, ten trunk roads, where there is one city hosting ten tenants. HQs *are* sometimes their own settlement (a mine or shipyard sits alone) and *sometimes* a tenant (a bank wants a downtown); that split is intrinsic to the corp's specialization and never varies by run, so it is authored, not derived. Baking `population`/`settlement_class` (rather than deriving them from the seed) keeps generation deterministic with **only position variable**, matches the already-baked body population, and deletes an unbuilt seed step instead of building it. The names pool ([D-223](#d-223)) returns to being **cities only**. - **Raised by:** Jeroen, 2026-06-14 — reviewing the T-1038 road-graph hub model; the corp-HQ-as-city rows surfaced as duplicate map settlements. - **Implementation:** corp-HQ model + specialization map + standalone-HQ settlements, remove the cross-ref insert, resolve `headquarters_body` (**T-1074**); per-settlement population/settlement_class baking (**T-1075**); and the gated road-graph hub refinement — scaled-cap hubs + co-location collapse + hybrid minor-settlement attach + the deferred `RailHeadFacing` junction pass (which itself needs a [D-213](#d-213) amendment) — (**T-1076**, blocked by T-1074 + T-1075). All under Phase 4 (epic T-750). The T-1038 road-graph **core** (MST + A\* + MaintenanceAuthority + waypoints + junction detection) ships independently of this record — it routes over whatever settlement list it is handed. - **Implementation status (T-1074/T-1075, 2026-07-16): done.** Both stories landed together, in order. - **`corp_type` was already known degenerate** (158 `corporation` / 7 `combine`) — confirmed the key is `corporations.specialization`, not `corp_type`. That free-text `specialization` column is legacy prose (only 20/165 rows ever populated, by the orphaned `tooling/populate-corporations.sh` — **retired/deleted**, T-1074 point 6) and is left alone (`generate_corporations`'s fuzzy brand-relevance match still reads it). The HQ-placement key is a **new, separate column**: `corporations.corp_specialization`. - **Vocabulary choice — reused AND extended, per this record's own instruction to prefer reuse.** `corp_specialization` reuses the [D-237](#d-237) `specialization_vocabulary` id-space directly rather than a parallel corp-only taxonomy — every value already carries `bulk_class_projected` (`NonPhysical` vs everything else), which *is* the CityTenant/Standalone signal this record calls for. Backfilling all 165 corps (155 with a wiki page; the categorization pass read every one) found the 27 system-authored values genuinely under-cover corp-shaped identities: ~10% of corps do **trade/logistics distribution**, **hospitality/tourism**, **general professional/advisory**, or **licensed multi-site clinical** work — moving or selling what someone else made, or delivering certified services, not producing goods — which none of the 27 values name. Four values were **added** to `specialization_vocabulary.toml` (all `NonPhysical` → `CityTenant`): `trade_distribution`, `hospitality_hub`, `professional_services`, and `licensed_clinical_services` (the fourth via PR #177 review M1 — Somatic Futures is a 40+-clinic *licensed network*, one of several certified operators: neither generic consulting nor `longevity_monopoly`, which is Prometheus, THE sole facility). The vocabulary is now 31 values, shared by two authored axes: `system_economy.economic_specialization` ([D-237](#d-237), system-scale) and `corporations.corp_specialization` (this record, corp-scale) — nothing stops a future system from adopting the 4 new values too. Per-corp authoring lives in wiki frontmatter (`wiki/corporations/*.md`, key `corp_specialization`) — the natural per-corp source, parsed by `_parse_corp_frontmatter`/`load_wiki_corps` (extended) — not a per-corp TOML stanza file (unlike D-237's per-*system* TOML, 165 corps would make one file unwieldy; one wiki page per corp already exists and is the authoritative source for everything else about it). The corp pages are part of the `import_economics` meta-stamp source set (PR #177 T1): a frontmatter edit without a regen trips the stamp check like any TOML edit. - **The placement map is authored explicitly, not computed from `bulk_class_projected` at import time** — `wiki/economics/corp_hq_placement.toml`, one `hq_placement` stanza per vocabulary value (all 31), defaulting to the `NonPhysical` rule but reviewable/overridable per value without a code change. Standalone stanzas also carry `standalone_economic_role`, mapping the 31-value specialization onto the pre-existing 10-value `economic_role` vocabulary ([D-195](#d-195)/[D-197](#d-197)) the emitted settlement row needs (`match_cities`/`CompatibilityMatrix` index by the 10-value set, not the 31-value one). - **`headquarters_body` is recomputed from source on every run — never kept from prior DB state (PR #177 H1/H2).** The importer-owned `corporations` columns (`corp_specialization`, `hq_placement`, `headquarters_body`) are NULL-reset and re-derived each run: `corporations` is append-only, so a keep-if-set guard would freeze the first run's output forever and silently promote DB state to source (exactly the asset-pipeline golden-rule inversion — the review caught the original one-shot backfill doing precisely that). An authored `headquarters_body:` frontmatter override wins where present (hard-validated: body must exist and sit in the corp's HQ system; none authored yet); otherwise `most_populated_body_in_system` (`atlas.py`, descended from the retired cross-reference function's heuristic) derives it with a four-stage deterministic tiebreak: **population DESC → city-bearing body (has `atlas_city_names` rows) → body-type rank (`planet` < `moon` < `gas_giant` < belt/oort) → `body_id`**. The city-presence tier is what the review's H2 added — the first draft ranked body-type directly and rested on a false premise ("belts can't host settlements"): belts in this world *do* host them (GJ845-belt: Orkney Ceramics; GJ268-belt: Jeju Lattice; GJ222A-belt: two Standalone HQs), so presence keeps settlements where they are, and the type rank only fires in fully cityless, fully unpopulated systems (GJ 702B — 12 bodies, zero population, zero city rows → planet `GJ702Bb` instead of the belt the old alphabetical sort picked). The presence set is read at step 7b, *before* the run's pool rebuild — i.e. the previous run's settled state, a deliberate, documented hysteresis (settlement continuity across regens) whose fixpoint is stable: each run's output reproduces the presence distribution it read. - **CityTenant is a new schema link, not a repurposed one.** `corporations.headquarters_city_id` (nullable FK → `atlas_city_names.id`) is the "corp→city link" this record calls for — the OLD `atlas_city_names.corp_id` column is left in place (additive-safe, no `DROP COLUMN`) but is now permanently `NULL`: nothing writes it once the cross-reference insert is gone, and nothing ever read it downstream (confirmed: zero references in `city_context_reader.rs`/`attractor_matching.rs` — the Rust consumption side never depended on it). - **The merge is free.** Standalone-HQ settlements are inserted as ordinary `atlas_city_names` rows (`populate_standalone_hq_settlements`, Phase B) — same schema, no corp-linkage marker — so `read_body_settlements` → `match_cities` (the "natural seam") needed **zero Rust changes**; a Standalone HQ competes for attractors purely on population/`settlement_class`, exactly like any pooled city (verified live: `cascade_for_body("GJ251c")` places "The Gate Corporation" alongside "Tributarium"/"Ruhr" through the unmodified pipeline). Two new Rust unit tests lock this invariant in (`city_context_reader.rs`, `attractor_matching.rs`). - **Verified against real, repeated `make regen-db` runs — each run a full recompute, not a no-op (wording corrected per PR #177 H3; the pre-review text described the one-shot backfill being stably inert):** 0 duplicate `(body_id, name)` groups (was the whole bug); 155/155 wiki corps get `corp_specialization`/`hq_placement`; **154/155 `headquarters_body` re-derived on every run** (the 1 gap is a pre-existing broken `headquarters system` reference on `sova-station-works.md`, unrelated to this record), with two consecutive runs producing **zero differences** across all 165 corps' placement columns and tenancy targets; against the prior committed snapshot the recompute changes **exactly 2 placements** — `prometheus-labs`/`kovalev-freight`, `GJ702B-belt` → `GJ702Bb` (the intended H2 outcome) — and **zero tenancy targets** (`calluna-wellness` → (GJ845-belt, Orkney Ceramics) and `namsan-collective` → (GJ268-belt, Jeju Lattice) preserved via the city-presence tier). 95 `Standalone` corps → 94 settlements emitted (same 1 `sova` gap) + 60 `CityTenant` corps → 58 links (2 unmatched: the GJ 702B pair — now correctly on planet `GJ702Bb`, which still has zero authored city names, like every body in that system: a wiki content gap [T-1115], logged, not an import error). A poisoned-copy test confirmed no DB state survives the reset: hand-written garbage in all three columns is wiped and re-derived (wiki corp) or reverted to NULL (legacy corp with no authored source — the removed-key case). The 10 legacy DB-only corps seeded by the now-deleted `populate-corporations.sh` (`mvg`, `adams-ford`, `dsmc`, and the 7 Cygni-B "combines") have no wiki page to author `corp_specialization` from and are reset to NULL across the board each run — a logged, non-fatal gap (they are load-bearing for 2,468 `brand_products` rows via `generate_brands`, which reads `corporations` directly and doesn't care about wiki-page existence, so their rows are kept, not deleted). - **Amendment (T-1075, 2026-07-16 — population/class derivation clarified):** the phrase above, "the per-settlement spread is **carried down into** the city/settlement source data," is superseded by **derivation-at-import, not hand-authoring**: `import_economics` computes the per-city population spread from the authored `bodies.population` on **every** `regen-db` run, via a documented integer rank-size (Zipf, exponent 1) curve (`atlas.py::_zipf_population_spread`, constants named + commented — tuning the curve is a deliberate code change, not a data edit). There is no per-city population *source file* — only `bodies.population` (already-baked) is authored; the per-settlement split is 100% derived. The spread runs over the **corrected, merged pool** (post-T-1074: Standalone-HQ settlements are ordinary rows by the time this runs, so they get a real share of their body's population like any city, not a bolted-on afterthought). `settlement_class` defaults every pooled row to `PopulationBudget` (D-196); a small authored override TOML (`wiki/economics/settlement_name_locked.toml`, `[[hero]]` stanzas keyed on `(body_id, name)` — not the volatile numeric id) pins 8 hero settlements to `NameLocked`. **EconomicTriggered and OrganicGrowth stay simulation-time** (D-196) — out of bake scope; so is the D-196 *generation-time* consumer that reads the baked population against the >=50k-active/<5k-ghost thresholds to decide skeleton depth (confirmed live: no such consumer exists yet anywhere in `server/src/atlas` — this record's bake only writes the values a future ticket will read). - **Amendment (T-1076 item 0, 2026-07-16 — the Standalone/CityTenant boundary made explicit; Jeroen's ruling):** the original examples ("extraction/mining, heavy industry, shipyards …" → Standalone) were operationalised at T-1074 as *`bulk_class_projected ≠ NonPhysical` → Standalone* — an over-generalisation that made every physical-goods specialization a standalone settlement (95/60 split) and produced a **winery and a leatherworks as standalone asteroid-belt settlements**. The boundary is now explicit, so it cannot be re-coarsened: **Standalone applies ONLY where the HQ is itself an industrial complex / company town** — (1) extraction/mining/quarrying, (2) heavy vehicle + shipyard manufacturing, (3) heavy energy (refinery/extraction complexes), (4) gate/orbital infrastructure fabrication. **Everything else is CityTenant**, explicitly including craft/consumer manufacturing (distilleries, breweries, wineries, ceramics, textiles, furniture), agriculture of every scale (the estate/agribusiness *office* sits in a market town; the fields are not the HQ), and high-tech fabs. Applied per-value in `corp_hq_placement.toml` (one-line justification on every flipped stanza): 9 values flipped Standalone→CityTenant (`estate_farming`, `breadbasket`, `terroir_agriculture`, `terroir_spirits`, `terroir_organics`, `general_industrial`, `consumer_goods_bazaar`, `precision_tech`, `marine_farming`); 11 stay Standalone (`ore_extraction`, `company_mining`, `marble_monopoly`, `rare_mineral_extraction`, `lattice_material_source`, `fuel_production`, `geothermal_hub`, `shipbuilding`, `vehicle_production`, `gate_fabrication`, `military_industrial`). Corp split moves 95/60 → **11 Standalone / 144 CityTenant**; `atlas_city_names` 423 → **340** rows (329 pool + 11 HQ settlements); tenant links 58 → **129** (15 unmatched: 14 on bodies with no city to tenant + `sova-station-works`, whose broken HQ-system ref predates this record). The vanished belt settlements (Orkney Ceramics, Jeju Lattice, Groot Karoo Cellars, Kalahari Leatherworks) took their two belt tenancies with them — expected, correct data change under the boundary; the H2 placement hysteresis converged in one transitional run (fixpoint verified: two subsequent regens byte-identical across all 165 corps, 0 duplicate `(body_id, name)` groups, 0 import errors). Cygni B's `NameLocked` hero pin moved from the retired "Cygni Combines" HQ settlement to its pure-pool city "Metropolis". **Jeroen's rendering note, recorded here so it is not re-litigated as placement:** CityTenant HQs often sit on a settlement's **edge** or in **industrial clusters** — that is CityTenant *rendering/district-placement color* for later phases (building placement inside the settlement), **not a third placement class**. - **Amendment (PR #178 M1 + T1, 2026-07-16 — five steel-complex corps re-tagged; hub-cap scale note):** Miri's content pass verified 19/20 of the item-0 boundary judgments but found five already-authored corps that ARE the steel-complex identity the `general_industrial` flip comment reserved for its own value — re-tagged in wiki frontmatter (one-line justification on each page): `cygni-combines` → `shipbuilding` (the Cygni B yards' hull-structure consortium — the D-237 hero identity, fits directly); `sova-station-works`, `stalownia-kowalski`, `westphalia-heavy-works` → `military_industrial` (habitat/orbital-module and heavy-equipment works — the `heavy_equipment`-anchored yard/complex value; **note:** it now carries civilian heavy works, not only military-administered industry — if that reads wrong later, the clean split is a new `heavy_works` vocabulary value, not re-coarsening `general_industrial`); `sede-chemical-works` → `fuel_production` (feedstock refining/chemical synthesis — the refinery-complex shape; the closest call, per Miri). `shipbuilding` and `military_industrial` previously carried zero corps, so this also closes a content hole. Post-M1 counts: **16 Standalone / 139 CityTenant**, **15** HQ settlements emitted (`sova-station-works` emits nothing — its broken HQ-system ref is T-1054), `atlas_city_names` **344** rows (329 pool + 15), tenant links **125** (14 unmatched); fixpoint immediate — two consecutive regens identical (every re-tagged corp's HQ body already carried pool cities, so the presence tier saw no transition). The Cygni Combines settlement returns on GJ820Bc; the Cygni B hero pin deliberately **stays on "Metropolis"** (Miri: the pool city is the canon-clean hero anchor — the combines' yard is a place beside the city, not the city itself). **Scale note (T1):** the road-graph hub cap (`HUB_SPACING_DIAG_PX`, T-1076 §1) keys off the working-grid diagonal, a fixed 512×256 for every planet today — revisit that premise when [D-243](#d-243)'s elastic planetary seam gives bodies genuinely varying grid sizes. - **Cross-reference:** [D-207](#d-207-fully-generative-placement--markersjson-stripped-to-topographic-features) (corp-HQ cross-ref — the insert path is removed here), [D-223](#d-223) (names-only pool — the corp-HQ-stays + pop/class-deferred notes are superseded here), [D-211](#d-211) (placement — consumes the merged list; only position is seed-derived), [D-213](#d-213) (`FoundingOrientation` — a `RailHeadFacing` variant is the gated follow-on), [D-237](#d-237) (corp `specialization` vocabulary — the HQ-placement key, extended here from 27 to 31 values), [D-195](#d-195)/[D-197](#d-197) (the 10-value `economic_role` vocabulary `standalone_economic_role` maps onto), [D-196](#d-196) (`SettlementClass` — the bake target), [D-010](#d-010) (determinism — only position is seed-derived). - **Dissent:** None --- ### D-243: Spatial scale ladder — nested absolute containment with a single elastic planetary seam (resolves Q-110) - **Date:** 2026-06-14 - **Resolves:** [Q-110](../questions/architecture.md#q-110). - **Decision:** The world's spatial structure is a **fully nested, absolute-metre containment ladder** with **exactly one elastic seam** — the jump to planetary scale. Below the seam every level is a fixed integer multiple of the level below, in real metres, identical on every body; only the planetary seam floats per body. This resolves Q-110's three-way contradiction (region documented as ~1 km but implemented as 8 heightmap cells ≈ 624 km, with `chunk_context` assuming a third value) by fixing one ladder and one anchoring rule. **The ladder** (side length; the 2-D child count is the square of the linear ratio): | Level | Side | Linear ratio | Role | |---|---|---|---| | voxel | 1 m | — | the tile ([D-228](#d-228)/[D-220](#d-220)) | | chunk | 64 m | ×64 voxels | stream / derive unit ([D-222](#d-222), [D-239](#d-239)) | | block | 128 m | ×2 chunks | generator planning unit ([D-222](#d-222)) | | quarter | 512 m | ×4 blocks | settlement footprint cell ([D-222](#d-222)) | | district | 2,048 m | ×4 quarters | urban division **and** local-climate cell ([D-222](#d-222), [D-239](#d-239) §2) | | **region** | **204.8 km (~205 km)** | **×100 districts** | **top hard block** — climate/weather/season lockdown + planetary grid | | *(elastic seam)* | | | | | planet | per body | `round(2πR / 204.8 km)` regions | the **only** floating quantity | **(1) The elastic seam — region ↔ planet.** A body holds `round(2πR / 204.8 km)` regions around the equator (and half that pole-to-pole), `R` = `body_radius_km` ([D-204](#d-204)) — the single per-body quantity. The fixed 1024×512 heightmap ([D-202](#d-202)) is the coarse elevation field, **finer** than the region grid (≈ 5 heightmap pixels per region on an Earth-sized body) and consumed per **district**, not per region. Everything below the region is fixed integer math; `ChunkPos → … → RegionPos` is body-independent. **(2) Detail-scatter synthesis — the heightmap is the data ceiling.** The heightmap carries continental shape only (one elevation sample per ~40–78 km). All sub-heightmap detail (region → voxel) is **invented deterministically, not stored** ([D-227](#d-227)): the heightmap is interpolated and a **simple, spatially-coherent detail-scatter field** (a few octaves of seed+position-keyed value/fractal noise, shaped by local morphology + slope, continuous — never per-tile dice) is composed on top, alongside the [D-239](#d-239) §4 domain warp. It is calibrated to **read as plausible terrain at a glance** — visible, but never measured against a ground truth that does not exist (the player never traverses planetary scale continuously, which is *why* the top jump can be elastic). The containment ladder is for **addressing/streaming**, not for holding terrain features: large-wavelength landforms come from the low-frequency octaves of this field keyed to absolute coordinates, **not** from a container that size — which is why the ladder stops at the region and needs nothing coarser. **(3) Region is the climate/weather lockdown scale** (refines [D-239](#d-239) §2; feeds [Q-105](../questions/architecture.md#q-105)). Weather, the seasonal clock, and the temperature **baseline** are region-scale phenomena (~hundreds of km), resolved **once per region** — the cheap shared-dynamism source Q-105 sought; every district and tile inside inherits it. Climate is a **three-level stack**: **region** = the climate *context* (latitude-driven temperature baseline, weather state, season); **district** (2 km) = local temperature = that baseline **+** elevation lapse + slope aspect (this is [D-239](#d-239) §2's 2 km climate district, now derived as a *modulation* of the region rather than from scratch); **chunk/voxel** = the [D-239](#d-239) §3 freeze/snow scatter on the local temperature. "Region temperature" is the *context*, not a uniform 205 km slab — intra-region latitude (~1.8°) and elevation land in the district/tile modulation. "Locked down" = *defined at this scale*, not frozen in time (the weather/season state still ticks; the region is the unit it ticks at). **(4) Edge fuzz — climate does not change on a line.** The region (and district) is where climate is *computed*, **not** where it *steps*. A tile's climate value is a **continuous, warp-perturbed blend of the surrounding regions' baselines** (bilinear across region centres, displaced by a noise field so the blend boundary is ragged, not a straight gradient); the same applies at district edges for the local modulation. The ~205 km and 2 km grids are therefore **invisible in the output** — temperature, weather, and season grade smoothly and raggedly across boundaries the way a real frontal gradient does. This is the **climate counterpart to [D-239](#d-239) §4's domain warp** (same meta-rule: the addressing grid must never be visible in the result), with one distinction from [D-239](#d-239) §7: **morphology** seams stay *sharp-but-organic* (hard gates, warp-displaced — real geology is sharp), whereas **climate** is a continuous scalar field and is *feathered/blended*, not gated. **(5) Vocabulary** — fixed, and recorded in CLAUDE.md. "Region" now means **only** the ~205 km top hard block. The old 1 km "RegionProfile" scale is **removed** — its terrain/climate carrier role moves onto the **district** (2 km), aligning it with [D-239](#d-239) §2's existing climate district. [D-201](#d-201)'s tier-4 "Region" (50–500 km watershed/political) is renamed **Province** (its actual content — drainage basins, [D-205](#d-205), territory), an **overlay** painted across regions, *not* a containment rung. Region (a fixed metric grid cell) and Province (an irregular lore-bearing boundary) sit at overlapping scales but are different kinds and must stay distinct in the docs. No new words are invented (`sector` is reserved by `perception::VisibilitySector`; `tract` and the rest were rejected). - **Amendment (2026-07-24, body-map-viewer workshop — `gridunit` vocabulary added; full record [D-255](#d-255)):** **`gridunit` (at zoom step)** is added to the locked vocabulary — additive, no rung changes. A gridunit is the **per-step data-canvas cell** of the Atlas stepped zoom ladder ([D-255](#d-255)) — **not a new spatial rung**, a *role* name for whichever of this ladder's rungs a given zoom step is pinned to. **At every *fixed* rung, gridunit spacing equals one of this ladder's fixed metre values and never floats with viewport size or display resolution** — a fixed step's gridunit is a rung above, full stop. The Atlas ladder's fixed rungs run from **Region (~205 km, rung 1) down to chunk (64 m, rung 5, the deepest Atlas rung)**; **tile/voxel (1 m) is NOT an Atlas gridunit** (Phase-5 in-world content — [D-255](#d-255) / [D-166](#d-166) 2026-07-23 amendment). The **one exception to fixed-metre snapping is rung 0 (Global)**, the *variable* map opener: one gridunit per region, so its gridunit is a whole region (~205 km) but its **canvas extent floats with the body's region count — the elastic seam of this record made visible** (region count = `round(2πR/204.8 km)`). Global's gridunit is still not viewport-derived (it is the body's region grid, a fixed body property), so the "never floats with viewport/display" discipline holds; only the body's own region count varies. Separately, the **display ratio** (screen-px per gridunit — 1×1 ideal, ≥5×5 acceptable) is a free, client-side, viewport-dependent parameter, kept architecturally separate from gridunit spacing: two monitors requesting the same step get the **same** absolute-metre canvas (same cache entry) and merely display it at different px ratios (a GPU resize, never a re-derivation) — which keeps the derivation cache key free of any presentation parameter. - **Rationale:** The cascade glued absolute voxel/chunk scales to the body-relative heightmap with three contradictory assumptions and no record fixing metres-per-pixel, so nothing downstream could be metre-precise (Q-110). A rigid absolute ladder with a single elastic seam removes the contradiction at the source: the only body-specific function in the whole chain is region↔planet via `body_radius_km`; everything below is fixed integer math, unblocking the production wiring (T-1046). Every rung earns a job — the region in particular is not mere addressing: it is the natural lockdown scale for weather/season/climate (the Q-105 dynamism source) and the sane planetary grid (~195×98 regions on an Earth-sized body, vs an unusable ~19,500 districts). Confining invented terrain detail to a deterministic scatter layer, and climate variation to an edge-fuzzed continuous blend, honours derive-don't-store ([D-227](#d-227)) while keeping both believable — the grid never shows and the planetary seam is never walked. - **Raised by:** Jeroen, 2026-06-14 — driving Q-110 during the cascade-spine sequencing (next: wire the tier into production, T-1046). - **Implementation:** unblocks **T-1046** (`ChunkPos → … → RegionPos` is now defined); the region↔heightmap interpolation + detail-scatter synthesis + climate edge-fuzz is the elastic-seam stage (new ticket). [D-201](#d-201) amended (Region→Province). The canonical ladder is recorded in **CLAUDE.md**. Refines [D-239](#d-239) §2 (district temperature now modulates a region baseline) and contributes the region-clock answer toward [Q-105](../questions/architecture.md#q-105). - **Cross-reference:** [D-201](#d-201) (spatial hierarchy — tier-4 Region→Province, amended here), [D-202](#d-202) (heightmap 1024×512), [D-204](#d-204) (`body_radius_km`), [D-220](#d-220)/[D-222](#d-222) (locked sub-settlement dims — chunk/block/quarter/district), [D-205](#d-205) (province boundaries — the overlay), [D-225](#d-225)/[D-227](#d-227) (derive-on-demand / don't-store), [D-239](#d-239) §2/§3/§4/§7/§10 (climate primitive refined; warp; seams; budget reframes to per-active-derivation), [D-255](#d-255) (body-map-viewer render architecture — `gridunit` vocabulary + the Global rung-0 elastic-seam view), [Q-105](../questions/architecture.md#q-105) (region seasonal/clock state — answered at the region). - **Dissent:** None --- ### D-244: Asset rendering — 3D objects in-world; 2D limited to textures + flat artwork - **Date:** 2026-06-16 - **Decision:** The in-world view renders **3D objects directly**. Environment and props come from the Trellis image→3D `.glb` pipeline (`/glb-gen`, promoted per [D-241](process.md#d-241-spike-binary-artifacts-are-local-only--gitignored-never-tracked)); characters come from the Quaternius 3D-mesh pipeline composited at runtime via `CharacterVisualDescriptor` ([D-159](scope.md#d-159-character-body-type-enum-5-types--2-genders--1-child-skeleton-11-total)–[D-164](#d-164): 11 body types, segmented regions, separate head mesh, slot system, shape keys, region tints). The **only flattened 2D content** is **(a) textures** (PBR maps on 3D surfaces) and **(b) flat 2D artwork** — paintings, flags, billboards, signage, screen content — images that live as textures/decals on flat surfaces *within* the 3D world. There is **no per-object sprite layer**: game objects are not pre-rendered to 2D sprites for display. This **completes** the 3D pivot already ratified for the camera ([D-148](#d-148-30-low-angle-camera-with-45-map-rotation--supersedes-d-019), supersedes D-019) and live 3D characters ([D-149](#d-149-3d-characters-rendered-live-in-scene--not-pre-rendered-sprites)), extending it to all objects and the asset pipeline. - **Rationale:** The character system is irreducibly 3D — runtime slot compositing across body × head × hair × clothing × skin-tone × colour-overrides × shape-keys × directions × animation-frames cannot be expressed as pre-rendered sprites without combinatorial explosion. The 3D direction was already implicit in the character architecture (the 2026-03-19 character-asset doc states "no mesh work from the spikes carries over") and the Trellis environment pipeline, but was never ratified — so the retired 2D-sprite assumption drifted on in tooling and docs. - **What this retires / repurposes:** the early-spike **3D→2D-sprite render pipeline** (`renderer/`, the `/sprite-gen` skill, added in #541) is **not** the in-world format. It is **repurposed as the 2D-artwork generator** — producing the flat paintings/flags/billboards/signage assets in (b). The sprite-centric asset catalog (`docs/assets/visual/` sprites/tilesets framing, `_templates/sprite.md`) is re-scoped to a **3D-model + texture + artwork** catalog. - **Root-cause note:** T-1049/T-1050 (the 2026-06-12 `fable-ous.md` audit) re-injected the dead sprite model because the retired spike scaffolding sat in the repo as live ground truth with no decision marking it dead. This record is that marker; T-961/T-1049/T-1050 are held in backlog pending re-scope. - **Cross-reference:** **Builds on** [D-148](#d-148-30-low-angle-camera-with-45-map-rotation--supersedes-d-019) (3D camera, supersedes D-019) and [D-149](#d-149-3d-characters-rendered-live-in-scene--not-pre-rendered-sprites) (live 3D characters, not sprites) — D-244 extends both to all objects + the asset pipeline. [D-159](scope.md#d-159-character-body-type-enum-5-types--2-genders--1-child-skeleton-11-total)–[D-164](#d-164) (character 3D pipeline), [D-241](process.md#d-241-spike-binary-artifacts-are-local-only--gitignored-never-tracked) (asset promotion), [D-227](#d-227) (derive/synthesise, don't store). **Reconciles the sprite-era decisions** (all amended 2026-06-17): [D-043](perception.md#d-043-art-direction--visual-style-functional-warmth) ("not 3D"/Light2D scoped to overlay layers), [D-044](perception.md#d-044-art-direction--visual-hierarchy-entity--object--structure) (sprite footprint retired; sim occupancy + hierarchy survive), [D-049](perception.md#d-049-z-level-rendering-stack-8-layers) (z-stack logical model survives; sprite implementation language retired), [D-066](#d-066-dual-scale-grid--05m-simulation-1m-visual-2x-retina-factor) (coordinate model + 2×2 geometry minimum survive; sprite-footprint language legacy); [D-019] is already superseded by D-148. Phase 5 (in-world rendering) builds on this. - **Dissent:** None --- ### D-245: Nature-layer believability acceptance gate - **Date:** 2026-06-28 - **Decision:** The cascade's **nature layers** (terrain, hydrology, climate, vegetation, sub-biome — everything that produces the *natural* environment, as distinct from the built layers: settlements/roads/buildings) are **"done" only when the believability litmus passes at randomly-sampled locations across all habitable bodies and world seeds.** The deliverable of the nature half of the cascade is **"the world reads as a living, real place *anywhere*,"** not "the layers are implemented." This is the current **holy-grail objective** for the generation cascade's nature half (the counterpart to "walkable exteriors" for the built half, [D-166](scope.md#d-166)). - **"Anywhere" = held-out random sampling.** The gate draws *seeded-random* locations across the full surface of *multiple* habitable bodies and *multiple* world seeds — never curated spots. A body passes only when (essentially) all **land** probes pass; water is a separate coherence check. This is what stops "looks alive in the demo, dead everywhere else." - **The litmus is multi-scale and never-repeating** (the Netherlands principle): one **macro** identity (e.g. "delta") that is **locally non-stationary** — the sub-biome *distribution* drifts every km², and no two same-class km² are identical. Macro coherence and local non-repetition are both required. - **Automated necessary-conditions** (the screen, per probe): **(1) Coherence** — water zones render wet; vegetation is *conditioned* on moisture/water-distance/slope/aspect (not random); drainage monotonic; no impossible combos. **(2) Non-stationarity** — same-class km² measurably differ; sub-biome mix drifts; no detectable tile-period repeat. **(3) Intra-class variety** — a patch shows ≥K distinct micro-features, never 100% one material. **(4) Relief/landmark** — relief variance above a floor where the macro terrain warrants it. **(5) Climate-appropriateness** — ecology matches the climate envelope; *structured-sparse* passes, *blank* fails. - **Irreducible human gate** — the automated checks are *necessary, not sufficient*. Final sign-off is a person reading rendered layer-maps of random probes and answering the *narrate-the-natural-history* litmus. Locked by **process** (periodic sampled review + recorded sign-off), not by code. - **Strictness ramp** — starts **budgeted + advisory** (≥X% of land probes pass + human sign-off) and ratchets to **strict** (every land probe; hard push-gate block) once the enforcer's first baselines calibrate the thresholds — so all of Phase 4 is not blocked on an uncalibrated metric. - **Rationale:** "Implemented" is a false summit. The aliveness probe ([T-1083](../../.pql)) showed the nature layers can pass **every binding law** ([D-239](#d-239) §8) and still render a **dead, uniform world** (`Wetland 100%`, dry ocean, no gradient). The deliverable that actually matters is *believability*, and the only way to make a gestalt judgment lockable is a random-sample gate where automated necessary-conditions **screen** a human sufficiency **sign-off**. Naming this the acceptance gate turns "feels alive" from an aspiration into a measurable, regressable target. - **Implementation:** Enforced by **T-1083** (the believability sampling protocol — automated screen + per-layer map export for the human gate; snapshots baselines and runs in the push gate so nature cannot silently rot). Becomes the **definition-of-done** for epic **T-1079** and the needle-movers under it (**T-1080** meso non-stationarity, **T-1081** relief, **T-1082** water-bodies, **T-1084** intra-class micro-mosaic). Seeded by the probe `server/src/bin/aliveness_probe.rs`. Concrete thresholds are **TBD** — calibrated from T-1083's first baselines (a calibration Q-record may follow). - **Cross-reference:** [D-239](#d-239) (binding laws — *necessary but not sufficient*; this is the sufficiency gate **above** them), [D-227](#d-227) (derive-don't-store — the tier the non-stationary variation lives in), [D-243](#d-243) (the scale ladder the multi-scale litmus spans: macro region / meso district / micro chunk), [D-210](#d-210) (SubBiomeVariant — amended in spirit: sub-biome must be a *spatially-varying distribution*, not a single tag, [T-948](../../.pql)), [D-166](scope.md#d-166) (generation-before-player — this defines "done" for the nature half). Tickets: T-1079 (epic), T-1080/T-1081/T-1082/T-1084 (findings), T-1083 (enforcer). - **Dissent:** None --- ### D-246: Intra-class micro-habitat mosaic — sub-chunk palette modulation of the D-228 axes - **Date:** 2026-07-02 - **Decision:** Within a *single* terrain class the sub-chunk seed paints a **spatially-coherent mosaic of micro-habitats** — the third believability lever (after T-1080 meso non-stationarity and T-1081 voxel relief), and the mechanism that resolves the aliveness probe's "Wetland 100%" into clearings, copses, creeks, meadows, bogs and marshes (and the equivalent for every other class). The mosaic is a **derive-don't-store** ([D-227](#d-227)) modulation of the existing [D-228](#d-228) axes — never a new axis. Five load-bearing choices, resolved with Jeroen (2026-07-02, via the `/whats-next` refinement pass on [T-1084](../../.pql)): 1. **Mechanism — a new sub-chunk noise band + relief-conditioned selection.** A `voxel_mosaic()` octave band at ≈8–64 m wavelengths (`[64, 32, 16, 8]` m — finer than T-1081's `voxel_relief` `[1024…128]` m sub-district band, so patches read distinct *inside* a single 64 m chunk), keyed by a new body-global `SeedDomain::VoxelMosaic = 12` (append-only; the pinned-discriminant guard in `seed.rs` gains a line). The field value indexes the class palette's cumulative weights to pick a micro-habitat, and the pick is **causally conditioned, not random** — the local `voxel_relief` micro-thalweg (lowest-relief band → the palette's *wet* entry: creek/bog/marsh/brook/active-channel; drier rises → meadow/glade/clearing) plus `moisture_q`. Creeks are **lines following the relief gradient**, not blobs; blobs are smooth (low-frequency value noise), never per-tile salt-and-pepper. This **replaces** the current `scatter_vegetation()` per-voxel seed-bit scatter (`voxel.rs:1907`) — which is exactly the salt-and-pepper the mosaic exists to kill. 2. **Vocabulary front-loaded — all shipped micro-habitats mapped to D-228 axis values now.** Every micro-habitat is a `(TerrainMaterial, Vegetation, Water)` combination plus derived micro-relief/name. Where no honest existing value exists, a **new value is added to an existing axis** (never a new axis), and *all* of them land now rather than incrementally — because each value is a distinct selectable outcome in the weighted palette, so adding one later re-rolls the deterministic realization of every affected voxel. New values (amended into [D-228](#d-228)): `Vegetation::{Meadow, Deadfall, Lichen}`, `TerrainMaterial::{Hardpan, Scree}`. The table below is the **temperate/baseline register**; the biome variants (savanna gallery-copse, tundra thermokarst / lichen-heath, boreal muskeg / deadfall, …) fill the same niche spine per the Palette-key rule and are enumerated in T-1084 **before first generation** — so the table grows past these baseline rows, which is expected (and cheap — see Palette key). 3. **Palette weights live in `server/data/mosaic_constants.toml`**, runtime-loaded like `climate_constants.toml` (the T-1080 precedent). The TOML is part of the **deterministic input surface** — weight edits change realizations, so they are expected only during Q-123 calibration and frozen after. 4. **The believability metric is in scope for T-1084.** `ContrastMetrics` (`believability.rs`) gains a within-patch `micro_habitat_distinct` field (min distinct axis-triples across sampled same-class patches), and `evaluate_criteria` gains a provisional `≥ 3` criterion (K), calibrated by [Q-123](../questions/architecture.md#q-123) item 3. Mechanic and metric land together (the T-1081 precedent — it shipped both `voxel_relief` and `contrast.voxel_relief_m`). 5. **Family gating mirrors `voxel_relief`.** The **flat** families (AlluvialPlain, LavaField, BraidedDelta, DuneStrand, MeanderReach) take the **full** mosaic — vegetation/material *and* a micro-relief nudge (dune crest/slack, bog lows) and Shallow-water creek carving. The **dramatic** families (CliffCoast, FjordWall, IncisedGorge) take a **non-structural** pass — vegetation/material only, **no elevation change** (a relief nudge would warp their carved geometry) — conditioned on their own generator geometry. WaterBody takes **none**. The `<5 ms`/chunk budget ([D-239](#d-239) §10) is verified in the T-1081 believability/budget harness as part of implementation (`voxel_mosaic` adds ≈4 `value_noise` calls on top of `voxel_relief`'s 4). - **Palette key — a lookup table on `(surface-class × climate-biome × biosphere-register)`** (reworked 2026-07-02; the original surface-class-only key was biome-blind). The mosaic selects a weighted micro-habitat palette from three deterministic inputs, no stored tag: **(1) surface-class** from the family generator's base output (`terrain == Wetland|Sand|Rock|Gravel|Lava` → that class; `terrain == Soil` split by dominant vegetation → Forest vs Grassland); **(2) climate-biome** from the district's `temperature_c` / `moisture_q` / `precipitation_class`, so one Grassland surface reads *savanna* vs *temperate meadow* vs *cold steppe* vs *tundra* — [D-210](#d-210) sub-biome realized as a *spatially-varying distribution*, and the fix for the biome-blindness a surface-class-only key leaves; **(3) biosphere-register** from [D-247](content.md#d-247) — native-mirror-wild vs introduced-Earth-managed (the D-228 wild→managed override precedence), which *skins* a niche (an alien mirror clade vs an Earth crop) without changing its gameplay semantics. A fixed **niche spine** (canopy / understory / wet-hollow / pioneer / bare) is the authoring scaffold climate + register fill differently — the vocabulary is authored per-niche, not as a free biome×class cross-product. **Table size is not a performance or storage constraint** (a text lookup stays flat well past ~100k entries and is negligible beside textures/models), so author *generously and expressively* where a biome earns distinct fills — though ~100k is headroom, **not** a target; the aim is coherent believability, bounded by the niche spine and taste. The **only** binding rule is determinism: the full vocabulary is fixed **before the mosaic generates its first world** (a later addition re-rolls every affected voxel), so T-1084 completes the table before generation, never after. - **Vocab → D-228 axis table** (the load-bearing content — items sharing a triple are separated by *derived* micro-relief/shape or *derived* name, per D-228's "names are derived, never stored"; **bold** = new axis value): | Class (palette key) | Micro-habitat | TerrainMaterial | Vegetation | Water | Separated by | |---|---|---|---|---|---| | Wetland | clearing | Wetland | Barren | Dry | drier hummock (relief +) | | Wetland | copse | Wetland | Forest | Dry | — | | Wetland | creek | Wetland | Barren | Shallow | linear (micro-thalweg) | | Wetland | meadow | Wetland | **Meadow** | Dry | — | | Wetland | bog | Wetland | Scrub | Shallow | lowest relief | | Wetland | marsh | Wetland | Grass | Shallow | reed graminoid | | Forest | dense stand | Soil | Forest | Dry | base | | Forest | glade | Soil | Grass | Dry | open gap | | Forest | deadfall | Soil | **Deadfall** | Dry | — | | Forest | brook | Soil | Thicket | Shallow | linear riparian | | Forest | fern undergrowth | Soil | Thicket | Dry | renderer fern texture (cosmetic) | | Grassland | tussock | Soil | Grass | Dry | base | | Grassland | scrub island | Soil | Scrub | Dry | — | | Grassland | wildflower | Soil | **Meadow** | Dry | shares Meadow | | Grassland | dry wash | Gravel | Barren | Dry→Shallow | linear ephemeral channel | | Sand | dune crest | Sand | Barren | Dry | high relief | | Sand | slack | Sand | Grass | Dry | interdune low (relief −) | | Sand | hardpan | **Hardpan** | Barren | Dry | — | | Sand | oasis | Sand | Forest | Shallow | — | | Sand | scrub clump | Sand | Scrub | Dry | — | | Rock | outcrop | Rock | Barren | Dry | protruding (relief +) | | Rock | scree | **Scree** | Barren | Dry | open slope | | Rock | talus | **Scree** | Barren | Dry | derived name (cliff foot) | | Rock | ledge | Rock | Barren | Dry | flat step (relief) | | Rock | lichen | Rock | **Lichen** | Dry | — | | Gravel | bar | Gravel | Barren | Dry | base | | Gravel | active channel | Gravel | Barren | Shallow/Deep | flowing water | | Gravel | vegetated island | Gravel | Scrub | Dry | stabilized | | Lava | fresh sheet | Lava | Barren | Dry | base | | Lava | weathered crust | Lava | **Lichen** | Dry | shares Lichen | | Lava | tube collapse | Lava | Barren | Dry | depression (relief −); reconcile with existing `tube_depression`, `voxel.rs:710` | | Lava | pioneer scrub | Lava | Scrub | Dry | — | - **New D-228 axis values** (append-only discriminants, [D-010](#d-010)): `Vegetation::Meadow = 7` (forb-rich flowering herbaceous — the wetland *meadow* and grassland *wildflower*), `Vegetation::Deadfall = 8` (dead woody debris — obstruction + fire fuel, no canopy concealment), `Vegetation::Lichen = 9` (crustose/pioneer biological crust — the rock *lichen* and lava *weathered crust*), `TerrainMaterial::Hardpan = 6` (compacted flat crust — hard fast footing, does not slump like sand), `TerrainMaterial::Scree = 7` (loose angular rock debris — unstable footing; *scree*/*talus* differ only by derived name). `Water` is unchanged (Dry/Shallow/Deep suffice). Adding these lights up exhaustive `match` arms across `voxel.rs`, `from_vegetation_class`, the renderer, and any serializer — an intentional compile-time checklist. - **Rationale:** T-1084's finding was that a class can pass every binding law ([D-239](#d-239) §8) and still render as one uniform material ("Wetland 100%"); [D-245](#d-245) makes *believability* the gate, and intra-class variety is one of its five necessary conditions. Reusing the T-1081 `detail_scatter` machinery (same enveloped-fBm core, one more octave band, one more `SeedDomain`) keeps the mechanism inside the derive-don't-store tier with no new architecture. Front-loading the whole vocabulary — rather than growing it as needs appear — is forced by determinism: the palette is a *weighted selection* the seed indexes, so every added value shifts the realization of already-generated worlds. Expressing micro-habitats as combinations of orthogonal axes (plus derived name/relief) rather than a flat `MicroHabitat` enum is the same anti-combinatorial-explosion logic as D-228 itself. - **Implementation:** [T-1084](../../.pql) (story under epic [T-1079](../../.pql)). Touchpoints: `detail_scatter.rs` (`voxel_mosaic()` + the `[64,32,16,8]` m band); `seed.rs` (`SeedDomain::VoxelMosaic = 12` + the pinned-discriminant guard); `voxel.rs` (new per-family-gated mosaic pass after the relief block ~`:495`; **replaces** `scatter_vegetation()` ~`:1907`; new axis-value `match` arms); `believability.rs` (`micro_habitat_distinct` + criterion); new `server/data/mosaic_constants.toml`. Budget verified in the T-1081 harness. - **Amendment (T-1084, 2026-07-05 — v1 implementation scope, PR #172 H2/C4):** the first implementation lands a **narrower pass than item 5 specifies**, recorded here so the record and code agree (the [D-239](#d-239) T-1080/81/82 postscript pattern): - **Only the Soil-derived vegetated/wet classes take the mosaic** in v1 — Wetland, Forest, Grassland (the `apply` gate in `voxel.rs`). The **material-driven** families (Sand, Rock, Gravel, Lava) are gated `_ => false`, so the material-variant rows of the table above — dune crest/**hardpan**, **scree**/talus, dry-wash Gravel, and every Lava row — are **authored but not yet constructed on any reachable path**, and the two new `TerrainMaterial::{Hardpan, Scree}` values are consequently unreachable in v1. A material change on those families must respect the family's own lithology and carved geometry (shape-aware §8 reconciliation), so they are the deferred follow-up. Item 5's promise that the flat families take the *full* (material + micro-relief) mosaic is **deferred, not delivered** at v1. - **No elevation change in v1.** The item-5 micro-relief nudge (dune crest/slack, bog lows) and the non-structural dramatic-family pass are both deferred — v1 modulates **vegetation + water only**, never `elevation_m`. The D-239 §8 **lithology** law therefore holds trivially: material and shape are left exactly as the family generator set them. - **For the three classes it does touch, the mosaic OWNS the §8 climate→vegetation law** (C4), not merely stays inside it. The **no-skip** half holds — no palette emits `Barren` in a Forest / Wetland / Grassland zone. The **climate-appropriateness** half is enforced by a **unified `!Barren` apply-gate spanning all three classes** (C1 + N1): a climatically-barren district (frozen `< -50 °C` = surface ice / geology per D-239 §2, or hyper-arid `moisture_q < 5` — both resolved to `VegetationClass::Barren` upstream by `derive_vegetation`) is **skipped**, so the mosaic never re-grants cover the climate withheld. The guard must span **Wetland as well as Grassland**: `derive_morphology_zone` has no temperature gate, so a frozen + wet + flat district still yields Wetland material and would otherwise be painted with wetland copse/bog — the same C1 symptom via the material path (a frozen ice world must read as barren ground, not as vegetated tundra *or* marsh — [D-245](#d-245) §5). Forest is `Barren`-free by construction (`SurfaceClass::Forest` requires `dominant_veg == Forest`), so the guard is a no-op there. The trade: the family generators' *guaranteed* riparian Thicket band is softened to a **probabilistic** wet-habitat clustered near `has_active_channel` chunks (T-1040-gated) — acceptable at v1. - **Cross-reference:** [D-228](#d-228) (the axes this modulates — amended 2026-07-02 to add the five values), [D-227](#d-227) (derive-don't-store — the tier the mosaic lives in), [D-245](#d-245) (believability gate — this is condition 3, intra-class variety), [D-239](#d-239) §8 (binding laws — the mosaic stays inside them) / §10 (sub-chunk budget), [D-243](#d-243) §2 (the detail-scatter tier T-1081 and this extend), [D-247](content.md#d-247) (native-mirror vs introduced-Earth **biosphere register** — palette-key input 3; the mosaic is chirality-blind, but its register labels each niche native or introduced), [D-210](#d-210) / [T-948](../../.pql) (SubBiomeVariant — now **palette-key input 2**: the macro biome realized as the finer intra-class distribution, not a single tag). Tickets: T-1084 (this), T-1079 (epic), T-1081 (relief — a conditioning input + the reused machinery), T-1083 (enforcer — measures the mosaic), [Q-123](../questions/architecture.md#q-123) item 3 (calibrates K). - **Dissent:** None --- ### D-248: 3D locomotion presentation — per-leg constant-velocity interpolation keyed to the stance throttle - **Decision:** The 3D client rig turns discrete server tile steps into motion with **per-leg constant velocity**: on each confirmed step, leg speed = `distance / stance interval`, the interval read live from `InputMapper.MOVE_INTERVAL_MS` (Sprint 200 / Walk 400 / Careful 600 / Crouch 800 ms — never copied). Multi-tile deltas (latest-wins snapshots drop intermediate steps) close under a **3× catch-up clamp**; ≥ 2.5 m (5 subtiles) snaps all channels (position, yaw, camera, animation hard-cut). Diagonal legs run 1.41× (no √2 on the wire, [D-053](scope.md#d-053)) — accepted as sim truth so held diagonals arrive on time. Gait clips are **cadence-synced** (`speed_scale = clamp(leg_speed / NATIVE_MPS[clip], 0.6–1.8)` — constant per leg, killing foot-slide); Careful uses `Walk_Formal`; idle enters after 0.18 s hysteresis (snapshot-jitter guard). Exactly **one smoothing layer per channel**: position constant-velocity (never eased), yaw eased under per-stance deg/s budgets, camera exponential (rate 6.0 — converges to a constant offset against constant velocity). Purchased UAL tiers (or `ual_extended`) wire via **explicit AnimationLibrary names + `"lib/Clip"` addressing** (the imported default library name is `""` — a naive second copy collides). Stepped player camera yaw remains **out** (not canon; the T-key tilt cycle stays a dev affordance, [D-148](#d-148-30-low-angle-camera-with-45-map-rotation--supersedes-d-019)/D-158). - **Rationale:** resolves the D-054 ↔ D-053 cadence conflict: a fixed 100–150 ms tween dashes-then-stands at Walk/Careful/Crouch cadences, and exponential position lerp (the 2D renderer's model) produces a per-step velocity sawtooth foot cadence can never sync to. Constant-velocity legs sized to the throttle make held-key motion seamless and make cadence sync possible at all. **Annotates [D-054](#d-054):** its "client-side Tween interpolation (100–150 ms)" sentence is scoped to the 2D renderer henceforth. - **Implementation:** T-1088 (`client/scripts/sandbox/locomotion_rig.gd`, `locomotion_anim.gd`, `sandbox_constants.gd` — constants are initial values pending the live tuning pass). Strictly interpolate-only: a silently rejected move needs zero client handling by construction (Q-020 annotation) — facing updates, position doesn't, gait keys off render velocity ("bump-to-turn"). - **Cross-reference:** [D-054](#d-054), [D-053](scope.md#d-053), [D-055](#d-055), [D-066](#d-066-dual-scale-grid--05m-simulation-1m-visual-2x-retina-factor), [D-149](#d-149-3d-characters-rendered-live-in-scene--not-pre-rendered-sprites), [D-249](#d-249-3d-facing-presentation-authority--server-feet-client-eyes). Tickets: T-1088. - **Dissent:** None ### D-249: 3D facing presentation authority — server feet, client eyes - **Decision:** Which source rotates the 3D character model: **moving** (incl. the idle-hysteresis window) → the snapshot `player_facing` octant (the server overwrites Facing from the move delta in the same tick — it *is* the motion direction); **idle** → the client-local aim octant, i.e. **the same snapped octant that rides the `SetFacing` wire** ([D-054](#d-054): only octants cross the wire, so mouse-responsive idle facing is client-local by construction, and the model never shows an octant the server wasn't told). Facing freezes under input suppression (`dialogue_active`/`free_camera_mode`). The idle source is an **injected provider** — NPCs leave it unset and collapse to pure-wire, one code path. The **verified octant→yaw table** is `yaw = π/2 − θ` (South 0°, East +90°, West −90°, North 180°), applied on ModelRoot in WorldRoot-local space so octant→yaw composes with the D-148 45° map rotation **exactly once** (confirms compositor-api-spec §2). **Trap:** the rig must never call `CharacterVisual.set_facing()` — its internal table is E/W-mirrored relative to this convention (`character_visual.gd:184-193`). - **Rationale:** single-source alternatives are strictly worse — pure-snapshot idle facing is ~250–300 ms of laggy 45° pops (the 2D client already draws its indicator from client-local aim, `entity_renderer.gd:186-193`); pure-client facing breaks server-authoritative display while moving. The residual artifact is bounded: idle, the model leads the server's vision cone by ≤ 1 RTT, and the disagreement is exactly the in-flight SetFacing. - **Cross-reference:** [D-054](#d-054), [D-151](#d-151), [D-248](#d-248-3d-locomotion-presentation--per-leg-constant-velocity-interpolation-keyed-to-the-stance-throttle), Q-084 (presentation half settled here; the walk-vs-aim *animation* split stays open — the purchased 8-direction walk/jog/crouch clip sets now make a direction-matched-clip solution tractable). Tickets: T-1088. - **Dissent:** None --- ### D-252: Facing is view-only — movement no longer writes Facing; NPC gaze is intent - **Decision:** The `Facing` component (and the wire's `player_facing`/entity facing octants) is the **view direction only** — the vision-cone heading. Accepted moves **no longer overwrite Facing** (`facing_from_delta` is removed from the movement path): the player's Facing changes only via explicit `SetFacing` (the mouse octant, D-054's change-gated send). **Movement direction is not a wire concern** — the client derives body heading from position deltas (the 3D rig's per-leg velocity, [D-248](#d-248-3d-locomotion-presentation--per-leg-constant-velocity-interpolation-keyed-to-the-stance-throttle)). **NPC gaze becomes intent:** the NPC path-follow system sets Facing to the step direction explicitly, relocating look-where-you-walk into AI intent — later behaviors (a patrolling guard glancing sideways, an NPC backing away while watching the player) become Facing writes by the AI, no special cases. *Implementation correction (2026-07-06): the old coupling was player-only — NPCs never received Facing from movement; their cones sat at spawn direction while walking. Path-follow gaze is therefore a strict improvement, not preservation.* - **Motivation (play evidence, T-1088 live session 2026-07-06):** during client path-follows the vision cone flapped to path-forward on every accepted step and stuck there until the mouse crossed an octant boundary — the movement overwrite fought the explicit aim, and the best client-side mitigation (post-step re-assert, ~2-tick delay) still left a ~100 ms flap per step because movement wins within a tick. Splitting the semantics is the correct fix; the walk-one-way-look-another model this enables is exactly the split [Q-084] parked. - **Consequences:** "bump-to-turn" (blocked moves updated facing) is retired — correct under a mouse-view model. The client's post-step `reassert_facing` mitigation is removed. [D-249](#d-249-3d-facing-presentation-authority--server-feet-client-eyes) is **amended by this record**: while moving, the body yaw source is the leg-velocity direction (wire facing while moving is now the *view*, which must never rotate the body); idle behavior is unchanged; the layered head/torso look-at ([T-1088] follow-facing) may run during any movement, not only path-follows. The 2D renderer's player sprite direction follows the view octant in all states (was: move direction while moving) — accepted drift on D-166-frozen code. **Amends [D-054](#d-054-tile-based-movement-with-same-tile-occupancy)** (its facing sentence: octant-only wire + explicit sends stand; the movement overwrite is struck). - **Cross-reference:** D-054, D-248, D-249, D-015 (vision cone), Q-084 (walk-vs-aim split — resolved by this record for semantics; camera rotation option stays parked). Tickets: T-1093 (implementation), T-1088. - **Raised by:** Jeroen ("we need to split movement direction and viewing direction in the server protocol", live session 2026-07-06, after the T-1093 mitigation demonstrated the residual flap) with Claude (mitigation evidence, NPC-intent relocation, consequence audit). - **Dissent:** None --- ### D-253: Region transient state model — seasonal/tidal/weather/snow phase functions (resolves Q-105) - **Date:** 2026-07-08 - **Resolves:** [Q-105](../questions/architecture.md#q-105). - **Decision:** The region ([D-243](#d-243)) is the single scale at which **transient (clock-bound) surface state** is resolved — "computed once per region per phase, inherited by every district and tile inside" ([D-243](#d-243) §3, the cheap-dynamism source). This record pins that model: **what** transient state a region carries, **how** its clock advances and recomputes, **how** tiles inherit it, and its **determinism** — but explicitly **not** the gameplay/rendering that later consumes it. **(a) The transient bundle — four phase terms + their derived surface scalars.** A region carries a memoized `RegionPhase` (proposed carrier name) built from **four clock terms, one per natural clock rate** (fastest → slowest), each a pure function of the in-game clock ([D-031](#d-031)) and the region's static params: - **Diurnal** — day/night, from the [D-031](#d-031) day-phase clock; amplitude = the atmosphere heat-retention already defined in [D-239](#d-239) §2 (thick air → small swing). *This term is the one [D-239](#d-239) §2 already owns for temperature; D-253 folds it into the same region phase model as the other three, rather than leaving it a separate mechanism.* - **Tidal** — from the lunar/day clock; amplitude from the body's satellite config; **absent (zero) when the body has no moon**. Drives the tidal component of water-height. - **Weather** — a coarse, seeded, deterministic precipitation/condition term (a bounded pseudo-sequence keyed on `(region, weather-bucket)` — **not** a simulated advecting weather system; deliberately minimal to stay "nearly free"). Drives puddles + general conditions. - **Seasonal** — from the year clock, **phased continuously by latitude** (amplitude → 0 at the equator and sign inverting across it — a continuous function of latitude, not a binary hemisphere flag, so the equatorial seam never steps). The slow temperature term + the seasonal component of water-height + the snow/crop drivers. - From these terms the region derives its **transient surface scalars** (region-level, not per-tile): **temperature(time)** = the T-1078 / [D-240](#d-240) static baseline **+** seasonal offset **+** diurnal offset (this makes [D-239](#d-239) §2's "temperature(time)" concrete — the static baseline T-1078 ships is the mean-phase value); **water-height(time)** = mean water level **+** seasonal term **+** tidal term; **snow/ice depth** = `f(temperature(time), moisture)` feeding the **existing T-1030 scattered band** ([D-239](#d-239) §3) — D-253 supplies only the *depth over time*, T-1030 owns *where* the band scatters; **weather intensity/category**; and the **crop-cycle phase** (sown → growing → ripe → harvested → fallow) = `f(seasonal)`, the cadence clock for [D-228](#d-228)'s managed-`Vegetation` farmland override (D-253 owns the *cadence*; the settlement/economic layer owns crop *type* and farmland *placement*). **(b) Phase-stepping — recompute on bucket rollover, never per tick, never integrated.** Each term declares a **clock-bucket size** (diurnal ≈ day-phase; tidal ≈ day-phase; weather ≈ a multi-phase block; seasonal ≈ a season step). `RegionPhase` is **memoized with the clock-buckets it was computed for**; a query recomputes a term **only when its bucket rolls over** — the "nearly free" requirement, and [D-226](#d-226)'s dynamic-state path. Crucially, every scalar is evaluated from the **absolute clock value** (`state = f(clock)`), **never integrated step-by-step** (`state += Δ`): absolute evaluation is drift-free, reproducible, and hands [D-226](#d-226) its **frozen-phase inspection** for free (freeze the clock → the whole world's transient state is stable and re-derivable). Default bucket sizes are source-canonical + tunable (the `climate_constants.toml` precedent), provisional pending calibration. **(c) Inheritance — region computes the scalar, the tile realizes it locally, edge-fuzzed.** Districts and tiles **never compute their own phase** — they read the region scalars, blended across region neighbours by the **same [D-243](#d-243) §4 edge-fuzz** the static climate baseline uses (warp-perturbed bilinear across region centres), so a weather front / seasonal gradient / thaw line never steps on the ~205 km grid (and the equatorial seasonal seam is continuous by construction, per (a)). The **per-tile realization is a local comparison**, not an inherited value: a tile is **flooded** iff `region-water-height(time) > tile.elevation`; **snowed** to a depth capped by the region snow-depth, scattered by the T-1030 band and gated on its own material/moisture; **puddled** iff the weather term is wet and the tile sits in a micro-relief low (the T-1081 `voxel_relief` hollows) that diurnal evaporation has not burned off; its farmland shows the crop stage the region phase dictates. So floodplain / tidal-flat / seasonal-river **emerge** ([D-228](#d-228)) — the static world stays static; only the region scalars carry the clock. This directly discharges T-1082's deferred TidalFlat wet/dry alternation (`Shallow`/`Dry` = tidal water-height vs local elevation). **(d) Determinism ([D-010](#d-010)).** Every term and scalar is **integer fixed-point** (phase positions as basis-points of their cycle; depths/heights in integer mm/cm; temperature offsets on the existing integer scale) and a **seeded pure function** of `(absolute clock, region params, body seed)`. **No per-tick float accumulation exists anywhere in the model** — see (b). Weather's pseudo-sequence is a seeded hash of `(region, weather-bucket)`, so it replays identically. Frozen clock → byte-identical state on every platform and every reload; this is what lets the transient overlay live in the [D-227](#d-227) derive-don't-store tier (the memo is **not persisted** — a save records only the clock and the state re-derives). **(e) Scope boundary — this is the STATE MODEL, not its consumers.** D-253 defines *derivable transient state and the functions that produce it* — Phase-4-appropriate, viewable as an Atlas layer like every other cascade output, and completing the forward-contracts left open by T-1030 / T-1078 / T-1082. It does **NOT** build: weather/flood/snow **gameplay** (movement, perception, passability effects — [D-239](#d-239) §9 names the passability hook but its wiring is later), the **farming/crop simulation**, snow/water **rendering** (Phase 5), or any **verb** that reads this state. Those are later phases and inherit a fixed contract here. Adding a fifth term or a consumer is a future amendment, not a reinterpretation of this record. - **Rationale:** The model is cheap *because* the four transient terms line up with the four natural clock rates and reuse the existing temperature primitive — temperature(time) and water-height(time) are just a static baseline plus the same seasonal/diurnal/tidal offsets, so **one clock model drives temperature, flooding, tides, snow, puddles, weather, and crops** with no per-system simulation. The single load-bearing choice — **absolute-clock evaluation rather than step integration** — satisfies three requirements at once: determinism ([D-010](#d-010), no drift), cheapness (recompute only on bucket rollover, and per-region not per-tile → O(regions)), and inspectability ([D-226](#d-226) frozen phase). Computing at the region and realizing per-tile by comparison is what keeps the dynamism O(regions) while still producing per-tile flood/snow/puddle/crop extent — the "computed once, inherited" contract [D-243](#d-243) §3 promised. - **Raised by:** Jeroen (Q-105 model constraints — clock + hemisphere bound, moon-gated tides, computed-once-per-region, "nearly free", 2026-05-25) + Tyre (T-1057 triage — the four-term phase-function model, the absolute-clock / memoized-by-bucket / edge-fuzzed-inheritance pins, 2026-07-08). **Authored as a proposed design for PR review** — the flagged judgment calls (below) are open for the lead/user to adjust. - **Design choices flagged for review:** (1) **diurnal promoted to a first-class term** — Q-105 enumerated three terms (seasonal/tidal/weather); D-253 adds diurnal as the fourth, since [D-239](#d-239) §2 already owns it for temperature and T-1030's dawn-frost already depends on it (a coherence unification, but it extends Q-105's framing). (2) **weather kept deliberately minimal** (a seeded coarse precipitation/condition term, no advecting fronts) — the depth is a taste call. (3) **recompute-bucket granularities** (diurnal/tidal ≈ day-phase, weather ≈ multi-phase, seasonal ≈ season-step) are provisional/tunable, not hard-pinned. (4) **memoized-not-persisted** reconciles D-228's "region property computed once per phase" with [D-227](#d-227) derive-don't-store (the region *caches* the bundle keyed by bucket; it is never durable state). (5) **"phased by hemisphere" read as continuous-in-latitude** (amplitude→0 at equator) rather than a north/south sign flag, so the seam edge-fuzzes cleanly. - **Implementation:** Phase 4+ (epic [T-750](../../.pql)), the forward contract from T-1030 (transient freeze/snow depth), T-1078 (region climate stack — shipped the static baseline + region-clock *structure*; D-253 adds the transient terms), and T-1082 (tidal-flat wet/dry). Proposed home: a `RegionPhase` carrier alongside `RegionProfile` in `BodyWorldState` ([D-203](#d-203) / [D-239](#d-239) §10 region storage), memoized by clock-bucket; proposed derivations `derive_region_phase(clock, region)` + per-tile realization helpers (flood / snow / puddle / crop) — names non-binding, the impl ticket fixes them. Constants in a source-canonical TOML (`climate_constants.toml` precedent). A new implementation ticket lands under T-750. - **Cross-reference:** [D-243](#d-243) (region = climate/weather/season lockdown scale; §3 computed-once-inherited; §4 edge-fuzz — the inheritance contract), [D-228](#d-228) (the seasonal water-height / snow-cover overlay / crop cycle this model drives — its open water-height sub-question resolved here), [D-239](#d-239) §2 (temperature(time) primitive — made concrete) / §3 (T-1030 scattered freeze-snow — given its clock) / §9 (seasonal passability — a later consumer), [D-240](#d-240) (`planet_class` temperature envelope — the baseline the seasonal/diurnal terms offset), [D-226](#d-226) (dynamic-state frozen-phase inspection), [D-031](#d-031) (game clock / day phases — the clock source), [D-010](#d-010) (determinism), [D-203](#d-203) (BodyWorldState region cache — the memo home), [D-227](#d-227) (derive-don't-store — the memo is not persisted). Tickets: [T-1057](../../.pql) (this triage), T-1030 / T-1078 / T-1082 (the discharged forward-contracts), T-1081 (`voxel_relief` micro-lows — puddle/flood sites). - **Dissent:** None --- ### D-254: Standalone Atlas companion app — `make atlas`, dual-connection reader - **Date:** 2026-07-17 - **Decision:** The implant Atlas (D-169/D-170's `implant/map` app) ships as a **second, independent Godot entry point** — `client/scenes/atlas_standalone.tscn`, launched via a new `make atlas` target — that boots the SAME implant scene tree used in-game but skips the player entirely: no character, no `main.tscn`, no gameplay HUD. It connects to the simulation server either by **attaching** to an already-running game (inheriting that world's state read-only) or by **spawning** its own server process (offering seed selection now; save selection is a recorded, unbuilt hook — saves are Phase 5+). The server enforces read-only **server-side** via a distinct `ConnectionRole` on the handshake (`Player | Reader`, Reader spawns no character and receives no `ObserverSnapshot` at all) — the Atlas app itself gains no new client capability, it is the existing Atlas UI pointed at a bridge connection the server structurally refuses inputs from. A future market-trading widening (§6) adds a `TradingReader` role as a strict superset of `Reader` (never a replacement) rather than inventing a second connection type. **(1) CONNECTION MODEL — attach vs. spawn, discovery.** **Today's reality, confirmed in code:** `main.rs` binds a `TcpListener`, prints `LISTENING:{port}`, then calls `listener.accept()` **exactly once** — blocking, no loop. A second TCP client completes its TCP-level handshake (kernel backlog accepts it) but never gets an application-level accept — it hangs forever waiting for `HandshakeMessage`. **Not refused, not replaced — silently starved.** This is the actual failure mode the reader connection must design against; zero multi-connection plumbing exists anywhere in `server/src/bridge/` today, confirming the ticket's own framing ("almost certainly single-connection"). Default port `9876` (`sim_bridge.gd:26`, matches `main.rs` fallback), overridable via positional addr / `--port` / (client-side) `SR_PORT`. `SR_PORT` is already the env var two Godot scripts read today for "which port do I dial" (`visual_capture.gd:99`, `locomotion_sandbox.gd:67`) — the discovery mechanism reuses that existing convention rather than inventing a third (`SR_ADDR` is a server-side bind override and is not load-bearing for either connection mode below). - **Attach-mode discovery:** fixed default port 9876 + `SR_PORT` override — the same two-tier scheme the game client already uses to find its own server. Raw TCP connect with a ~500ms timeout (localhost, not WAN — no reason to wait longer). `ECONNREFUSED` is a real, unambiguous signal ("no server listening") and falls through to spawn-mode. A connection succeeding does not yet mean attach is *safe* — that gate is the Reader-role handshake in §2, not the TCP connect itself. - **Spawn-mode lifecycle:** reuse the `tests/run-visual` precedent exactly — `--port 0` (OS-assigned), parse `LISTENING:{port}` from stdout — but **without `--test-mode`**: the companion needs the real `systems.db` world, not Gauntlet test fixtures. Ownership: the companion app owns the child process it spawns, the same pattern `server_process.gd` already implements (`OS.create_process`/`OS.kill`/`NOTIFICATION_PREDELETE` safety net) — reused directly, not reimplemented. World seed is passed via the companion's own `StartupMessage.world_seed` post-handshake (not a `--seed` CLI flag) — this keeps the save/load seam (§5) as the single source of truth for how a spawned world gets populated, rather than splitting seed-selection across a CLI flag and a wire message. - **Mode selection UX:** auto-attach-else-spawn — try attach for ~500ms, fall through silently to spawn on refusal. Zero friction for the common case ("inspect the world I already have running"), and the fallback is never wrong (spawn always works). An explicit Attach/Spawn chooser is deferred — only justified if reader-mode failures turn out confusing enough in practice that users need visibility into *why* attach didn't happen; not assumed necessary at design time. **(2) READER CONNECTION CLASS — handshake variant, server-side enforcement.** Enforcement is **server-side at the protocol layer**, never client politeness — a hostile or buggy companion client is exactly D-010's adversarial case, and the read-only guarantee has to hold against that, not just against a well-behaved reference client. The one seam that matters: before the server unconditionally spawns a `PlayerCharacter` (`main.rs`, today unconditional on every accepted connection). **Handshake extension:** add `role: ConnectionRole` to `StartupMessage` — enum `Player | Reader` (widened by §6 to `Player | Reader | TradingReader`) — with `#[serde(default = "ConnectionRole::player")]` for back-compat, rather than a separate pre-startup negotiation message. This follows D-192's existing "no lockstep negotiation" precedent (protocol_version field dropped for the same reason): role is **data on the existing message**, not a new protocol gate. Critical determinism guard: a Reader's `world_seed` field is **ignored server-side and never re-seeds `SimRng`** — a second StartupMessage touching `SimRng` after tick 0 would break determinism for whatever Player is already in session (spawn-mode readers get their seed from the world THEY spawned, at genuine tick 0; attach-mode readers must never be able to perturb an already-running world's RNG state via their own handshake). **Server enforcement — structural, not filtered:** Reader role skips the character-spawn path entirely **and receives no `ObserverSnapshot` at all** — not a stripped/redacted one, none. This is the load-bearing point: `ObserverSnapshot` is a per-character observation record (facing, inventory, visible_tiles are all meaningless without a character), so forwarding the Player's own snapshot to a Reader — even filtered — would be a direct D-010 boundary violation (a second observer silently granted the first observer's fog-cleared view). What a Reader *can* legitimately receive is proven by the existing handler signatures: `handle_star_map_request`, `handle_city_names_request`, `handle_atlas_request` (and this record's new browse-request handlers, §4) all take **no observer/character/query parameter whatsoever** — just `body_id`/`world_seed`/`path` — which is the independent proof that this data was already install-static/world-public before D-254, not a new carve-out invented for readers. | Message | Player | Reader | |---|---|---| | `Vec` (inputs) | yes | **no** | | `ObserverSnapshot` (outbound) | yes | **no — not even filtered** | | Atlas/StarMap/CityNames/Browse request+response | yes | yes | | `HandshakeMessage` | yes | yes | **Violation handling:** a Reader sending `Vec` is syntactically valid (the existing `decode_inbound` demux parses it fine) but role-disallowed. Log + drop on first offense, mirroring the existing recoverable `DeserializationWithDump` pattern; escalate to disconnect only on repeated violations — a natural fit for the already-flagged N-consecutive-errors handling in the bridge module, made per-connection once multiple connections exist. **Multi-connection architecture — scoped honestly as 0-1 Player + 0-N Readers**, explicitly NOT general N-player (that is D-009's separate, larger, and currently out-of-scope ambition — this record does not reopen it). `BridgeResource` (today a single `Box`) becomes a collection; the single blocking `accept()` becomes a non-blocking accept-loop polled per-tick, so a Reader connecting mid-session never stalls the Player. The inbound drain loop routes `Vec` only from the Player-role connection; atlas/starmap/citynames/browse requests are accepted from any connection, but responses need a connection-id tag (today's response buffers have no "whose request was this" notion, because there has only ever been one connection). Outbound `ObserverSnapshot` sends target the Player connection only — this is a structural enforcement of the boundary above, not merely a convention that could be gotten wrong by a future edit. **Back-pressure/lifecycle — the sharp existing edge:** today `BridgeError::Disconnected` sets `ServerRunning = false` and kills the **whole server process**, because currently one connection's disconnect *is* the session ending. That behavior must NOT fire on a Reader's disconnect once roles exist — only a Player disconnect should flip `ServerRunning`; a companion app closing its window must never kill the game it's attached to. Determinism holds by construction as long as reader frames never reach the InputQueue/SimRng path (guaranteed by the enforcement above, not by a separate check). Recommend a lower per-reader inbound frame cap (e.g. 8/tick vs. the existing Player cap of 64/tick) — a reader has no legitimate reason to send that volume of requests per tick, and the cap is cheap insurance against a runaway/misbehaving companion client. **(3) APP SHELL — how `make atlas` launches the Atlas standalone.** **Decision: a dedicated entry scene, not a feature flag on `main.tscn`.** `client/scenes/atlas_standalone.tscn` is a bare root (`Node2D` or `Control`) with a script (`atlas_standalone.gd`) following the exact boot shape `client/tests/visual_capture.gd` already establishes for minimal Godot entry points (`_init() -> _run.call_deferred()`, connect, wait for handshake, open UI) — except `atlas_standalone.gd` is a real scene script (`extends Node2D`, normal `_ready()`), not a `SceneTree`-extending test harness; the `SceneTree` pattern is for offscreen capture tooling, the standalone app needs a visible window. Why not a flag on `main.tscn`/`main.gd`: `main.gd` is saturated with player-only wiring that a "headless" branch would have to route around at every touch point, not bypass cleanly — 18 `@onready` gameplay HUD nodes (minimap, stance indicator, inventory grid, dialogue box, interaction list, gauntlet HUD…), a `SnapshotEventRouter` with a dozen player-centric `register_always`/`register` handlers (`update_zone`, `play_recognition_chimes`, `consume_dialogue`…), free-camera WASD panning tied to `GameState.free_camera_mode`, and a `_process()` loop whose entire second half is input-queue flushing (`InputMapper.flush_queue()` → `SimBridge.send_input()`). None of that exists to serve the Atlas — it exists to serve a playing character, which a reader connection never has (and, per §2, structurally cannot send inputs for even if it tried). A flag would mean auditing and branching every one of those systems to no-op correctly; a dedicated scene means writing on the order of 100 lines that do only what the Atlas needs, with zero risk of a reader session accidentally exercising player-only code paths (interaction prompts, dialogue, bug report capture) that assume a character exists. **Boot sequence** (`atlas_standalone.gd`, modeled directly on `visual_capture.gd`'s live-mode wait blocks): 1. `_ready()`: run §1's auto-attach-else-spawn discovery (try `SR_PORT`-or-default-9876 connect, ~500ms timeout; on refusal, spawn a server child via the `server_process.gd` pattern with `--port 0` and parse `LISTENING:{port}`). Configure `SimBridge` accordingly (`server_path` set for spawn, unset + resolved attach port for attach). 2. Call `SimBridge.connect_to_sim()` using the **Reader handshake variant** (§2's `role: ConnectionRole = Reader` on `StartupMessage`), not the character-startup path `main.gd` uses. This is the one place `atlas_standalone.gd`'s connect call diverges from `main.gd`'s. 3. Poll `SimBridge.state` until `CONNECTED` — same `ConnectionState` enum, same polling shape as `visual_capture.gd`'s live-mode wait, minus the fixed-frame-count settle (a real window can just `await` the signal instead of budgeting frames for a screenshot). 4. On connect: `HudGroups.open_app("implant/map")`. There is no gameplay group ever registered in this scene, so D-170's gameplay/implant mutual-exclusivity degenerates harmlessly to "implant is always the sole active exclusive group" — no `HudGroups` code changes needed; the invariant it enforces (only one of gameplay/implant visible) is trivially satisfied when gameplay never registers anything. 5. `ImplantRegistry.instantiate_all(self)` — the same call `hud.gd._ready()` makes in the normal game — populates every installed implant app (Atlas + Economics both come along for free; Economics degrades gracefully since it's reachable but not the entry point, and read-only holds for it too automatically, since it rides the same Reader connection). 6. The Atlas's own `KEY_M`/`KEY_ESCAPE` handling (`atlas_app.gd`) currently calls `HudGroups.close_app()` on M/Escape from the top-level "reach" screen, which would leave the standalone window showing a blank Control with nothing to fall back to (there is no gameplay layer). Two options, left for the implementation ticket to pick: (a) `atlas_standalone.gd` intercepts the close and either quits the app or re-opens `implant/map` instead of demoting to a nonexistent gameplay layer, or (b) `atlas_app.gd` gains a `standalone_mode` flag that no-ops the close-to-gameplay branch. **(a)** is recommended — it does not touch `atlas_app.gd` at all, keeping the in-game and standalone Atlas byte-identical. **Automatic extension + lockout flag (added mid-implementation per Jeroen, 2026-07-17):** the companion is a generic implant HOST, not an Atlas launcher — every implant app registered with `ImplantRegistry` is automatically available in the standalone shell, so future Atlas screens and future implant apps (the `implant/browser` app of §4, wiki/GTTR, economics dashboards, whatever comes) extend the external app with **zero companion-side work**. The lockout is the exception, not the rule: the implant app manifest (the `app.tres` resource each app already carries) gains `available_in_companion: bool = true` — an **opt-out** flag set `false` only for apps that structurally cannot work without a playing character/gameplay context. The standalone shell consults the flag at `instantiate_all`/app-open time (flagged-off apps are not instantiated and not offered in any app-switching surface); the **in-game implant ignores the flag entirely** — it exists only for the companion host. Apps needing finer granularity may gate individual screens on a standalone-mode query, but v1 needs only the app-level flag (both current apps — Atlas and Economics — are read-only-safe and stay `true`). Naming note: Jeroen's suggested name was `availableInAtlasApp`; recorded here as `available_in_companion` (snake_case per GDScript convention, and "companion" avoids colliding with `client/ui/implant/apps/atlas/` — the Atlas is itself one of the hosted apps, not the host). **Window title/branding:** `atlas_standalone.tscn` sets its own window title via `DisplayServer.window_set_title()` in `_ready()` (e.g. "The Settled Reach — Atlas"), since `project.godot`'s shared `config/name` would otherwise make the standalone window read identically to the main game window in the taskbar/alt-tab — a second-monitor companion needs to be visually distinguishable at a glance. This is the only project-level Godot config touched; no `run/main_scene` override, no export preset changes in this ticket. **Dev launch (un-exported):** `make atlas` runs `$(GODOT) --path client client/scenes/atlas_standalone.tscn` — Godot accepts an explicit scene path as a positional argument, overriding `run/main_scene` for that invocation only (the same mechanism `godot --path client -s res://tests/visual_capture.gd` already uses to run a non-default entry script). No `project.godot` edit needed; `run/main_scene` stays `main_menu.tscn` for the normal game. Since discovery (§1) is auto-attach-else-spawn at runtime, `make atlas` itself stays a single simple target — it does not need `make game`'s explicit background-`cargo run` + `sleep` + launch + `make stop` choreography, because `atlas_standalone.gd` owns its own spawn decision and child-process lifecycle internally (§1/§2). `make atlas` is just: build client, launch it. **Exportable later:** because this is a genuine second scene (not a runtime-detected mode), it is also a legitimate Godot **export preset** target down the line — `godot --export-release "Atlas" build/atlas/...` with `atlas_standalone.tscn` as that preset's main scene. Nothing in this design blocks that; it is out of scope for this ticket (no export preset is added now) but the architecture does not need to change to support it later. This directly serves purpose (2) in the epic: "remains available as a LEGITIMATE player-facing pattern post-release." One caveat inherited from §2/§6, flagged here because it bears on export/distribution specifically: the default bind (`127.0.0.1:9876`) is loopback-only, and loopback is the entire security boundary the read-only guarantee currently leans on. A same-machine export is safe as designed. A LAN companion (a genuinely different second monitor — a different physical machine on the same network) is a different, larger feature: it requires the non-default-bind + real-auth work §6 already flags as a prerequisite for `TradingReader`, and arguably for `Reader` too once "same machine" stops holding. Not built now; recorded so nobody exports this to a non-loopback bind by default. **(4) DATA BROWSER — "scan ALL database data."** **Browse surface.** A new implant app, `implant/browser` (or folded into the Atlas as a new top-level screen reachable from "reach" — the implementation ticket picks the exact navigation entry point; recorded here as its own app since the entity set is broader than geography and doesn't naturally nest under the Atlas's reach→system→planet→regional drill-down), composed entirely from the existing D-169 component library (`ImplantPanel`/`ImplantHeader`/`ImplantDataRow`/`ImplantTextBlock`/`ImplantSeparator`) — no new UI primitives needed, this is exactly the list+detail pattern the library was built for. Two screen shapes, reused per entity kind: - **Index screen** — a scrollable `ImplantDataRow` list (name + one or two summary columns), filterable/searchable by name, one per entity kind. - **Detail screen** — an `ImplantPanel` of `ImplantDataRow`s (and `ImplantTextBlock` for free text / descriptions) showing every column the wire response carries for that one entity, `nav.push()`-reachable from the index row. **v1 entity scope (deliberately narrow, honest about phase).** `systems-schema.sql`'s table set spans registry data (systems, bodies, stations, corporations, commodities, trait templates) and cascade-derived atlas geometry (`atlas_cities`, `atlas_roads`, `atlas_rivers`, `atlas_province_boundaries`…) that is Phase-4-in-progress and per-body-optional (populated only once a body's generation cascade has run — the same `AtlasLayerStatus::Ready`-vs-`Pending` gating the Atlas's regional screen already handles). v1 ships **registry-tier screens only** — tables that exist, are fully populated, and are stable regardless of cascade progress: 1. **Star systems** (`star_systems` + `system_economy`/`system_factions`/`system_culture` folded into one detail screen — small tables, natural 1:1 join) 2. **Bodies** (`bodies`, filterable by system — the existing `SystemScreen`'s body list is the UI precedent) 3. **Stations** (`stations`) 4. **Corporations** (`corporations` + `corp_presence`/`corp_financial_state` folded in) 5. **Commodities** (`commodities` + `production_chains`/`chain_inputs`) 6. **Trait catalog** (`trait_templates`) — Jeroen's brief names this explicitly Deliberately **excluded from v1**, left for a follow-up ticket once Phase 4 cascade tables stabilize: `atlas_cities`/`atlas_roads`/`atlas_railroads`/`atlas_pois`/`atlas_rivers`/`atlas_oceans`/`atlas_mountain_ranges`/`atlas_province_boundaries` (cascade-derived, per-body, partially populated mid-Phase-4 — a browser screen over a table that's empty for most bodies today is not a useful v1 screen) and `corp_lifecycle_events`/`system_history`/`historical_events` (event-log tables, better served by a future timeline/log UI shape than list+detail). The six-entity v1 list above is the full set of "always fully populated, one row = one interesting thing" registry tables; everything else waits. **Data path — wire-only, extending the existing proxy pattern (no local SQLite read).** Two options exist in principle: (a) the client opens `server/data/systems.db` directly (it already ships in the client build — instant, complete, works even with no server running), or (b) every browser screen is a wire request/response pair through the bridge, exactly like `StarMapRequest`/`CityNamesRequest` today. **This record picks (b), unambiguously, for two independent reasons:** - **Pragmatic: Godot has no built-in SQLite.** `client/addons/` holds exactly two addons today (`gdUnit4`, `messagepack`) — no SQLite driver exists anywhere in the client. Reading `systems.db` locally would mean adding a third-party GDExtension (e.g. `godot-sqlite`) as a new dependency. D-020 explicitly rejected GDExtension for the core client-server bridge specifically to avoid "gdext pre-1.0 API churn, Godot version ABI breakage, FFI thread safety" — introducing a GDExtension now, for a companion-app convenience, reopens exactly the risk category D-020 spent effort closing. This is not a hard architectural violation (D-020 scoped its GDExtension rejection to the simulation bridge, not "any GDExtension ever") but it is the wrong trade for a feature whose entire value proposition is "lightweight." - **Architectural: the codebase already made this call, recently, on purpose.** T-949 migrated the star map — 100% static, authored, non-per-body data — off a direct client-side `FileAccess` read of `star_map_data.json` and onto a wire request, specifically because "the client never reads game data files directly" (D-010 boundary framing). That decision already resolved the "but this data is static, why not read it locally" question a companion-app data browser would otherwise re-litigate — T-949 answered it for star-map data, and there is no principled reason `star_systems`/`bodies`/`corporations` are different in kind. One data-access rule for the whole client (server-authoritative reads, always through the bridge) is simpler to reason about and extend than "static tables read locally, dynamic tables read over the wire, judgment call per table" — especially since today's "fully static" table can grow a cascade-dynamic column later (`corp_financial_state` already looks time-varying). So: **static registry data is NOT read locally — it goes through the SAME wire path as everything else**, because the server is already the sole owner of `systems.db` access and that ownership is a feature (single source of truth, single enforcement point for D-010 boundaries), not a latency cost worth working around. The "instant, complete, offline-capable" properties Jeroen's brief names as motivations are achieved a different way: attach-mode's "instant" comes from a fast local TCP round-trip (sub-millisecond on loopback — the ~1-5ms serialization cost D-020 already accepted is not the bottleneck for a data browser that isn't rendering 60fps), and "complete" comes from reading the same open handle the running server already has, with no second file-format copy to keep in sync. **Server-side extension (the actual new work).** One new proxy, following `atlas_data_proxy.rs`'s established shape: **per-entity-kind request types** (mirroring `StarMapRequest`'s "thin, one dataset" shape), not a generic SQL-ish query surface — a generic query API is a much bigger security/complexity surface for a v1 feature that only needs six fixed table shapes, and is explicitly rejected for that reason. Each handler is a `rusqlite` read against `systems.db` using the exact `CityContextReader::open()`-style pattern already proven server-side — the server already has this dependency and this pattern; this ticket is "write five more read functions," not "introduce a new capability." **D-010 boundary note — the "no character" framing, reinforced by §2.** A reader connection has no character (§2: it receives no `ObserverSnapshot` at all), so there is no per-character knowledge/fog to bound against — this is a SIMPLER boundary case than the normal player observation, not a harder one. What the Reader class is allowed to see is bounded by **connection class**, not character knowledge state, and §2 already proved the six v1 entities pass that bar independently (their handlers take no observer/character parameter — they were install-static/world-public before this record, not a carve-out invented for readers). The one thing explicitly ruled OUT of v1 scope: browsing a **specific save's diverged dynamic state** (an economy snapshot that has drifted from the shared baseline via play, one corp's post-game-start financial trajectory) is information a Reader attached to someone else's playthrough should not casually have. v1's six entities are registry-tier (identical across all saves, cascade-independent), so this doesn't bite yet — it becomes live the moment a market-state screen is added (§6) or a cascade-tier table (the excluded list above) is browsed against an attach-mode connection to someone else's running game. Flagged here so whichever follow-up ticket adds those screens re-reads this paragraph first. **(5) SAVE/LOAD SEAM — recorded hook, not built.** Saves are Phase 5+ (per the cascade); `meta.schema_version` (T-888) already carries the lineage-migration seam on the DB side, but no save file format or save/load UI exists yet anywhere in the client. This record fixes WHERE the Atlas's save/load interaction slots in, once it exists, without building any of it: - **Attach-mode** has no save/load UI at all — it inherits whatever world the attached game session is running, save/load included; the Atlas is a read-only window onto a live session, and "loading a different save" from inside an attached reader is a contradiction (that's just attaching elsewhere, not loading). No hook needed here. - **Spawn-mode v1** (this ticket's actual scope) offers **seed selection only** at launch — the standalone app's own minimal startup screen (part of `atlas_standalone.tscn`, shown before the `HudGroups.open_app("implant/map")` call in the boot sequence above) asks for a world seed the same way `character_creation.tscn`/`GameState.world_seed` does today for a normal new game, then spawns a Reader-role server against that seed via §1's `StartupMessage.world_seed` (not a CLI flag — §1 already fixed this as the single source of truth for spawn-mode seeding). - **Spawn-mode's future save picker** slots into that SAME pre-Atlas startup screen, as a second choice alongside "new seed": once a save file format exists, the startup screen gains a "load existing save" option that spawns the server and immediately issues whatever the (then-existing) `LoadGame` flow is — the exact wire action `main.gd`'s `_dispatch_pending_load()` already sends today (`InputMapper.Action.LOAD_GAME` → `SimBridge.send_input()`), reused verbatim. The Reader-role server applies the load exactly as a normal server does, then simply never accepts player inputs afterward (§2's enforcement doesn't care how the world was populated — it gates on connection role, not on world provenance). **No new save/load mechanism is invented for the Atlas** — it is a consumer of whatever Phase 5+ builds, hooked in at exactly one point (the pre-launch startup screen), recorded now so future work knows the seam exists and where. **(6) FUTURE TRADING — what changes when the app gains write verbs.** Designing the seam now, not implementing it. **Per-verb allowlist via a widened role, not a new connection type.** The `ConnectionRole` enum from §2 extends to `Player | Reader | TradingReader`. `TradingReader` is strictly **additive** to `Reader` — everything a `Reader` gets, plus a narrow, explicitly-enumerated `PlayerAction` allowlist for trade verbs — never a replacement. This keeps `Player ⊇ TradingReader ⊇ Reader` a strict superset relationship, so widening later only adds match arms at the same enforcement point (§2's role-gated input handling) and never touches the `Reader` path at all — the base read-only guarantee this whole record establishes is structurally unaffected by trading being added later. **Idempotency/ordering.** Trade commands travel through the existing `tick`-stamped `PlayerInput{tick, action}` envelope (not a bespoke unstamped request), so ordering against the Player's own concurrent actions falls out of the existing `InputQueue` ordering for free — no new sequencing mechanism needed. Unlike movement (visibly-wrong-but-harmless if accidentally duplicated), a duplicated trade command is a real bug class (a double-sell). Recommend a client-generated idempotency token + a short server-side dedup window — cheap and bounded for a localhost, single-user, low-frequency command class. Rejected alternative: relying on TCP's delivery guarantee alone — that only catches transport-level duplication, not the actual threat (a user double-clicking through a UI hiccup and generating two distinct, both-valid application-level messages). **Identity/auth — the assumption that must stay visible.** Same machine, same user, no auth — loopback-only IS the security boundary (the server already effectively enforces this via the `127.0.0.1:9876` default bind). This reasoning breaks the instant `SR_ADDR` or any non-default bind lets a `TradingReader` connect from a different machine — which is exactly D-009's actual multiplayer future, or even this record's own §3 export-later note about a genuinely-remote second-monitor companion. **The moment loopback-only stops holding, real auth (at minimum a session-minted token) is required before `TradingReader` widens beyond it.** This assumption is recorded here explicitly so it is visible to whoever eventually picks up a LAN-companion or remote-trading idea, rather than being silently inherited as "it already works, why would auth be needed." - **Rationale:** Three independent product goals (Jeroen's brief) converge on one architecture cleanly: a dev data-inspection surface (purpose 1), a legitimate post-release second-monitor pattern (purpose 2), and an attach-or-spawn reader with a save seam (purpose 3) all want the SAME thing underneath — an implant UI that can run without a player. Building that once (dedicated entry scene + `ConnectionRole`-gated reader connection + wire-only data access) serves all three simultaneously; there is no version of this where the dev tool and the shipped companion app are different pieces of software. The wire-only data path is the one design choice that could have gone either way and didn't — it is deliberately consistent with T-949's precedent rather than reopening it, and it avoids a new GDExtension dependency for a "lightweight" feature. The app-shell choice (dedicated scene over a `main.tscn` flag) keeps blast radius smallest: the standalone Atlas cannot regress player-only code paths because it never touches them. The `Reader`/`TradingReader` superset relationship (§2/§6) means the read-only guarantee this record exists to make is never at risk from the later trading feature — it can only be extended, never weakened, by construction. - **Implementation:** New ticket tree under [T-1128](../../.pql) (epic) — proposed tree delivered in the T-1129 design-pass report, not filed here (tree ownership: team lead). Client: `client/scenes/atlas_standalone.tscn` + `atlas_standalone.gd`, a new `implant/browser` app (or Atlas-nested screen) under `client/ui/implant/apps/`, `Makefile` `atlas` target. Server: `ConnectionRole` on `StartupMessage`, the accept-loop + `BridgeResource` multi-connection change, per-connection response tagging, and the Player-only `ServerRunning`/snapshot-targeting fixes (§2) in `server/src/bridge/`; a new `BrowseRequest`/`BrowseResponse` proxy in `server/src/atlas/` (sibling to `atlas_data_proxy.rs`, §4). No `systems-schema.sql` changes required — v1's six entity screens read existing tables as-is. - **Cross-reference:** [D-010](#d-010) (client-server boundary — the wire-only data-path rationale; "no character" reader framing; the adversarial-client enforcement stance), [D-009](#d-009) (multiplayer design-for-it baseline — this record's 0-1 Player + 0-N Reader model is explicitly NOT that larger ambition), [D-020](#d-020) (subprocess/IPC over GDExtension — why local SQLite is rejected; `SimBridge`/bridge trait extension point), [D-169](#d-169) (implant component library — the data browser is composed entirely from existing components), [D-170](#d-170) (HudGroups — the standalone scene's degenerate single-group case), [D-192](#d-192) (no lockstep negotiation precedent — why `ConnectionRole` is a `StartupMessage` field, not a new pre-handshake message). T-949 (star map wire-migration precedent this record extends rather than re-litigates), T-888 (schema_version save lineage — the seam §5 hooks into once it exists). Tickets: [T-1128](../../.pql) (epic), [T-1129](../../.pql) (this design pass). - **Raised by:** Jeroen (2026-07-17, brief: standalone Atlas via `make atlas`, read-only reader against the running game or its own spawned server, save/load interaction seam, future trading). Designed by Tyre (architecture lead, §3–§5, integration, record author) + Oscar (§1, §2, §6 — connection model, reader protocol, trading seam). - **Dissent:** None recorded at design time. Two judgment calls flagged for confirmation rather than dissent, both revisable by the implementation ticket without touching the rest of this record: (a) §4's choice to make the data browser a **separate `implant/browser` app** rather than a new top-level screen nested inside the existing Atlas — the six v1 entity kinds don't share the Atlas's geographic drill-down shape, so a separate app was chosen for navigational clarity (the Atlas stays "the map," the browser is "the database") but this is a naming/IA call, not architecture; (b) §3's `atlas_app.gd`-unmodified close-handling option (a) vs. a `standalone_mode` flag option (b) — recommended but not forced. --- ### D-255: Body Map Viewer — stepped Atlas render architecture (supersedes the T-1143 continuous-ladder mechanism) - **Date:** 2026-07-24 - **Decision:** The Atlas map handler is rebuilt on three locked premises: **(1) content determination is server-side** — the CPU/Rust server answers "what is at this world coordinate at this zoom step" as a per-step data canvas; the client never invents geometry (GPU is presentation only). **(2) The client is a map-art function** — it colorizes, styles, and annotates the server's canvas via a render-to-texture terrain layer (texel-exact, drawn at the display ratio) plus an unscaled screen-space sibling layer for vector annotations. **(3) Zoom is stepped** — discrete gridunit-spacing levels, one server-canvas fetch per crossed step boundary, cursor-anchored, with edge-scroll pan and a hard full-zoom-out reset to the canonical **Global (rung-0)** body-surface frame. This dissolves the T-1143 error class (the client compensating for a zoom-scaled `_canvas.scale` — `_zs`/`_zs_stroke`/`_zs_ring_radius`, the line-rasterizer floor) at the root: there is no zoom-scaled canvas anymore, so per-call compensation cannot occur. **(a) The step ladder — six levels: the Global map opener + five fixed metre rungs.** **Global (rung 0, body-surface view) → Region (204.8 km, rung 1) → District (2,048 m, rung 2) → Quarter (512 m, rung 3) → Block (128 m, rung 4) → Chunk (64 m, rung 5, deepest)** — the "Option D" ladder, every fixed rung measured. **Rung 0 (Global) is the map opener and is *variable*-extent, not a fixed metre rung** (Jeroen, post-briefing correction): it is the whole body surface at **one gridunit per region**, so its canvas *is* the body's region grid — variable per body (~19K gridunits on an Earth-class body; the [D-243](#d-243) elastic seam made visible, region count floats per `body_radius_km`), and it is the sole always-kept tier. **Region (rung 1) is the largest *fixed*-size rung** — viewport-sized like every fixed rung below it, evictable, **not** the canonical tier. Every fixed rung's gridunit spacing is one of [D-243](#d-243)'s absolute-metre rungs, never viewport-derived. **Tile/voxel (1 m) is dropped from the Atlas ladder** (a 10-px-per-tile full-screen view is ~192×108 m of ground — in-world viewport content, Phase-5's scope, not an Atlas map); voxel is reserved for Phase-5 in-world rendering. **Deepest bottom-out rule: 1 screen px per 64 m gridunit, no magnification margin** — the old "10 px per tile" margin does not carry over (it existed only because a 1 m unit is sub-readable at 1×1; a 64 m gridunit is already a legible map feature, so chunk uses the plain fixed-canvas budget with no display-ratio-sized exception). The **display ratio** (screen-px per gridunit, 1×1 ideal → ≥5×5 acceptable) is a free client-side presentation parameter, decoupled from spacing (see the [D-243](#d-243) gridunit amendment). **(b) Canvas policy — rung 0 canonical/always-kept, every fixed rung viewport-sized.** Rung 0 (Global) is the **sole canonical, always-keep tier** — the body-surface region-grid canvas, [D-226](#d-226)(d)-legal by construction (at one gridunit per region it is *coarser* than the region grid, never a sub-region metre-resolution whole-body derivation). **Every fixed rung — Region through Chunk — is viewport-sized and evictable**, bounded to a fixed canvas-pixel budget (not a per-monitor echo), which keeps the deep ladder legal under [D-226](#d-226)(d) **per request** by construction. The D-226(d) prohibition is stated as a **per-request / per-derivation** constraint, not aggregate-storage (D-226 2026-07-23 amendment); the client-cache accumulation path is closed by a per-body deep-rung retention cap (below), not by improbability. **(c) Wire — a tagged-envelope carrier, executing D-225's deferred migration.** The step-canvas payload cannot ride the legacy `district_window` windowed carrier (measured 21×–563× over the ~30 KB windowed-payload reference across the three canvas sizes — a cell-count gap of two-to-three orders of magnitude no encoding closes), so it rides a **new tagged carrier**: a `StepCanvasRequest` inbound variant carrying a **required marker field** (`step_canvas: bool`, extending the existing `star_map`/`city_names`/`browse` discriminated-shape `ShapeProbe` pattern) plus a **dedicated `StepCanvasResponse`** outbound message (not a field on `AtlasLayerResponse`, per the [D-226](#d-226) §2 windowed-family ceiling, which is re-scoped to govern the legacy carrier only). One flat tagged response carries all fields together: dense classification fields ship **PNG-per-field** (the measured smallest-and-fastest encoding); sparse feature lists (`courses`, `cliffs`) stay MessagePack-native. Both the Global opener (rung 0) and every fixed rung ride this one carrier (rung 0's variable extent is a field value, not a different message shape); the legacy `district_window` survives byte-unchanged for its existing consumer until that consumer is replaced. This discharges D-225's 2026-06-12 tagged-envelope deferral. **(d) Cache — three tiers, determinism makes every geometry tier a pure cache.** Cheapest-first: client in-memory LRU → client disk-backed `FileAccess` store (self-cleaning TTL for sim-state entries) → server ([D-203](#d-203)-shaped resident global tier + `TTL(detail, time, distance)` for finer rungs). Determinism ([D-227](#d-227)) makes every geometry tier a pure cache, never a source of truth (evict → recompute, byte-identical, always valid). **Staleness and storage are distinct eviction axes** (D-227 amendment): geometry never goes stale (storage-evicted only); sim-state planes ([D-253](#d-253) frozen/flooded) carry a real short staleness TTL. **The server global (rung-0) tier costs ~8.85 MB PNG-encoded (27.61 MB raw, SI decimal) across the entire real ~267-body population** (measured: 4,825,615 total region-grid cells, avg ~18,073/body; Earth-class reference 195×97 = 18,915 — this supersedes an earlier ~174 MB figure that mis-priced a fixed 4K-class Region-*spacing* canvas per body, ~440× too many cells), and derives in **~16–21 ms per body single-thread** (populated lazily once per body on first Atlas-open via the [D-206](#d-206) background queue; the ~4.0 s all-267-summed figure is a sanity ceiling never paid synchronously). At ~8.85 MB the keep-always tier is trivially process-resident. **Two client-cache hardening requirements:** (i) a **per-body deep-rung retention cap** (max resident chunk/block-spacing tile count or disk quota per body, independent of the rung-0 retention floor) — the structural ceiling closing the [D-226](#d-226)(d) accumulation gap against a systematic exhaustive pan (most plausibly the D-226 item-(4) `AtlasAgentInterface` QA sweep); (ii) every persistent cache entry carries a **schema/version tag**, mismatch = cache miss + re-fetch, never decode (the one place [D-192](#d-192)'s co-ship guarantee does not reach — handled in the [D-227](#d-227) amendment). **(e) Determinism boundary — what the client may interpolate.** The client may interpolate **only within a closed, server-supplied input set** — two arrived textures (step-cross morph/tween, cosmetic only), a finite list of wire-carried control points (spline-fit river/road curves through server-given stations), or texture-to-viewport resize (display-ratio scaling). It may **never** invent a sample outside that set (client-side upsampling of terrain detail — the T-1143 error class) nor smooth across a step/rung-truncation/cliff boundary as if continuous. This generalizes the T-1170 course-invention discipline: derivation stays CPU/Rust server-side, GPU is presentation only. **(f) Seed-chaining — a cache-accelerated pure function.** A finer step may consume a coarser step's resolved output as an **optimization** (reading a resident coarser canvas) with a **derive-fresh fallback** — staying inside derive-don't-store because the coarser value is itself re-derivable to identical bytes (full ruling in the [D-227](#d-227) amendment). What is consumed is the coarser rung's continuous primitive baseline, never its resolved categorical classification; a byte-identical cache-hit==cache-miss determinism test is the correctness gate; the benched costs are the cache-cold worst-case ceiling. - **Rationale:** The shipped model compensated for a zoom-scaled client canvas at every draw call — an "invisible until it's wrong" error class (T-1143's Lendel failure) rooted in the client re-drawing derived geometry inside a `_canvas.scale` node. Relocating content determination to the server, making the client a pure map-art function, and making zoom stepped eliminates that class at the root: sizes are texel-exact by construction, the render is auditable (the data canvas is inspectable server-side, decoupled from draw), and the one hard part feared going in — reconciling derived geometry with a zoom-scaled canvas — stops existing because there is no zoom-scaled canvas. Every pre-workshop cost gate came back GO with no extrapolation (hydrology 0.7–0.8 s all 273 bodies; row-chunked derive flat ~190–220 ns/cell from 330K to 8.3M cells; PNG-per-field the smallest-and-fastest encoding; texture upload 0.03–4.6 ms; deepest chunk canvas ~1.7 s parallel). The tagged-envelope migration is the one genuine chunk of new work — challenging but doable, executing D-225's planned deferral rather than inventing one; at the code level Dudley's read of `bridge/mod.rs` puts it at "doable" (one `Inbound` variant + one `SimBridge` method on a pattern proven five times), with the harder work in the client rebuild. - **Raised by:** Jeroen (the design outline: server-determines-content, client-draws-art, stepped zoom, lazy/late compute, the tile-drop and Global/Region rung-identity corrections, seed-chaining ruling — body-map-viewer workshop, 2026-07-23/24). Designed by Tyre (governance/architecture lead, record author) + Dudley (server derivation, wire/serving, hydrology, cache costs) + Araminta (named-feature encoding, payload schema, lakes) + Stig (client component, cache store, measurement ⑥) + Troblum (adversarial pass — D-226(d) accumulation, unit note, cache version tag). - **Implementation:** Phase 4 (epic [T-750](../../.pql)). Implementation chain (measurement-informed): server step-canvas serving via the tagged envelope (the migration), the client two-layer component (RTT terrain + unscaled screen-space annotations) replacing the `_canvas.scale` model, the three-tier cache, the two lake tickets (morphology-sourcing from `HydrologyResult`; basin-outlet→D8 wiring, pre-cleared by T-1170 Ruling 7b). Ticket reconciliations: **T-1176** (this design discussion) closes as delivered; **T-1158** (viewer decomposition) cancelled — the canonical-frame machinery it would extract changes shape under stepped zoom; **T-1175** (nature polish) re-scoped onto the new annotation layer (c1 = CPU-first, its measurement-⑥ blocker discharged); **T-1157** (visual-capture goldens) re-scoped as the stepped mechanism's verification story; **T-1174** (batch-vs-window derive divergence) kept, priority raised (the envelope depends on batch/window derive agreeing); **T-1152/T-1153** stay `done`, their continuous-zoom + coverage-walk + Region-tile-mosaic code and test suites retire with the `_canvas.scale` model. Measurement appendix, per-agent positions, and the full deprecation sweep: `docs/workshops/body-map-viewer/`. - **Cross-reference:** [D-166](#d-166) (development cascade — the 2026-07-23 corollary repoint; this is the stepped successor to the continuous ladder), [D-226](#d-226) (live-pause harness — the windowed-family ceiling re-scoped, item-(d) per-request + floor-partial-restore, the T-1143 rulings this supersedes; item-(4) `AtlasAgentInterface` the cache cap protects), [D-227](#d-227) (derive-don't-store — the four cache/derive amendments: TTL-split, version-tag, seed-chaining, lakes; the discipline every cache tier obeys), [D-243](#d-243) (spatial scale ladder — the rungs gridunit snaps to, the Global rung-0 elastic-seam view, the `gridunit` vocabulary), [D-225](#d-225) (layer-stream proxy — the tagged-envelope deferral this discharges), [D-192](#d-192) (no version handshake — persistent-cache boundary), [D-253](#d-253) (region transient state — the sim-state planes the map-time split carries), [D-010](#d-010) (determinism — server-owns-derivation, client-art-function; the cache-accelerated-pure-function and byte-identical-paths tests), [D-169](#d-169)/[D-170](#d-170) (implant UI — the map component lives in the implant Atlas app, occludes gameplay via HudGroups), [D-239](#d-239) §6 (the frozen 17-zone `MorphologyZone` vocabulary the lake fix reuses, never widens). Tickets: [T-1176](../../.pql) (design), [T-1177](../../.pql)/[T-1178](../../.pql)/[T-1179](../../.pql)/[T-1180](../../.pql) (measurements ①–⑤), the reconciled T-1152/T-1153/T-1157/T-1158/T-1174/T-1175 above. - **Dissent:** None. - **Amendment (2026-07-25, T-1192 / PR #205 — rung-0 integer viewport-fit display ratio):** Premise (2)'s "texel-exact, drawn at the display ratio" is **clarified for rung 0**: the Global opener's display ratio is **viewport-fitted per body** — the largest integer screen-px per gridunit that fits the legend-reserved viewport on both axes, floored at 1×1 (an over-viewport canvas draws at native ratio and crops/pans like every fixed rung) — rather than a fixed tunable. Texel-exactness is preserved by construction (every gridunit an exact integer pixel square); the letterbox remainder centres the canvas; fixed rungs keep their tuned constant display ratios. **Non-integer resting-state scaling remains unsanctioned** — a PR #205 review finding (Tyre) removed an implemented fractional-fit branch whose comments cited this record for an exception it does not contain; the [D-227](#d-227) 2026-07-23 corollary's "only sanctioned display-time scaling" (the transient between-step magnification) is joined only by this integer viewport fit, nothing else. --- ### D-256: Canonical sampling convention — one absolute-metre derive core; the batch layer is a survey raster - **Date:** 2026-07-24 - **Decision:** Resolves [T-1174](../../.pql). The "batch vs window derive paths sample different world positions for the same DistrictPos" divergence is ruled a **namespace collision, not a sampling-offset bug**: batch `DistrictPos` was an index on a 64×32 heightmap pseudo-grid (8×8 working-pixel blocks, pole-anchored, ~1/64 circumference per cell on a real body, sampled at block centre), while window `DistrictPos` is the true [D-243](#d-243) 2,048 m grid (equator/lon-0 anchored, sampled at cell origin) — one `(i32, i32)` type carrying two coordinate systems. Underneath it the investigation surfaced two latent *same-position* divergences: the batch path computed **three inconsistent latitudes** for one cell (row-index linear, `pixel_to_world_m` implied, and the noise-field anchor), and keyed its region-climate baseline on pseudo-grid indices — which collapse the **entire body onto region (0,0)'s baseline** (a live residue of the Q-110 failure D-243 was written to kill). The ruling, six parts: **(a) Canonical convention.** `derive(seed, absolute world metres)` — the `derive_at_metres` / `derive_orbital_at_metres` family is the **only** code that answers "what is at this world coordinate". `DistrictPos` canonically means the true D-243 2,048 m grid, **origin-corner quantization** (`dp * DISTRICT_M`, equator/lon-0 anchor), per the existing wrapper-contract tests; fractional positions stay legal. This is the convention every position-pinning test, every bench suite, and the [D-255](#d-255) step-canvas costing already assume — no test anywhere pinned the batch position. **(b) The survey raster.** The batch 64×32 pseudo-grid is re-scoped as a **survey raster** — the coarse planning pass for L2/L3 (settlement placement inputs, believability, overlays, skeleton dispatch context). Its key becomes a **real newtype** (`SurveyCellPos`, not a type alias) so the compiler rejects cross-namespace passing — the collision's root cause is two semantically different `(i32,i32)` grids sharing a bare tuple type. **Vocabulary guard:** "survey" is a *role name for a coarse planning raster, not a spatial rung* (mirroring D-255's gridunit-is-not-a-rung language); the D-243 ladder gains no level. The **only** bridge between namespaces is one explicit, testable function: survey cell → its centre world-metres (the pixel-index-midpoint centre `8rx+3.5` is kept — it is the geometrically correct midpoint of the 8-point bilinear sample lattice, not an error). **(c) One derive core.** `derive_district_profile` becomes a **thin wrapper over the shared metres-addressable core** at the survey-cell-centre world position — one derive core, two position sets. This auto-fixes both latent divergences (one inverse mapping computed once is definitionally self-consistent; region keying inherits the core's true-metre floor-divide, giving latitude-graded climate instead of the body-uniform artifact). Two preservation requirements are **binding**: (1) **`basin_direction`** — an inert pass-through field (proven: nothing in the derivation reads it) — is preserved via post-call field override with the true L1 D8 value; the window path keeps its documented North fallback. (2) **The riparian verdict** must come from `near_perennial_water_at` (on-demand course invention) exactly as today — a naive wrapper passing the empty course slice would silently regress every riverside cell to `near_perennial_water = false` and flip `vegetation_class`; the fix is an extracted private core parameterized on the pre-computed riparian boolean, leaving `derive_at_metres`'s public signature and byte-behavior untouched (window goldens must stay green unchanged). **(d) Exact-position judgments.** Terrain judgments attached to point features (the quarter-skeleton context's `morphology_zone`, today a survey-map lookup keyed off the settlement position) move to an **exact-position derive at the feature's world metres**, server-side, resolved where the terrain cache is reachable — a survey cell spans ~hundreds of km and its centre is not the settlement's terrain. This closes the annotation-vs-canvas disagreement class ([D-255](#d-255)'s Lendel failure class) *before* T-1181/T-1182 draw batch-judged annotations onto window-derived canvases. **(e) Fencing for D-255.** Step canvases (all six rungs) derive **exclusively** via the canonical family; survey products are never canvas sources. The survey raster is a planning input — [D-227](#d-227)-legal exactly like every cache tier (evict → recompute → byte-identical), never a second source of terrain truth. **(f) Deferrals, tripwires discharged.** The collapsed `LayerRegionOutput`/`region_grid` rebuild defers to T-1181 rung-0 (verified: its sole production reader is the `build_region_grid` body-view overlay; no consumer reads it against `DistrictProfile` climate, so no new disagreement enters this ruling's blast radius — the overlay is visibly stale until the rung-0 Global canvas replaces it, accepted and noted). Voxel-carrier rewiring defers to Phase 5 (the carrier inherits self-consistent profiles from (c) for free, per the [D-166](#d-166) same-derivation amendment). **Known costs (accepted):** batch-layer *values* change (latitude fix + true-region climate) → believability goldens regen and map overlays legitimately diff against screenshot baselines. **Settlement and road positions do not move** — placement positions source from Layer-1 attractor search, untouched; the T-1075 install bake is keyed by DB row identity, orthogonal on both axes. D-245 believability gates need **no threshold changes** — every gate rewards variance, which this fix strictly increases (the same direction as the T-1080 moisture amendment, one layer up). - **Rationale:** T-1181's acceptance gate (cache-hit == cache-miss, byte-exact, every rung) and D-255(f)'s cache-accelerated pure function require a sample's identity to be `(seed, position)` and nothing else; two paths resolving different positions for the same nominal cell break that identity at the root. The evidence for the canonical choice was uniform (all position-pinning tests, all five bench suites, the D-255 measurements, and the Phase-5 streaming amendment all sit on the absolute-metre family), and the unification *deletes* reconciliation code rather than adding it — one inverse mapping replaces three latitude conventions and a collapsed climate key. The newtype is the structural fix that makes the class un-reintroducible; a rename alone would relabel the footgun without disarming it. - **Raised by:** Found by Dudley (T-1168 integration testing, the layer_proxy design note). Ruling drafted from a four-reader code sweep; stress-tested by Tyre (APPROVE-WITH-CHANGES: basin preservation binding, exact-position judgments pulled into scope, newtype over alias, region-consumer tripwire, vocabulary guard) and Dudley (FEASIBLE-WITH-CHANGES: inert-basin proof, riparian empty-slice regression + extracted-core shape, fallout inventory, T-1075 orthogonality, sample-lattice centre ruling). - **Implementation:** [T-1174](../../.pql) (single PR: core extraction + wrapper + `SurveyCellPos` newtype + skeleton exact-position judgment + strengthened cross-path agreement test + golden regen). Successor obligations riding existing tickets: T-1181 (rung-0 replaces the collapsed region layer), Phase 5 (voxel carrier). `RegionPos` shares the bare-tuple hazard — noted, out of scope here. - **Cross-reference:** [D-255](#d-255) (the dependency that raised this — envelope needs derive agreement; the Lendel failure class (d) closes), [D-243](#d-243) (the true grid, the Q-110 residue this clears, vocabulary lock honoured), [D-227](#d-227) (derive-don't-store — the survey raster as planning cache; the seed-chaining purity this repairs), [D-239](#d-239) (§8/§10 basin preservation, §2 climate-as-district-tier-field made body-honest, frozen vocabulary untouched), [D-245](#d-245) (believability — the body-uniform climate artifact this removes), [D-166](#d-166) (Phase-5 same-derivation amendment this makes satisfiable). Tickets: [T-1174](../../.pql) (this ruling), [T-1181](../../.pql) (gate consumer), [T-1168](../../.pql)/[T-1170](../../.pql) (where the quirk surfaced and the invariants this strengthens). - **Dissent:** None. --- ### D-257: Environment props share the character toon shading treatment; minimal-PBR carve-out for glazing - **Date:** 2026-07-25 - **Decision:** Environment props and furniture (the D-235 ObjectTag vocabulary's rendered assets — walls, roofs, facades, street furniture, and freestanding furniture/prop `.glb`s produced via `/glb-gen`) render with the **same toon shader family already used for characters** (`toon.gdshader` / `toon_masked.gdshader`, per `client/assets/characters/shaders/`), not a distinct PBR material response. The glb-gen postprocess convention of forcing `roughness=1.0`/`specular=0.0` ("for toon compatibility," `postprocess_glb.py::setup_materials()`) is confirmed as the correct default for this vocabulary's bulk. - **Named carve-out — glass and polished/mirror metal.** The ratified glazing tokens — **`glass_curtain_wall`** (D-235 wall axis) and **`industrial_glazing`** (D-235 facade axis) — plus any deliberately reflective/polished-metal prop (chrome fixture, mirror, glossy display case), get a **minimal-PBR material** layered over the toon base: `roughness` in the 0.05–0.2 range with non-zero `specular`/`metallic`, not a full PBR stack (no environment-map reflections, no fresnel-driven rim lighting). The toon shadow-band lighting model still governs the base surface response; only a controlled specular/transparency term is added. - This binds all future environment-shader and glb-gen-postprocess work; it is not a per-asset style suggestion. - **Rationale:** Consistency compounds — a station built from one shader family reads as one coherent object; mixing a toon character against a PBR-lit room breaks the "one world" read the instant the player looks at their own hands next to a table (D-043/D-044 visual-hierarchy principles: entities and objects share one legible material language, distinguished by outline weight and saturation tier, not by rendering technology). The postprocess default already assumes this — `setup_materials()`'s "for toon compatibility" comment was written against the one Trellis pipeline that produces both character and environment `.glb`s, so sharing the treatment requires no new postprocess fork. The glazing carve-out is functionally forced, not a taste call: a flat-lit `roughness=1.0` glass pane reads as a frosted grey panel, which actively misleads the player about sightlines in a game whose core mechanic is occlusion-based perception (D-033's asymmetric-information model depends on the player correctly judging what they can and cannot see through). The carve-out is narrow and named — it closes only the case where toon-only actively lies to the player about transparency, and does not reopen "should props be PBR" generally. - **Raised by:** Araminta (T-1052, delegated art-direction ruling), reviewed by Tyre (PR #211 — flagged the original carve-out's use of retired pre-amendment example tokens `precision_glass`/`smart_facade`, corrected here to the ratified D-235 tokens). - **Implementation:** `docs/assets/visual/palette.md` §2 (practical guidance, cites this record as authority). Applies to all environment-prop material setup from Phase 4 asset production onward; no existing shipped assets to migrate as of this record's date. - **Cross-reference:** [D-235](#d-235) (ratified ObjectTag vocabulary — `glass_curtain_wall`/`industrial_glazing` are its tokens, not this record's invention), [D-244](#d-244) (3D objects in-world; establishes the shared Trellis/glb-gen pipeline this record's postprocess argument depends on), [D-043/D-044](perception.md#d-043-art-direction--visual-style-functional-warmth) (visual-hierarchy/"functional warmth" rationale — entity/object/structure share one material language), [D-033](perception.md#d-033-entity-color--relationship-to-player) (the occlusion/asymmetric-information rationale for the glazing carve-out). - **Dissent:** None. --- *110 decisions (D-001 through D-256, excluding gaps). Last updated: 2026-07-24 (D-256 — canonical sampling convention: one absolute-metre derive core (`derive_at_metres` family), batch pseudo-grid re-scoped as `SurveyCellPos` survey raster (real newtype), thin-wrapper unification fixing the three-latitude inconsistency and the region-(0,0) climate collapse, exact-position feature judgments, step-canvas fencing; discharges the T-1174 blocker on D-255's T-1181 envelope).*