Files
settled-reach/governance/decisions/architecture.md
T

376 KiB
Raw Blame History

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 (256×256 visual tiles per district, 4×4 blocks). D-094 explicitly amends this decision.

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). 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<T> 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.
  • 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.
    • 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<StableId, EntityKnowledge>, (2) fact knowledge map: BTreeMap<FactId, FactKnowledge>. 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<KnowledgeGraph> 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 principle 4). Tile-based enables shadowcasting (D-238), pathfinding, chunk-based maps (D-012), 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), shadowcasting (D-238)
  • 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 (100150 ms)" sentence is scoped to the 2D renderer henceforth. A fixed-duration tween conflicts with the D-053 stance cadences (200800 ms/step) — dash-then-stand stutter at every stance below Sprint. The 3D presentation layer interpolates per D-248 (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), 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), monologue (D-016)
  • 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), pathfinding, occupancy (D-054), 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) 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): 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): 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-244), 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), shadowcasting (D-238), fog (D-059), art direction (D-043), z-stack (D-049), stance system (D-053)
  • 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 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).
    • 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 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).
  • 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), audio dip (D-069), client-server architecture (D-020)
  • 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), client-server (D-020), ambient assets (D-038)
  • 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/<game-id>/ where <game-id> is <timestamp>-<seed> (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): 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 (03 steps, 15° increments), variable street width (0.752.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<PaletteModifier> }. 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 (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 (12 z-levels, surface + roof/mezzanine), S2 (310), S3 (1130), S4 (30+). Shadow length is the primary height signal in top-down view (240 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<ChunkCoord>, docked_since: SimTick, scheduled_departure: Option<SimTick>. 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.54KB 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 stateDamageOverlay (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<char, TileType> 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:
    # 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 (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 (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). 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 (alarm clock tone), D-135 (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).
  • 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 (generator proof-of-life), D-119 (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 (hardware detection for voice pipeline), D-010 (information boundaries), Q-059 (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
  • Cross-reference: D-149 (3D rendering), D-151 (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-150, D-151, D-152, 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-152, D-154

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-149

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-150

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 (body type enum), D-149

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-163

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, 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-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, D-160

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.
  • 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-746751, CLAUDE.md cascade table, docs/workshops/world-generation/workshop-outcomes.md

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:
    • ImplantPanelPanelContainer with theme override, serves as root for any implant overlay
    • ImplantHeaderHBoxContainer (title label + subtitle label)
    • ImplantSeparatorHSeparator with theme override
    • ImplantDataRowHBoxContainer (key label + value label, two-column)
    • ImplantTextBlockRichTextLabel 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_summarybodies.planet_class), schema SQL, all Rust atlas code, wiki table headers (BiomeClass), 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 34: 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 modifiershadow_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): 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 (development cascade — Phase 3), D-036 (Sova as canonical setting), D-093 (Sova spatial layout), D-094 (district hierarchy), D-095 (Horizon stations), D-170 (HUD visibility/implant apps), D-169 (implant component library), D-181 (signal vocabulary/visibility ladder), D-174 (shadow economy intensity), D-175 (corporation taxonomy), D-138 (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), 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.
  • 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 (subprocess model — always co-shipped).

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.03.0) applied to that district type's base probability for cities of that economic role.
    3. Political archetype modifiersPoliticalArchetype 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.03.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.03.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:
    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.01.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 bonus0.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 12 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.01.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_classSettlementClass enum value (D-196, determines whether to generate at all)
    • Fields 15 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 (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 — 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.
    struct CityGenerationContext {
        city_id: u64,
        political_archetype: PoliticalArchetype,
        prosperity_baseline: f32,
        surrounding_biome: SettingType,
        road_entry_directions: Vec<u8>,     // compass octants (07)
        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 ~50500km Province boundaries (watershed-derived, D-205), biome zones
    5 Settlement ~130km 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 69 (District → Chunk) are the sub-settlement spatial hierarchy, canonical in 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 15 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): Tier 4 renamed Region → Province — it always was the watershed/political tier (D-205), and the name "Region" is now reserved for 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 owns the metric containment ladder (voxel→chunk→block→quarter→district→region); Tiers 15 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 68), 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:
    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::<u8, f32>() 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.pngreliefmap.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 12 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 12 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:
      struct BodyWorldState {
          body_id: u64,
          heightmap: Vec<f32>,               // downsampled working grid
          river_network: RiverNetwork,       // D-208 output
          drainage_basins: Vec<DrainageBasin>,
          attractors: Vec<GeographicAttractor>, // 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:
    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: 412 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): 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): 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.
    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 ~815 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<GeographicAttractor> in BodyWorldState. Each attractor has a position [row, col] and a strength: f32 (0.01.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 1060% 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<GeographicAttractor> 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,000999,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<CityPlacement { city_id, position: [row, col], attractor: AttractorType, score: f32 }> 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:
    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_instituteCommissionControlled (the Assembly is the Reach's central government; the Veil Institute is Assembly-funded and -aligned); syndic_dominantCorpTerritory; compact/compact_sympatheticAutonomistHeld; disputed/mixedContestedZone; 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 (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.
    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 0359 (0 = North, clockwise). Integer to preserve D-010 determinism.
    • The founding orientation is derived from the matched attractor type (D-211): RiverMouthCoastal; RiverAligned; CoastalAccessCoastal; ValleyFloorTerrainFollowing; 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.
  • Ticket: T-914
  • 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-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:
    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; CorpTerritoryCorporate; FrontierUnclaimedPioneer; 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 — 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 ~4060 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 (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 24 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 23 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 23 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.01.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:495499):

    settlement_wave era range (years ago) founding-edge fallback
    wave_1 Core Founding 500600 600
    wave_2 Trade Expansion 300500 500
    wave_3 Working Reach 100300 300
    wave_4 Frontier Push 40100 100
    wave_5 Activation Edge 040 40
    origin Sol — predates the wave taxonomy NULL (Sol excluded per 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.01.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:
    enum WorldTier {
        Epicenter,  // Hub system. Full simulation. High faction pressure. Multi-district cities.
        Regional,   // Regional hub. 14 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): 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.52.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.21.8
    
    // Wealth disperses (for specific districts within a settlement)
    if district.prosperity_baseline > 0.8:
        effective_density *= 0.40.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 130km 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 (spatial hierarchy — renamed/superseded), D-201 (tier table — superseded for the sub-settlement tiers), D-220 (tile scale — Tile/Subtile retained; its "district" footprint cell is the Quarter), D-066 (dual-scale — Subtile is the 0.5m grid), D-093 (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, and the hand-authored-template references in 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. 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 (superseded — names-only, no reserved pinning), D-191 §8 (markers format — names-only), D-208 (drainage → river courses), D-211 (settlement placement), D-199 (economic read set), D-222 (lore≠code names on generated geometry), 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:

    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 (determinism — integer-only, seed→identical output), D-200 (three-tier execution model), D-208 (RNG-free drainage), 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<PlayerInput>). 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 <root>/<terrain_reference>. 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<PlayerInput>, 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.

  • Cross-reference: Q-098 (resolved by this), Q-099 (mod content catalog — spun off), D-191 (Atlas viewer), D-166 (per-layer Atlas progress viewer), D-200 / D-203 (three-tier execution, LRU cache), D-005 / D-192 (client+server co-ship — no version handshake), D-224 (SeedChain — feeds the cascade the proxy runs)

  • 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 intentsselect_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.
  • 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 (layer-stream proxy — the data path), D-166 (per-layer Atlas progress viewer), D-191 (Atlas viewer), D-169 / D-170 (implant components / HUD occlusion — gameplay_occluded trigger), D-200 / D-203 (execution tiers / LRU cache), Q-099 (mod content catalog), tests/run-visual (capture primitive), save_state.rs (save-inspection consumer)

  • Dissent: None


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).
  • Raised by: Jeroen (derive-don't-store, volumetric, drop-the-floor-cap directives) + Claude, atlas-derivation workshop, 2026-05-25.
  • Cross-reference: D-010 (determinism — now save-critical), D-222 (subtile/tile/chunk hierarchy), D-110 (signed z-levels), D-225 (layer-stream proxy + cache pattern), D-203 (LRU cache tier), D-224 (SeedChain — feeds derive), D-228 (composite tile schema — the derived value type), Q-101 (refinement contract), Q-103 (mutator op schema), 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 — 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.
  • 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 (derive-don't-store — these axes are the derived value type), D-010, D-210 (sub-biome — region biome axis), D-222 (subtile/tile), D-049 (z-stack render — TileKind seed), D-208 (D8 — flow-direction source), D-226 (dynamic-state inspection — the water overlay), Q-100 (biome authority), Q-101 (refinement contract), Q-102 (cohesion-matrix algorithm), 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<str> 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<u8>). Q-104 resolution (the D-110 ↔ D-227 bridge): two pure functions — floor_at_voxel_z(z) -> Option<i8> 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 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_surfaceport_maritime (+port_fishing for Transit), rural_*rural_aquaculture, extraction_surfaceextraction_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 (zone-type taxonomy), D-096 (layout mode), D-197 (prosperity baseline), D-217 (tile condition), D-110 (signed z-levels), D-227 (voxel substrate), D-101 (ZonePalette), D-028 (dialogue access — name disambiguated), D-230, D-231, D-232, 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<BuildingPropertyTag>> }, stored in BodyWorldState.districts: BTreeMap<DistrictId, DistrictWorldState>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 × ~1015 ns = 0.40.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 (three-tier execution — amended), D-203 (LRU cache), D-225 (layer-stream proxy), D-227 (derive-don't-store), D-215 (arrangement patterns), D-220 (density), D-010 (determinism), 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-097 (guarantee audit reads connectivity), D-106 (rooftop clause / hidden door), D-110, D-227 (mutators), D-142, D-217, 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.tomltrait_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<String> 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, ~3040). 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: ~2535 templates at floor, 4045 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 — 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<String> 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 23, 2026-05-25.
  • Cross-reference: D-167 (corridor cultural system — completed here), D-223 (Gemma naming pipeline + corridor mixing), D-142 (zone types — the function baseline filtered by templates), D-228 (morphology zone), D-233 (economic channel — what vs how), D-234 (morphology→street — the hard-gate layer), D-235 (visual bundle + fallback hierarchy), D-217/D-198 (condition — where era's wear lands), D-224 (SeedChain — the draw PRNG), D-199 (read-set — extended), D-237 (authored cultural_specialization — template-pool selection), D-101/D-104/D-105/D-107 (superseded in part), Q-106 (era reframed), 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.850.95 → PrecisionDense 0.750.90 → Perishable 0.500.65 → BulkSolid 0.250.40 → BulkLiquid 0.200.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.40.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/oversizedBulkSolid [size is orthogonal to bulk-form; a future axis if it earns visual payoff], precisionPrecisionDense, liquidBulkLiquid, perishablePerishable, non_physicalNonPhysical). 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 (district mix — upstream), D-184 (commodity catalog — amended), D-199 (read-set — amended), D-220 (density), D-197 (prosperity), D-142 (zone types — downstream), D-176, D-180 (EconEvent — overlay trigger), D-198 (condition overlay — amended), 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. (Civ14'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 TileRects 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 (morphology zone), D-215 (arrangement patterns — gated), D-096 (layout mode / ±45° cap), D-220 (density), D-229, D-213 (founding orientation — water bearing), Q-106, 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 (composite tile / FloorMaterial — extended), D-232 (flavor — the filter), D-217 (condition / heritage markers), D-142 (zone types), D-220 (density → setback), 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 ~80100 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 ~510% 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 ~60100 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 ~4060 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 (re-amended — economic block-fill now sourced from this layer), D-184 (commodity catalog), D-199 (CityGenerationContext read-set — extended), D-232 (architecture-flavor template draw — consumes cultural_specialization), D-214 (PoliticalArchetype — consumes dominant_faction), D-172 (currency zone alignment — dominant_faction = compact validates), D-174 (shadow economy intensity — compact elevates), D-177 (productivity constraints — informs monopolistic pins), D-220 (density), D-197 (prosperity), D-222 (multi-block reservation — Groombridge landmark mechanism), 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

  • 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: 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. L1L5 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, 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) is literally temperature(time). This formalises 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.)

    (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 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, 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; 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 13 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 315 m, gorge floors 28 m); seasonal/tidal state produces real passability changes, not cosmetic — BraidedDelta/MeanderReach ElevationDelta calibrated so channels fall below and levees above the 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, ~6k/body, populated in the D-206 background pass) so the Atlas reads zone labels without triggering voxel derivation; voxel derivation is on-demand + cached, never persisted (D-227). Budget ~2.24.2 ms/chunk (validate per-family in Phase 4 — FjordWall/IncisedGorge are far costlier than the AlluvialPlain fallback).

  • Rationale: D-227/D-228 fixed what a tile is (derive-don't-store voxels; orthogonal derived axes); D-239 fixes how the finished upper cascade (L0L4) 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, 0100). 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-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 "03 m, no hills to navigate by" bug). A voxel_relief pass (detail_scatter.rs, the terrain_detail fBm at a 0.131 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 440 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 (~4078 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 (derive-don't-store voxel model), D-228 (composite tile axes / cohesion / seasonal state), D-210 (temperature proxy — formalised), D-203 (BodyWorldState cache), D-206 (background analysis pass), D-208 (drainage / D8), D-010 (determinism), D-234 (street/footprint geometry — consumes morphology), D-142 (zone types), D-217 (tile condition), Q-102 (cohesion = the warp), Q-103 (mutator schema — open), 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). 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 §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 §2 (temperature primitive — the sun-driven portion is superseded here), D-010 (determinism — seed-based variety), D-227 (derive-don't-store), 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-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'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 placement pipeline). This supersedes D-223's "population/settlement_class deferred to placement (T-955)" and retires the never-built "assign population from the seed" stepmatch_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, 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) 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 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.shretired/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 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, or general professional/advisory work — moving or selling what someone else made, not producing it — which none of the 27 values name. Three values were added to specialization_vocabulary.toml (all NonPhysicalCityTenant): trade_distribution, hospitality_hub, professional_services. The vocabulary is now 30 values, shared by two authored axes: system_economy.economic_specialization (D-237, system-scale) and corporations.corp_specialization (this record, corp-scale) — nothing stops a future system from adopting the 3 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 placement map is authored explicitly, not computed from bulk_class_projected at import timewiki/economics/corp_hq_placement.toml, one hq_placement stanza per vocabulary value (all 30), defaulting to the NonPhysical rule but reviewable/overridable per value without a code change. Standalone stanzas also carry standalone_economic_role, mapping the 30-value specialization onto the pre-existing 10-value economic_role vocabulary (D-195/D-197) the emitted settlement row needs (match_cities/CompatibilityMatrix index by the 10-value set, not the 30-value one).
    • headquarters_body backfill reused the lifted heuristic, with one fix found live. most_populated_body_in_system (lifted from the retired cross-reference function into atlas.py, reused by both the backfill and the Standalone-settlement emission) needed a third tiebreak: when every body in a system ties at population = 0 (found live — GJ 702B/Prometheus, all 12 bodies), a population-then-body_id-ascending sort can land on an asteroid_belt/oort_cloud (its naming convention's - sorts before letters) over a planet sibling — a type that structurally cannot host a settlement. A body_type preference (planet < moon < gas_giant < asteroid_belt/oort_cloud) was added between the two, engaging only among population ties.
    • 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_settlementsmatch_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 a real, repeated make regen-db: 0 duplicate (body_id, name) groups (was the whole bug); 155/155 wiki corps get corp_specialization/hq_placement; 154/155 get headquarters_body (the 1 gap is a pre-existing broken headquarters system reference on sova-station-works.md, unrelated to this record); 95 Standalone corps → 94 settlements emitted (same 1 gap) + 60 CityTenant corps → 58 links (2 unmatched: prometheus-labs/kovalev-freight, both on GJ702B-belt, which — even after the tiebreak fix — has zero authored city names anywhere in its system, a wiki content gap, not an import bug); deterministic across 3 consecutive runs. 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 left with corp_specialization/hq_placement/headquarters_body NULL — 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).
  • Cross-reference: D-207 (corp-HQ cross-ref — the insert path is removed here), D-223 (names-only pool — the corp-HQ-stays + pop/class-deferred notes are superseded here), D-211 (placement — consumes the merged list; only position is seed-derived), D-213 (FoundingOrientation — a RailHeadFacing variant is the gated follow-on), D-237 (corp specialization vocabulary — the HQ-placement key, extended here from 27 to 30 values), D-195/D-197 (the 10-value economic_role vocabulary standalone_economic_role maps onto), D-196 (SettlementClass — the bake target), 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.

  • 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-220)
    chunk 64 m ×64 voxels stream / derive unit (D-222, D-239)
    block 128 m ×2 chunks generator planning unit (D-222)
    quarter 512 m ×4 blocks settlement footprint cell (D-222)
    district 2,048 m ×4 quarters urban division and local-climate cell (D-222, 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) — the single per-body quantity. The fixed 1024×512 heightmap (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 ~4078 km). All sub-heightmap detail (region → voxel) is invented deterministically, not stored (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 §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 §2; feeds 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 §2's 2 km climate district, now derived as a modulation of the region rather than from scratch); chunk/voxel = the 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 §4's domain warp (same meta-rule: the addressing grid must never be visible in the result), with one distinction from 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 §2's existing climate district. D-201's tier-4 "Region" (50500 km watershed/political) is renamed Province (its actual content — drainage basins, 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).

  • 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) 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 amended (Region→Province). The canonical ladder is recorded in CLAUDE.md. Refines D-239 §2 (district temperature now modulates a region baseline) and contributes the region-clock answer toward Q-105.

  • Cross-reference: D-201 (spatial hierarchy — tier-4 Region→Province, amended here), D-202 (heightmap 1024×512), D-204 (body_radius_km), D-220/D-222 (locked sub-settlement dims — chunk/block/quarter/district), D-205 (province boundaries — the overlay), D-225/D-227 (derive-on-demand / don't-store), D-239 §2/§3/§4/§7/§10 (climate primitive refined; warp; seams; budget reframes to per-active-derivation), 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); characters come from the Quaternius 3D-mesh pipeline composited at runtime via CharacterVisualDescriptor (D-159D-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, supersedes D-019) and live 3D characters (D-149), 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 (3D camera, supersedes D-019) and D-149 (live 3D characters, not sprites) — D-244 extends both to all objects + the asset pipeline. D-159D-164 (character 3D pipeline), D-241 (asset promotion), D-227 (derive/synthesise, don't store). Reconciles the sprite-era decisions (all amended 2026-06-17): D-043 ("not 3D"/Light2D scoped to overlay layers), D-044 (sprite footprint retired; sim occupancy + hierarchy survive), D-049 (z-stack logical model survives; sprite implementation language retired), D-066 (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).
    • "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) showed the nature layers can pass every binding law (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 (binding laws — necessary but not sufficient; this is the sufficiency gate above them), D-227 (derive-don't-store — the tier the non-stationary variation lives in), D-243 (the scale ladder the multi-scale litmus spans: macro region / meso district / micro chunk), D-210 (SubBiomeVariant — amended in spirit: sub-biome must be a spatially-varying distribution, not a single tag, T-948), 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) modulation of the existing 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):
    1. Mechanism — a new sub-chunk noise band + relief-conditioned selection. A voxel_mosaic() octave band at ≈864 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): 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 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 §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 tundraD-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 — 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): 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 §8) and still render as one uniform material ("Wetland 100%"); 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 (story under epic T-1079). 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 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 §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 (the axes this modulates — amended 2026-07-02 to add the five values), D-227 (derive-don't-store — the tier the mosaic lives in), D-245 (believability gate — this is condition 3, intra-class variety), D-239 §8 (binding laws — the mosaic stays inside them) / §10 (sub-chunk budget), D-243 §2 (the detail-scatter tier T-1081 and this extend), 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 / T-948 (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 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) — accepted as sim truth so held diagonals arrive on time. Gait clips are cadence-synced (speed_scale = clamp(leg_speed / NATIVE_MPS[clip], 0.61.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-158).
  • Rationale: resolves the D-054 ↔ D-053 cadence conflict: a fixed 100150 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: its "client-side Tween interpolation (100150 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-053, D-055, D-066, D-149, D-249. 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: 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 ~250300 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-151, D-248, 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). 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 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 (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.

  • Decision: The region (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 §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) and the region's static params:

    • Diurnal — day/night, from the D-031 day-phase clock; amplitude = the atmosphere heat-retention already defined in D-239 §2 (thick air → small swing). This term is the one 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 static baseline + seasonal offset + diurnal offset (this makes 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 §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'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'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 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 §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) — 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). 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 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 §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, no drift), cheapness (recompute only on bucket rollover, and per-region not per-tile → O(regions)), and inspectability (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 §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 §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 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), 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-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 (region = climate/weather/season lockdown scale; §3 computed-once-inherited; §4 edge-fuzz — the inheritance contract), D-228 (the seasonal water-height / snow-cover overlay / crop cycle this model drives — its open water-height sub-question resolved here), 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 (planet_class temperature envelope — the baseline the seasonal/diurnal terms offset), D-226 (dynamic-state frozen-phase inspection), D-031 (game clock / day phases — the clock source), D-010 (determinism), D-203 (BodyWorldState region cache — the memo home), D-227 (derive-don't-store — the memo is not persisted). Tickets: T-1057 (this triage), T-1030 / T-1078 / T-1082 (the discharged forward-contracts), T-1081 (voxel_relief micro-lows — puddle/flood sites).

  • Dissent: None


107 decisions (D-001 through D-253, excluding gaps). Last updated: 2026-07-08 (D-253 — region transient state model: four clock-terms (diurnal/tidal/weather/seasonal), memoized-by-bucket absolute-clock evaluation, edge-fuzzed per-tile realization; resolves Q-105).