Adopts the T-NNN convention (T-N == old #N == pql ticket id) across the active
operational layer: governance/ decision records, .claude/{rules,agents,skills},
CLAUDE.md, DECISIONS.md. 283 references rewritten.
Guarded against false positives (17 correctly skipped, each logged):
- PR references kept (PR #136/#138/... — PRs are a separate #-namespace)
- non-ticket numbers kept (#4122; the "#1 process failure" idiom; "task #3")
- only #N where N is an actual ticket id is rewritten; the 1-4 digit word-bounded
match also excludes 6-digit hex colours in the visual decision records
Git history is NOT rewritten (a commit's #N already equals T-N numerically), and
historical archives (docs/sprints, docs/discussions, docs/workshops) keep their
point-in-time #N. The /pr-process ticket-ID extraction logic moves to T-NNN in the
Phase 4 consumer cutover.
Verified: pql decisions validate ok; sync 357 records / 1057 refs / broken 0 (the
prose edits don't affect decision parsing or the tickets.decision_ref linkage).
Transform committed at tooling/pql-migrate/retag_ticket_refs.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@@ -88,7 +88,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **Raised by:** Team Leader (timestamp model), Tyre (technical validation), Gestalt (scope tags)
- **Dissent:** None
### D-030: Testability architecture — 8 decisions for ticket #214
### 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`.
@@ -97,7 +97,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
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. **#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.
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.
@@ -119,26 +119,26 @@ Technical foundation decisions that constrain implementation: engine, client-ser
### 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 (#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.
- **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 #7).
- **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 #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.
- **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.
- **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:**#409 (UI microcopy)
- **Related ticket:**T-409 (UI microcopy)
- **Raised by:** Team decision in Sprint 5 planning
- **Dissent:** None
@@ -175,7 +175,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **Fog shader ([D-059](perception.md#d-059-fog--shader-based-five-layers-knowledge-graph-driven)):** Unaffected — fog is screen-space, driven by PointLight2D vision cone and LOS mask from sim-resolution shadowcasting. Gradient edge "3-4 tiles" is retuned to 6-8 sim tiles (= 3-4 visual tiles) to preserve the intended softness.
- **Cursor/interaction:** No change — cursor already resolves to sim tile from pixel position. Interaction range of ~2 sim tiles = 1m (arm's length).
- **Map authoring:** Author at 1m visual scale. Subdivision tool expands each visual tile to 4 sim tiles (2x2). Validation enforces 2x2 minimum on all world geometry layers.
- **Amends:** OQ-01 resolution (ticket #444). Sim tile size remains 0.5m; visual presentation changes from 1:1 to 2:1 retina factor.
- **Amends:** OQ-01 resolution (ticket T-444). Sim tile size remains 0.5m; visual presentation changes from 1:1 to 2:1 retina factor.
- **Cross-reference:** Tile-based movement ([D-054](#d-054-tile-based-movement-with-same-tile-occupancy)), shadowcasting ([D-238](perception.md#d-238-symmetric-shadowcasting-albert-ford-selected-for-los-computation)), fog ([D-059](perception.md#d-059-fog--shader-based-five-layers-knowledge-graph-driven)), art direction ([D-043](perception.md#d-043-art-direction--visual-style-functional-warmth)), z-stack ([D-049](perception.md#d-049-z-level-rendering-stack-8-layers)), stance system ([D-053](scope.md#d-053-movement-as-stance-toggle-system))
- **Rationale:** 0.5m sim tiles give stealth-grade granularity for movement stances, cover peeking, and interaction range. 1m visual tiles make spaces feel proportional, sprites look right, and world geometry readable. The 2x2 minimum on geometry eliminates visual/sim mismatch for cover and LOS — the only sub-visual-tile positioning is entity movement, which is communicated through fog feedback, not tile counting. Analogous to macOS Retina: logical resolution (visual) differs from physical resolution (sim), but the system is coherent because both agree on where solid objects are.
- **Raised by:** Team Leader (Jeroen) — proposed retina scaling analogy and 2x2 geometry constraint. Tyre (feasibility: trivial, half-day integration). Gestalt (approved with 2x2 constraint resolving LOS readability concern). Ozzie (approved: solves sprite scale without uncanny mismatch).
@@ -244,7 +244,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **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 #153, Sprint 20. Round document: `docs/discussions/round-20-station-district-layout.md`
- **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)
---
@@ -253,7 +253,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **Date:** 2026-02-27
- **Decision:** Two layout modes coexist for district generation. `Grid`: Commission-planned districts with rectilinear block placement. `Organic`: pioneer/growth districts with block offsets (±16 sim tiles per axis), rotation (0–3 steps, 15° increments), variable street width (0.75–2.0×). Hard technical ceiling: maximum rotation ±45°. Beyond 45°, tile-based pathfinding produces unacceptable movement artifacts. Organic districts produce curved-street impressions through angular jogs and irregular setbacks. Grid vs. Organic proportions must vary per seed to prevent predictable meta-level patterns.
- **Rationale:** Grid = power imposed (Commission-planned). Organic = power negotiated (pioneer settlements, organic growth). Both modes encode political and settlement history in spatial form.
@@ -262,7 +262,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **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.
@@ -272,7 +272,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **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).
- **Raised by:** Tyre (WallBackside), Gestalt (TileBehindState). Full team sign-off.
- **Dissent:** None.
@@ -280,7 +280,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **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.
- **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)
@@ -289,7 +289,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **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.
- **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)
@@ -300,7 +300,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **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.
- **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)
@@ -309,7 +309,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **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.
- **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)
@@ -318,7 +318,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **Date:** 2026-02-27
- **Decision:** Four height tiers: S1 (1–2 z-levels, surface + roof/mezzanine), S2 (3–10), S3 (11–30), S4 (30+). Shadow length is the primary height signal in top-down view (2–40 visual tiles). Lazy z-level loading: `ZLevelLoadState: Loaded | Skeleton | Ungenerated` — only current + adjacent z-levels filled by Phase 2. **Rooftop Bar Clause:** Every tall structure (z_band_count ≥ 3) must assign `RooftopConfig: Restricted | PublicWithHiddenLayer`. Discovery layer mandatory in both configurations. Heritage root **weights the probability** between the two configs — it does not determine the outcome. Final config is seeded per-building; a minority of buildings of any heritage root may be the non-dominant type (a Frost building with a rooftop bar must be possible). Z-band floor boundaries must have seed-variation within cultural ordering constraints. Vertical access routes are playthrough-history dependent.
- **Rationale:** Height has meaning — floor 30 has information floor 1 cannot have because it is harder to reach. Full determination of rooftop config by heritage root kills the discovery moment.
- **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)
@@ -327,7 +327,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **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.5–4KB metadata + up to 64KB ChunkData per vessel; paged by streaming model.
- **Rationale:** "The journey is content — mobile environments are social pressure cookers, not loading screens with chairs." Vessel persistence and crew state continuity make the world feel real.
- **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.
@@ -337,7 +337,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **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.
@@ -515,7 +515,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **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`.
### D-150: Character outline — inverted hull method
- **Date:** 2026-03-17
@@ -596,11 +596,11 @@ Technical foundation decisions that constrain implementation: engine, client-ser
| 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 #749 (now Phase 5) and #750 (now Phase 4) and the CLAUDE.md cascade table are updated to match.
- **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.
- **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.
@@ -722,7 +722,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **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 (#833).
- `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.
@@ -752,13 +752,13 @@ Technical foundation decisions that constrain implementation: engine, client-ser
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 (#748). Participants: Gestalt (systems design), Tyre (technical architecture), Miri (worldbuilding), with Jeroen as workshop participant.
- **Raised by:** Full planning team workshop, Sprint 34 (T-748). Participants: Gestalt (systems design), Tyre (technical architecture), Miri (worldbuilding), with Jeroen as workshop participant.
### 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 **#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 #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.
- **Decision:** Deprecate the snapshot envelope `version` field, the `PROTOCOL_VERSION` constants on both server (`server/src/bridge/types.rs`) and client (`client/scripts/protocol/protocol.gd`), and the version-mismatch guard in `Protocol.decode_snapshot()`. Removal is tracked in ticket **T-868** (server + client coordinated, sprint 37 or later). Once removed, genuine schema mismatches will surface as MessagePack decode errors or missing-field errors at the consumer; that signal is sufficient for our deployment model. Until T-868 lands, the field and guard stay in place — they are no longer load-bearing, but removing them requires coordinated edits on both sides and fresh fixture regeneration.
- **Rationale:** The version constants were designed for a network deployment where client and server can ship out of sync. Our actual deployment is a subprocess: the Godot client launches the Rust server it was built with. They are *always* in sync at runtime — the version check has never caught a real mismatch in the field, only dev-time forgetfulness. The cost has been measurable: every protocol-shaping sprint requires bumping two constants in lockstep, and we accumulated tautological tests asserting `PROTOCOL_VERSION == N` (deleted in sprint 36 — see ticket from this D-record). Removing the handshake makes the per-sprint cost zero. **Reversibility:** When/if networked multiplayer arrives (no firm date — see [D-005](#d-005-architecture-godot-client--rust-server-via-subprocess)), the natural fit is a one-time handshake at connection time (a single client-version vs. server-version exchange in the connection protocol), not a per-snapshot version stamp. So even the multiplayer path doesn't argue for keeping the per-snapshot field — that field would be doubly redundant once a connection-time check exists. The design space hasn't been narrowed.
- **What we lose:** A single eager, human-readable error at connect time ("client v22 ↔ server v23"). A genuine dev-time schema drift will now surface as a downstream decode/missing-field error, possibly seconds into a session rather than at handshake.
- **What we keep:** All field-presence and roundtrip tests in `test_protocol_bridge.gd`, `test_signal_sprint24.gd`, etc. — these cover the *behavior* the version constant was meant to gate. Decode failure in `Messagepack.decode()` still rejects malformed payloads.
@@ -778,17 +778,17 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- 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.
### D-195: Attractor-Matching Compatibility Matrix for Generative City Placement
- **Date:** 2026-05-01
- **Decision:** City placement on a planetary surface uses an attractor-matching model. A `GeographicAttractor` is a terrain feature that increases city placement score at nearby positions. Seven `AttractorType` variants: `RiverMouth`, `CoastalAccess`, `RiverCrossing`, `ValleyFloor`, `PassEntrance`, `LakeShore`, `PlainCenter`. A `CompatibilityMatrix` is a 10×7 scoring table (10 `economic_role` values × 7 attractor types) whose cells are float weights (0.0–3.0) representing how strongly that economic role favors that terrain feature. Examples: manufacturing → RiverMouth 2.8, ValleyFloor 2.1; financial → CoastalAccess 2.5, PlainCenter 1.8; agricultural → ValleyFloor 3.0, PlainCenter 2.5. The matrix is authored data (not computed at runtime). Attractor extraction from heightmaps is defined in D-209. Matching algorithm is defined in D-211.
- **Rationale:** Terrain-naive city placement produces spatially incoherent worlds. The compatibility matrix gives different city types different terrain affinities, so financial hubs appear on coasts and agricultural cities appear in fertile valleys — without hard-coding placement rules per city type. The weight matrix gives graduated preference, not binary requirement.
- **Amended 2026-06-03 (#955):** the matrix weights and the whole placement-scoring path are **integer basis-points, not f32** (D-010 determinism / D-227 save-critical). When #955 wired `match_cities` into the deterministic generation cascade, the original f32 scoring became a live cross-platform divergence risk (a near-tie score comparison or the Hungarian's f32 reductions can round differently per platform → a different world from the same seed). Now: `CompatibilityMatrix.weights` are `i32` bps (10000 = 1.0×; the examples above are 28000 / 30000 / 25000 …), `GeographicAttractor.strength` and `terrain_modification_cost` are bps, and `cell_score` / the Hungarian / `CityPlacement.score` use integer arithmetic. The 0.0–3.0 affinity semantics are unchanged; only the representation is now integer.
- **Amended 2026-06-03 (T-955):** the matrix weights and the whole placement-scoring path are **integer basis-points, not f32** (D-010 determinism / D-227 save-critical). When T-955 wired `match_cities` into the deterministic generation cascade, the original f32 scoring became a live cross-platform divergence risk (a near-tie score comparison or the Hungarian's f32 reductions can round differently per platform → a different world from the same seed). Now: `CompatibilityMatrix.weights` are `i32` bps (10000 = 1.0×; the examples above are 28000 / 30000 / 25000 …), `GeographicAttractor.strength` and `terrain_modification_cost` are bps, and `cell_score` / the Hungarian / `CityPlacement.score` use integer arithmetic. The 0.0–3.0 affinity semantics are unchanged; only the representation is now integer.
### D-196: SettlementClass Enum and Latent Settlement Active/Ghost Logic
@@ -808,8 +808,8 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- `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.
- `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:**#920 (consumer of prosperity_baseline)
- **Cross-reference:** D-194 (district mix — consumes prosperity_baseline), D-195 (attractor types that produce terrain bonus)
### D-198: Economic Simulation Independence from Layer 1–2 Spatial Data
@@ -832,8 +832,8 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **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.
- **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.
@@ -849,8 +849,8 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- Fields 1–5 are read from `systems.db` (bodies table + economics tables). Field 6 is derived at generator dispatch time.
- All 6 fields must be present before a generation task is dispatched. Missing fields abort the task with a logged error; generation does not proceed with partial context.
- **Rationale:** A fixed minimum read set prevents generators from accumulating unbounded dependencies on simulation state. The 6 fields cover the minimum information needed to produce a correctly-classified skeleton. The abort-on-missing-fields rule ensures generator output is always deterministic from a complete context, never silently degraded from a partial one.
- **Amended 2026-05-25 (D-229/D-232/D-233):**`CityGenerationContext` gains `morphology_zone`, `flavor_profile`/`architecture_flavors`, `dominant_bulk_class`, `dominant_production_ubiquity` (the record already permits >6 fields).
- **Amended 2026-05-31 ([D-237](#d-237) — authored specialization layer):** the read set gains `economic_specialization` and `cultural_specialization` (both new columns on `system_economy`). `dominant_faction` (field 4) is now sourced from authored values on `system_factions` where present, heuristic fallback otherwise. `economic_specialization` is the upstream source of `dominant_bulk_class`/`dominant_production_ubiquity` (resolved via `specialization_vocabulary`, D-233 re-amendment); `cultural_specialization` feeds the D-232 template pool. Still bounded — three authored fields, all NULL-safe with deterministic fallbacks.
@@ -858,7 +858,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
### 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 #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.)*
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:**
@@ -880,8 +880,8 @@ Technical foundation decisions that constrain implementation: engine, client-ser
}
```
- **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.
- Tiers 6–9 (District → Chunk) are the sub-settlement spatial hierarchy, canonical in [D-222](#d-222) — renamed/resized from the original D-094 ladder (the old 512m "District" is now the Quarter; District is now 2048m), at the Tile = 1m / Subtile = 0.5m scale of D-220. This decision formalizes Tiers 1–5 with equivalent lock status.
- Tier 3 heightmap resolution (512×256 equirectangular working grid; 1024×512 PNG) is the canonical format. Deviation requires amending D-191. **Amended (#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 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.
- 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.
@@ -927,16 +927,16 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- 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, [#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:
- **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 (#963):** Consumer done — `heightmap.rs::load_heightmap_png` reads the 16-bit grayscale PNG + `sea_level` tEXt chunk, rejects RGB, downsamples for Layer 1. Producer done — `import_heightmaps.py` is the bake (rename legacy `heightmap.png`→`reliefmap.png` for all bodies incl. Sol; for non-Sol inhabited bodies write a fresh clean `reliefmap.png` + 16-bit `heightmap.png` from `simulate()` at the bumped 1024×512 grid). `atlas_body_heightmaps` dropped via MIGRATION_SQL + removed from `systems-schema.sql`. Godot client (`atlas_viewer.gd`) loads `reliefmap.png` for display. Sim determinism guarded by `test_sim_determinism.py`.
- **Rationale:** Storing heightmaps in `systems.db` keeps the DB as the single source of truth for all generation inputs, avoids a separate file-fetching path in the Rust server, and allows the pre-push hook (asset pipeline rules) to detect stale heightmap data. The float32 LE layout matches what NumPy and PIL produce natively, minimizing conversion overhead in the Python pipeline. *(Superseded by the #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.)*
- **Implementation status (T-963):** Consumer done — `heightmap.rs::load_heightmap_png` reads the 16-bit grayscale PNG + `sea_level` tEXt chunk, rejects RGB, downsamples for Layer 1. Producer done — `import_heightmaps.py` is the bake (rename legacy `heightmap.png`→`reliefmap.png` for all bodies incl. Sol; for non-Sol inhabited bodies write a fresh clean `reliefmap.png` + 16-bit `heightmap.png` from `simulate()` at the bumped 1024×512 grid). `atlas_body_heightmaps` dropped via MIGRATION_SQL + removed from `systems-schema.sql`. Godot client (`atlas_viewer.gd`) loads `reliefmap.png` for display. Sim determinism guarded by `test_sim_determinism.py`.
- **Rationale:** Storing heightmaps in `systems.db` keeps the DB as the single source of truth for all generation inputs, avoids a separate file-fetching path in the Rust server, and allows the pre-push hook (asset pipeline rules) to detect stale heightmap data. The float32 LE layout matches what NumPy and PIL produce natively, minimizing conversion overhead in the Python pipeline. *(Superseded by the T-963 amendment above — the file-based model won out because the DB-as-single-source goal conflicts with binary-merge-conflict avoidance and DB size; a per-body committed PNG is itself a queryable, diffable-by-render asset.)*
### D-203: BodyWorldState Bevy Resource with LRU Cache
@@ -959,8 +959,8 @@ Technical foundation decisions that constrain implementation: engine, client-ser
```
- 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.
@@ -982,8 +982,8 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- `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:**#905 (schema), #910 (populate from planet_class fallback)
### D-205: Province Boundary Pre-Computation (atlas_province_boundaries)
@@ -1004,8 +1004,8 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **Province count target:** 4–12 provinces per inhabited body, derived naturally from watershed analysis. Bodies with less topographic relief (plains worlds, ocean worlds) produce fewer, larger provinces.
- **At runtime:** Province boundaries are read from `atlas_province_boundaries` at generation dispatch time and cached in `BodyWorldState` as `drainage_basins` (D-203). They are not re-computed at runtime.
- **Rationale:** Province boundaries define the cultural geography of a world — the mountain ranges and river systems that separated civilizations and produced distinct regional identities. Pre-computing them at build time keeps the runtime-background tier focused on city placement and district generation rather than watershed analysis. Storing as polylines (not rasterized masks) keeps the table compact and human-readable.
- **Ticket:**#904 (schema), #907 (populate from watershed analysis)
### D-206: Background Generation Priority Queue and Rayon Thread Infrastructure
@@ -1017,8 +1017,8 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **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.
- **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
@@ -1042,15 +1042,15 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- `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.
- **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 #963).
- **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
@@ -1059,8 +1059,8 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **Performance target:** ~50ms per body on a single Rayon thread for canonical 512×256 resolution.
- **Determinism:** Integer-only arithmetic throughout. No f32 in the priority-flood comparisons (use integer-scaled elevation). D-010 compliant.
- **Rationale:** D8 is the standard GIS drainage routing algorithm and produces the river networks that drive attractor scoring (river mouths, confluences = high-value `RiverMouth` attractors). The flow accumulation threshold of 200 was chosen empirically against the Lendel heightmap to produce ~8–15 named rivers per inhabited body — enough for cultural geography without over-fragmenting the landscape.
- **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)
@@ -1076,8 +1076,8 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- `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:**#925 (types), #919 (matching pipeline that consumes these)
### D-210: Sub-Biome Variant Classification and terrain_modification_cost
@@ -1088,8 +1088,8 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- 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.
- **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
@@ -1103,8 +1103,8 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **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.
@@ -1124,11 +1124,11 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- `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 (#956 — implementation):** two changes from the original spec.
- **Amended 2026-06-05 (T-956 — implementation):** two changes from the original spec.
1. **New variant `AutonomistHeld`** added between `FrontierUnclaimed` and `IndigenousHeld`. The original four control buckets (Commission/Corp/Contested/Frontier) predate the richer faction canon (`wiki/factions/`): the **Compact of Westphalia** is a self-governing autonomist bloc that rejects Concord Assembly authority — it governs its systems firmly, so it is neither `CommissionControlled` (it is the Assembly's *rival*), nor `FrontierUnclaimed` (it is *not* ungoverned), nor locally `ContestedZone` (the Compact is locally dominant). `AutonomistHeld` is its bucket, and gives the Compact its own colour on the political-zones overlay.
2. **Derivation source.** The numeric per-faction `faction_influence` thresholds the original record specifies are **not present in the data** — only a single authored `dominant_faction` per system exists (D-237's 8-value vocabulary). So the implementation maps `dominant_faction → TerritorialStatus` instead (`attractor_matching::territorial_status_from_faction`), grounded in faction canon: `concord_assembly`/`veil_institute` → `CommissionControlled` (the Assembly is the Reach's central government; the Veil Institute is Assembly-funded and -aligned); `syndic_dominant` → `CorpTerritory`; `compact`/`compact_sympathetic` → `AutonomistHeld`; `disputed`/`mixed` → `ContestedZone`; `independent`/NULL/unknown → `FrontierUnclaimed`. `IndigenousHeld` and `Derelict` remain unreachable from `dominant_faction` alone (they need the cultural-corridor autonomy flag / population density) — deferred. `dominant_faction` is system-level, so status is uniform across a body's provinces for now; it is still stored per-basin on `DrainageBasin.territorial_status` (forward-compatible for per-province faction data). `placed_at_generation` is not yet modelled (no runtime re-classification exists yet).
- **Cross-reference:** D-191 (atlas overlay — political zones), D-199 (economic read set), D-205 (Province — this status is a field on it), [D-237](#d-237) (authored `dominant_faction` source), D-214 (PoliticalArchetype — consumes this)
### D-213: FoundingOrientation Enum and Spatial Grid Rotation
@@ -1148,8 +1148,8 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- 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.
### D-214: PoliticalArchetype Enum and Settlement Spatial Character
@@ -1171,9 +1171,9 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **`AttractorAssignment` disambiguation:** `OrganicGrowth` (a `DistrictType` value and also an `EraCause` value) is always unambiguous in context. On `DistrictType`, it means the district grew without a planning mandate. As `EraCause`, it means the era tag was acquired through organic settlement expansion rather than a discrete historical event. Both usages are permitted; the type system distinguishes them.
- **Rationale:** Power structure should be legible in a city's spatial form without the player reading a wiki entry. Commission cities look different from Corporate cities look different from Pioneer cities — not just in palette, but in street geometry, district type distribution, and building scale. Encoding this as a named enum ensures the distinction is consistent across all generation code.
- **Amended 2026-05-31 ([D-237](#d-237) — authored specialization layer):** for named systems `dominant_faction` (a D-214 derivation input via `TerritorialStatus`/D-199) is now an **authored** value (8-value vocabulary: `concord_assembly | compact | compact_sympathetic | syndic_dominant | veil_institute | independent | disputed | mixed`) rather than one the heuristic guesses from hop-distance/currency; the existing derivation remains the fallback for unauthored systems. The archetype mapping itself is unchanged — it now reads a more trustworthy faction for the ~40–60 named systems where the heuristic was demonstrably wrong (e.g. Groombridge resolves `syndic_dominant` → Corporate, not the hop-2 default that would yield Commission). `lattice_commission` is deliberately **not** a faction value — the Commission is a regulator, not a governing faction (ACB and Bastion are `concord_assembly`).
- **Implemented 2026-06-05 (#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 (#957).
- **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).
### D-215: Five Explicit Political Archetype Spatial Arrangement Patterns
@@ -1187,9 +1187,9 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- The arrangement pattern constrains block adjacency during Phase 1 skeleton generation. Specifically: the first 2–3 districts placed in a settlement follow the pattern. Later districts are constrained only by the road network, not by the pattern.
- Arrangement patterns must **vary in angular orientation** per seed (not just position) — the same archetype's radial core must not always face the same direction across seeds.
- **Rationale:** The 14 D-ready items from the generator-architecture workshop established that spatial arrangement should encode power structure. These five patterns are the minimal set to cover the 6 archetypes (Pioneer and Industrial share ribbon development; hub-and-spoke is a cross-archetype pattern for transit-primary cities). Pattern variation in angular orientation prevents players from pattern-matching settlement layout after the first playthrough.
- **Implemented 2026-06-05 (#956):** the `ArrangementPattern` enum (the five patterns) and its derivation (`attractor_matching::arrangement_pattern`, from `PoliticalArchetype` + transit_hub override) land here, and the chosen pattern is **stored** per settlement on `CityPlacement.arrangement_pattern`. The block-adjacency **enforcement** (constraining the first 2–3 quarters' layout) is the Quarter-skeleton generator's job and is deferred to #957; the per-seed angular variation rides on `FoundingOrientation` (D-213, seed-derived `Free` bearing).
- **Implemented 2026-06-05 (T-956):** the `ArrangementPattern` enum (the five patterns) and its derivation (`attractor_matching::arrangement_pattern`, from `PoliticalArchetype` + transit_hub override) land here, and the chosen pattern is **stored** per settlement on `CityPlacement.arrangement_pattern`. The block-adjacency **enforcement** (constraining the first 2–3 quarters' layout) is the Quarter-skeleton generator's job and is deferred to T-957; the per-seed angular variation rides on `FoundingOrientation` (D-213, seed-derived `Free` bearing).
### D-216: BlockIrregularity from founding_age — Layout Age Character
@@ -1210,10 +1210,10 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- 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.
- **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 #1000):**`founding_age_years` is backfilled on every inhabited body in `tooling/economy-db/import_economics.py` MIGRATION_SQL via `COALESCE(events-first, wave-fallback)`. **Events-first:** the system's authored `historical_events.age_years` for `event_type = 'colonial_charter'` — the canonical founding event authored during the economics built-world workshop. 9 systems in the live DB carry a colonial_charter row, driving 11 inhabited body values; 5 of those diverge meaningfully from their wave fallback (e.g. GJ 144 = 580 vs wave-1 fallback 600, GJ 338B = 590, GJ 380 = 480). **Wave-fallback** — canonical **founding-edge** of each wave's range (`docs/design/systems-framework-final-miri.md:495–499`):
- **Amendment (2026-05-26 — `founding_age_years` backfill, ticket T-1000):**`founding_age_years` is backfilled on every inhabited body in `tooling/economy-db/import_economics.py` MIGRATION_SQL via `COALESCE(events-first, wave-fallback)`. **Events-first:** the system's authored `historical_events.age_years` for `event_type = 'colonial_charter'` — the canonical founding event authored during the economics built-world workshop. 9 systems in the live DB carry a colonial_charter row, driving 11 inhabited body values; 5 of those diverge meaningfully from their wave fallback (e.g. GJ 144 = 580 vs wave-1 fallback 600, GJ 338B = 590, GJ 380 = 480). **Wave-fallback** — canonical **founding-edge** of each wave's range (`docs/design/systems-framework-final-miri.md:495–499`):
| `settlement_wave` | era | range (years ago) | founding-edge fallback |
|---|---|---|---|
@@ -1243,8 +1243,8 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **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.
@@ -1269,8 +1269,8 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **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.
- **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)
---
@@ -1460,8 +1460,8 @@ Technical foundation decisions that constrain implementation: engine, client-ser
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 #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 (#951, 2026-05-22): done.** All 2,398 `markers.json` reduced to a names-only pool (25,264 names; 349 city names across 271 bodies). The Python atlas **geometry generator** (`generate_atlas.py`) and the **LLM naming cluster** (`gemma_naming.py`, `naming_core.py`, `apply_name_fixes.py`, their tests/QA, the `fix_fewshot_bleed`/`prune_atlas_features` geometry tools, the redundant `import_city_names.py`, and the `run-atlas-naming.sh` runner) were retired — the procedural server cascade (Phase 4) supersedes them. The Gemma prompting methodology is preserved in [docs/gemma-naming-methodology.md](../docs/gemma-naming-methodology.md). Shared atlas-DB utilities moved to `tooling/planet-gen/atlas_common.py`; **`import_economics.py` is now the sole regen-db generator that owns the atlas index** — it loads the names pool into `atlas_city_names` and empties the 8 geometry tables (`atlas_cities/roads/railroads/pois/rivers/oceans/mountain_ranges/body_grids`), which the cascade fills. `population`/`kind`/`settlement_class` on `atlas_city_names` are **deferred to placement (#955)** — `attractor_matching` reads them at 0/default until then; that empty state is the gap the cascade closes. The `reserved=1` corp-HQ cross-reference stays (corp HQ names are real places). 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.
- **Implementation:** tracked under Phase 4 (epic T-750) — strip markers to names, remove template machinery + `reserved` pinning, update the atlas pipeline/schema, regen. Historical archives (sprints, workshops, audits, CHANGELOG, atlas proposals) are left untouched.
- **Implementation status (T-951, 2026-05-22): done.** All 2,398 `markers.json` reduced to a names-only pool (25,264 names; 349 city names across 271 bodies). The Python atlas **geometry generator** (`generate_atlas.py`) and the **LLM naming cluster** (`gemma_naming.py`, `naming_core.py`, `apply_name_fixes.py`, their tests/QA, the `fix_fewshot_bleed`/`prune_atlas_features` geometry tools, the redundant `import_city_names.py`, and the `run-atlas-naming.sh` runner) were retired — the procedural server cascade (Phase 4) supersedes them. The Gemma prompting methodology is preserved in [docs/gemma-naming-methodology.md](../docs/gemma-naming-methodology.md). Shared atlas-DB utilities moved to `tooling/planet-gen/atlas_common.py`; **`import_economics.py` is now the sole regen-db generator that owns the atlas index** — it loads the names pool into `atlas_city_names` and empties the 8 geometry tables (`atlas_cities/roads/railroads/pois/rivers/oceans/mountain_ranges/body_grids`), which the cascade fills. `population`/`kind`/`settlement_class` on `atlas_city_names` are **deferred to placement (T-955)** — `attractor_matching` reads them at 0/default until then; that empty state is the gap the cascade closes. The `reserved=1` corp-HQ cross-reference stays (corp HQ names are real places). A latent duplicate-accumulation bug in name population (no clear + no unique constraint) was fixed with a deterministic rebuild. **Sol (system `GJ 0`) is permanently exempt from the normal generators:** it uses real Earth/Mars/Luna geography via the offline `sol_import.py` (left in place for future scripted integration), so its bodies keep geometry-bearing `markers.json` as preserved positional config and are skipped by the names-pool importer — Sol names will come from its own integration, not the cascade. `make regen-db` green.
- **Raised by:** Jeroen, 2026-05-22 — resolving the open question on merging preconfigured content into the deterministic cascade.
- **Cross-reference:** [D-207](#d-207-fully-generative-placement--markersjson-stripped-to-topographic-features) (superseded — names-only, no reserved pinning), [D-191](#d-191) §8 (markers format — names-only), [D-208](#d-208) (drainage → river courses), [D-211](#d-211) (settlement placement), [D-199](#d-199) (economic read set), [D-222](#d-222) (lore≠code names on generated geometry)
- **Dissent:** None
@@ -1489,17 +1489,17 @@ Technical foundation decisions that constrain implementation: engine, client-ser
**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 #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.
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 (#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 #952 Layer 0→1 golden fixtures are therefore SeedChain-independent and can be captured in any order relative to the SeedChain work.
**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 (#952) a stable contract to verify against. Promoting one mixer prevents two divergent implementations.
- **Implementation:**#952 (Phase 4, epic #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.
- **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.
- **Decision:** How per-body generation-cascade layer data reaches the Godot client for the Phase-4 Atlas progress viewer (#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:
- **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.
@@ -1519,11 +1519,11 @@ Technical foundation decisions that constrain implementation: engine, client-ser
**(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 #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 #960.
- **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 #960.
- **Implementation:**#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 #960, 2026-05-23.
- **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.
- **Cross-reference:** Q-098 (resolved by this), Q-099 (mod content catalog — spun off), [D-191](#d-191) (Atlas viewer), [D-166](#d-166) (per-layer Atlas progress viewer), [D-200](#d-200) / [D-203](#d-203) (three-tier execution, LRU cache), [D-005](#d-005) / [D-192](#d-192) (client+server co-ship — no version handshake), [D-224](#d-224) (SeedChain — feeds the cascade the proxy runs)
- **Dissent:** None
@@ -1537,7 +1537,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
**(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 (#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).
**(3) Human-visual viewer (T-960).** The Atlas renders cascade layers as **additive, toggleable overlays** (relief base + rivers + drainage basins + attractors; *shape* encodes attractor type, *color* encodes sub-biome), extending the existing `OVERLAY_DEFS` / `AtlasOverlayBar` with a `generation` overlay group + a left-side legend panel. Buttons start `pending`/locked and unlock as each layer's data arrives ("grows as each layer lands", D-166).
**(4) Agent-navigable channel.** A client-side `AtlasAgentInterface` exposing JSON `observe` (current data state + a walkable **UI affordance tree**) and `act` (named **semantic intents** — `select_body`, `open_regional`, `set_overlay`, `back`, … — backed by the same handlers a click calls, **not** pixel coordinates; the map uses `_gui_input`). Runs headless. Turns human-eyeball review into an agent-automatable QA sweep across the whole Reach (and later economics/saves).
@@ -1547,7 +1547,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **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 #750), built bottom-up — auto-pause substrate, #969 proxy (D-225), #960 viewer, agent channel, agent capture. Geography is the first consumer.
- **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.
@@ -1565,7 +1565,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **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 #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).
- **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).
@@ -1584,7 +1584,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **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.)
- **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 #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.
- **Implementation:** Phase 4+ (epic T-750), as the cascade reaches the tile layers. Today's `TileKind` (D-049 render stack) is the seed of the per-subtile axes; `SubBiomeVariant` (D-210) is the region biome axis.
- **Raised by:** Jeroen (FloorMaterial, shape-from-material, cohesion-matrix directives) + Claude, with the atlas-derivation workshop four — Tyre (composite data-structure), Gestalt (tag model + tactical form), Nigel (morphology-as-character), Burnelli (economic distinctions), 2026-05-25.
- **Cross-reference:** [D-227](#d-227) (derive-don't-store — these axes are the derived value type), [D-010](#d-010), [D-210](#d-210) (sub-biome — region biome axis), [D-222](#d-222) (subtile/tile), [D-049](#d-049) (z-stack render — `TileKind` seed), [D-208](#d-208) (D8 — flow-direction source), [D-226](#d-226) (dynamic-state inspection — the water overlay), [Q-100](../questions/architecture.md#q-100) (biome authority), [Q-101](../questions/architecture.md#q-101) (refinement contract), [Q-102](../questions/architecture.md#q-102) (cohesion-matrix algorithm), [Q-105](../questions/architecture.md#q-105) (dynamic water-height)
- **Dissent:** None
@@ -1604,10 +1604,10 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **`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 (#957 — authored zone-type selection table):** the `(ZoningType + economic_role + seed) → ZoneTypeId` lookup is made concrete for the **planetary** cascade. A third input, **`setting` (SettingType)**, is added as a *tweaker* (not a surface/station switch — station & orbital bodies run a separate cascade per [Q-109](../questions/architecture.md#q-109) and own the station-only ids `residential_station`/`extraction_space`/`port_space`/`rural_orbital`, which this table never selects). `zone_type_for(zoning, role, setting, seed)` builds a candidate slice from the base table below, applies the setting tweaker, then deterministically seed-picks one id. Every base cell is non-empty (no empty slices — mirrors D-195's no-zero rule); an unknown role falls back to the ZoningType default.
- **Amended 2026-06-05 (T-957 — authored zone-type selection table):** the `(ZoningType + economic_role + seed) → ZoneTypeId` lookup is made concrete for the **planetary** cascade. A third input, **`setting` (SettingType)**, is added as a *tweaker* (not a surface/station switch — station & orbital bodies run a separate cascade per [Q-109](../questions/architecture.md#q-109) and own the station-only ids `residential_station`/`extraction_space`/`port_space`/`rural_orbital`, which this table never selects). `zone_type_for(zoning, role, setting, seed)` builds a candidate slice from the base table below, applies the setting tweaker, then deterministically seed-picks one id. Every base cell is non-empty (no empty slices — mirrors D-195's no-zero rule); an unknown role falls back to the ZoningType default.
- **`setting` tweaker (post-pass):** Maritime/Water → `port_surface`⇒`port_maritime` (+`port_fishing` for Transit), `rural_*`⇒`rural_aquaculture`, `extraction_surface`⇒`extraction_platform`. Agricultural → bias `rural_agricultural`/`rural_pastoral` into Residential/Mixed. Wilderness → surface `wilderness_frontier`/`residential_dispersed` at the frontier. Urban/other → base unchanged.
- **Entry-class U-curve threshold (gap fill):** the "low prosperity on a Commercial zone → BreachOnly" degrade uses the D-217 bands — `prosperity_baseline_bps < 2300` (the D-217 Broken band) → `BreachOnly`. **Era** follows this record as written (`founding_age_years + prosperity_baseline + seed`); the round-3 "distance-to-origin" note is dropped (not in the data model). **FloorExtent** uses the D-220 density-class floor midpoints with seed-jitter within the class range. **Doors** are **not** populated here — `doors: Vec::new()`; door derivation (D-231) is #979.
- **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.
@@ -1693,7 +1693,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **Refined 2026-05-26 (street-network algorithm — two layers):** streets generate in **two layers**, mirroring real cities and the Civ road lineage. **Arterials** (`corridors`) = a **minimum-spanning / least-cost graph** over the district's key nodes (access-points, reservations, landmarks) — emergent trunk topology (Civ6-style, "roads where traffic wants to go"), ±45°-snapped; **this is the node/edge graph the guarantee audit (D-097) reads** (a chokepoint *is* an arterial bottleneck; an encounter-corridor *is* an arterial through-route). Ribbon = MST on linearly-constrained nodes; hub-and-spoke = MST with a forced centroid hub. **Local streets** (`chunk_layout`) = a ±45° **grid lattice** within each block (Civ4-style), modulated by the D-096 Grid/Organic mode (offsets + rotation). **Voronoi was rejected** — its arbitrary-angle edges violate the ±45° cap and its irregular cells break the axis-aligned `TileRect` footprint fast-path (D-229), degrading per-chunk fill from rectangle-containment to polygon rasterization. (Civ1–4's "road on every tile" is the anti-pattern this avoids; the arterial/local split is the city-scale fix.)
- **Rationale:** Morphology zone and `architecture_flavor` were added to the context (D-199) as *inputs without consumers* — two economically-similar bodies otherwise produce topologically identical street networks and identical block subdivision (Nigel's same-y-grid failure). These two rules give morphology structural teeth so the fjord port and the delta port are categorically different cities, not reskins.
- **Implementation:** Phase 4+. Consumed in the plan phase (D-230) when laying streets and subdividing blocks.
- **Implemented 2026-06-06 (#957):** the street network + footprint subdivision land in `skeleton_gen.rs` (absorbing #976 into #957 — one coherent walkable-quarter deliverable). `AccessPoint`/`CorridorSpine`/`ChunkLayout` are now typed structs (were `String` stubs): access nodes from road-entry octants + reservation gates; arterial corridors as a morphology-gated graph (Ribbon for fjord/canyon/mountain-pass, HubSpoke for delta/island/enclosed water, Prim-MST mesh otherwise), ±45°-snapped (a); per-block local lattice modulated by D-096 Grid/Organic. Footprint subdivision is a density-scaled BSP into axis-aligned `TileRect`s with D-233 BulkClass roofed-coverage. **Waterfront rule (b):** the water-facing quarter edge (from the settlement's `Coastal` founding orientation, D-213) drops its street setback to 0 so buildings present flush to the quay. The water bearing itself is now extracted in Layer 1 (`TerrainAnalysis::water_bearing`, 8-octant integer) and fed into D-213 founding orientation (#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.
- **Implemented 2026-06-06 (T-957):** the street network + footprint subdivision land in `skeleton_gen.rs` (absorbing T-976 into T-957 — one coherent walkable-quarter deliverable). `AccessPoint`/`CorridorSpine`/`ChunkLayout` are now typed structs (were `String` stubs): access nodes from road-entry octants + reservation gates; arterial corridors as a morphology-gated graph (Ribbon for fjord/canyon/mountain-pass, HubSpoke for delta/island/enclosed water, Prim-MST mesh otherwise), ±45°-snapped (a); per-block local lattice modulated by D-096 Grid/Organic. Footprint subdivision is a density-scaled BSP into axis-aligned `TileRect`s with D-233 BulkClass roofed-coverage. **Waterfront rule (b):** the water-facing quarter edge (from the settlement's `Coastal` founding orientation, D-213) drops its street setback to 0 so buildings present flush to the quay. The water bearing itself is now extracted in Layer 1 (`TerrainAnalysis::water_bearing`, 8-octant integer) and fed into D-213 founding orientation (T-956's `0` stub is gone for coastal/river settlements). **Pending dependency:** the waterfront rule reads `CityGenerationContext.founding_orientation`, which `city_context_reader` still stubs to `Cardinal` — real per-settlement orientation only reaches quarter generation once the **Layer-3 placement → Layer-4 `GenerateSkeleton` dispatch** is wired (it must copy the placement's founding orientation into the context). That cross-layer dispatch is the remaining integration; the rule is correct and tested, awaiting its input pipeline.
@@ -1721,7 +1721,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
**(1) `system_economy.economic_specialization TEXT` (new column).** A 27-value curated vocabulary compiled from `wiki/economics/specialization_vocabulary.toml` into a `specialization_vocabulary` DB table. Each value maps to `(commodity_id, production_ubiquity_override_or_null)` → `(BulkClass, ProductionUbiquity)` for D-233. Scale is encoded in the value (no separate scale column): `breadbasket` pins `ProductionUbiquity = Specialist` where the catalog default would be `Ubiquitous`; `terroir_agriculture` pins `MonopolySource`. Equal-or-higher rule: a `production_ubiquity_override` may only be equal or higher concentration than the commodity's global default (CI hard error V-SES-03). Authored for ~80–100 named systems; deterministic-varied heuristic fallback for ~200 unnamed systems (seed-hashed weighted draw over the vocabulary: W_ROLE_PRIMARY=8,000 bps, W_CORP=5,000 bps, W_NOISE=400 bps to every vocabulary value; same seed+system_id always produces the same value; the noise term ensures ~5–10% of unnamed systems draw an off-primary value, preventing all unnamed agricultural worlds from being identical).
**Relationship to `economic_base_primary` (the seam — clarified 2026-06-01):**`system_economy.economic_base_primary`/`_secondary` is pre-existing free-text prose (e.g. `"fusion_fuel, financial_services"`) rendered into the wiki index.md "Industries/Exports" infobox by `wiki_sync.py`. `economic_specialization` is a **deliberate parallel representation at higher fidelity**, NOT a duplicate: the prose is uncomputable GTTR flavor for human readers; the enum is the machine-actionable projection the D-233 generator consumes. They are the same *concept* (what the system produces) at two resolutions. The two must stay mutually consistent — `economic_specialization` should never contradict the `economic_base_primary` prose for the same system (the #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.
**Relationship to `economic_base_primary` (the seam — clarified 2026-06-01):**`system_economy.economic_base_primary`/`_secondary` is pre-existing free-text prose (e.g. `"fusion_fuel, financial_services"`) rendered into the wiki index.md "Industries/Exports" infobox by `wiki_sync.py`. `economic_specialization` is a **deliberate parallel representation at higher fidelity**, NOT a duplicate: the prose is uncomputable GTTR flavor for human readers; the enum is the machine-actionable projection the D-233 generator consumes. They are the same *concept* (what the system produces) at two resolutions. The two must stay mutually consistent — `economic_specialization` should never contradict the `economic_base_primary` prose for the same system (the T-1016 content pass authors the enum from the same lore the prose describes, and CI soft-warning W-SES-03 flags prose/enum divergence). Unification onto one source was considered and rejected: the prose carries multi-sector nuance and narrative voice the enum cannot, and the enum carries the deterministic (BulkClass × ProductionUbiquity) projection the prose cannot — collapsing either direction loses information. systems.db is canonical; index.md infobox is a read-only DB projection; only the index.md *prose sections* (Supply Dependency / Faction Notes / …, round-tripped by `wiki_sync``PROSE_SECTIONS`) are authored in-place.
**(2) `system_economy.cultural_specialization TEXT` (new column).** Directs D-232's template pool when a system's cultural character diverges from corridor baseline. Single field carrying two value sub-types: *activity/character values* (`agrarian`, `industrial_heritage`, `institutional`, `scholarly`, `artistic`, `financial_technocratic`, `cosmopolitan`, `compact_cooperative`, etc.) and *heritage-type values* (`scottish`, `vietnamese`, `zulu`, `afrikaans_cape`, `tagalog`, `chinese`, `italian_northern`, `french_provencal`, `norse_compact`, etc.). Heritage values take precedence when both apply. `NULL` = use corridor default (D-232 existing algorithm). Authored for ~60–100 systems where the corridor default would produce wrong D-232 architectural draws in Phase 4 (not a thin Phase 6 concern: a Vietnamese-founded system in the east_reach Korean/Japanese corridor draws the wrong templates without an explicit pin). Does NOT replace `system_culture.cultural_register` (prose NPC/dialogue voice); consumed by the physical generator only.
@@ -46,7 +46,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
### D-032: Separate monologue pools per character [SUPERSEDED — deferred to Phase 6]
- **Date:** 2026-02-11
- **Superseded by:** Development cascade (CLAUDE.md) — character/NPC monologue content is Phase 6 detail-coloring, below the current Phase 1 (wiki content). The smuggler/detective archetype enum and its hard-partitioned monologue pools were pre-cascade scaffolding and have been fully stripped from the server codebase (Sprint 37, #878, PR #137). The original D-117 supersession framing (single tycoon character in v0.2) is itself obsolete now that v0.2 is dropped (CLAUDE.md: "v0.2 target is dropped"). The partition *design pattern* is preserved in this record for when culture-driven / generator-produced monologue is reintroduced in Phase 6, but no corresponding code or content exists today.
- **Superseded by:** Development cascade (CLAUDE.md) — character/NPC monologue content is Phase 6 detail-coloring, below the current Phase 1 (wiki content). The smuggler/detective archetype enum and its hard-partitioned monologue pools were pre-cascade scaffolding and have been fully stripped from the server codebase (Sprint 37, T-878, PR #137). The original D-117 supersession framing (single tycoon character in v0.2) is itself obsolete now that v0.2 is dropped (CLAUDE.md: "v0.2 target is dropped"). The partition *design pattern* is preserved in this record for when culture-driven / generator-produced monologue is reintroduced in Phase 6, but no corresponding code or content exists today.
- **Amendment (2026-04-22, Sprint 37, PR #137):** Monologue pool selection is now unkeyed by archetype until a Phase 6 character system exists. `MonologueState.character` field and `CharacterArchetype` enum deleted from `server/src/bridge/types.rs`, `server/src/simulation/monologue.rs`, observer relabeling logic, and `server/content/modules/tier1/smuggling_ring_v0_1.yaml`. Uniform single-pool behavior is the intended end state pre-Phase-6 — not scaffolding deferred in place with a stub, but retired pending the real character system. Lead override recorded in `docs/architecture/sprint-37-878-audit.md` (2026-04-21 override section). Reintroduction gate: a confirmed Phase 6 character-model design is a prerequisite before this decision is revived.
- **Decision:** Internal monologue content is hard-partitioned by playable character. The smuggler and detective have completely separate monologue pools — no shared lines. The `character` tag on monologue lines is a hard partition, not a filter. File structure uses separate files per character per location (e.g., `monologue-smuggler.yaml`, `monologue-detective.yaml`).
- **Rationale:** Shared monologue would dilute character voice and undermine the dual-lens experience. Each character's internal voice must be independently coherent. Same trigger, different pool — this is how mirror moments work without either pool knowing about the other.
@@ -56,13 +56,13 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
### D-034: THE FRIEND — production-level NPC pattern
- **Date:** 2026-02-11
- **Decision:** Each playable character has one "FRIEND" NPC — a production-level complex character that exercises every content and systems pipeline at full depth. THE FRIEND is the emotional centerpiece of v0.1 and carries D-027 success criterion #3 ("player names an NPC they felt conflicted about"). THE FRIEND follows a reusable pattern: 3+ relationship phases (warmth → trust → doubt → conflict), observable contradiction discoverable through observation not dialogue, sympathetic motivation, no clean resolution, dual-lens resonance, triangle integration, tell progression, and dialogue shift pre/post discovery.
- **Decision:** Each playable character has one "FRIEND" NPC — a production-level complex character that exercises every content and systems pipeline at full depth. THE FRIEND is the emotional centerpiece of v0.1 and carries D-027 success criterion T-3 ("player names an NPC they felt conflicted about"). THE FRIEND follows a reusable pattern: 3+ relationship phases (warmth → trust → doubt → conflict), observable contradiction discoverable through observation not dialogue, sympathetic motivation, no clean resolution, dual-lens resonance, triangle integration, tell progression, and dialogue shift pre/post discovery.
- **Assignments:**
- **Smuggler's FRIEND: Kael Davan** — dock worker, ring member, smuggler's closest colleague. Contradiction: meeting with unknown contact in restricted corridor (trying to exit the ring to protect partner Naia). Exercises: full 10-axis model, multi-phase dialogue, tell system, named monologue, dual-lens notes.
- **Detective's FRIEND: Sera Venn** — Commission field tech, bar regular, detective's social anchor. Contradiction: avoids Torek Lintar (sitting on unreported evidence about Kael's manifest discrepancies, protecting friend Naia). Exercises: institutional access + personal loyalty conflict, avoidance pattern as tell, trust-contamination arc.
- **Content requirements per FRIEND NPC:** ~70-100 authored lines (25-35 dialogue, 15-20 trust-gated, 5-8 unprompted, 10-15 monologue per character, 5-8 tell observation, 3-5 contradiction discovery). No generation expansion — all hand-authored.
- **Cross-reference:** NPC triangle model ([D-024](#d-024-npc-generation-model--10-axes--combat-component)), relationship web ([D-029](#d-029-population-entanglement-ratio--305020)), vertical slice criteria ([D-027](scope.md#d-027-vertical-slice--smuggler--detective-two-character-proof))
- **Raised by:** Ozzie (emotional concept, Round 1), Paula (structural design and both FRIEND profiles, Round 2), project lead (confirmed, directive #4). Sera Venn confirmed by project lead over Mellanie's alternative proposal (Lera Sessik).
- **Raised by:** Ozzie (emotional concept, Round 1), Paula (structural design and both FRIEND profiles, Round 2), project lead (confirmed, directive T-4). Sera Venn confirmed by project lead over Mellanie's alternative proposal (Lera Sessik).
- **Dissent:** Mellanie proposed Lera Sessik (bar owner) as detective's FRIEND. Project lead selected Paula's Sera Venn design. Lera remains bar owner / mundane triangle member.
- **Amendment (2026-03-05, Where's the Fun? Workshop):** The FRIEND pattern (3+ relationship phases, observable contradiction, sympathetic motivation, no clean resolution, tell progression, dual-lens resonance) survives as a generator template for v0.2. Kael Davan and Sera Venn do not exist — [D-122](#d-122-all-npcs-generated--no-named-hand-authored-characters) eliminates all named hand-authored NPCs. In v0.2, the FRIEND role is filled by a generated NPC whose generator profile matches the FRIEND pattern template. The FRIEND pattern is now a generator instruction set, not an authoring assignment. Workshop convergence note: warmth with generated NPCs is earned through observed relationship progression, not authored backstory — this may produce stronger emotional investment than the hand-authored approach (Paula, Mellanie in Where's the Fun? Workshop §Phase Zero).
@@ -81,7 +81,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
-`tags` (list\<string\>): freeform escape hatch for author intent
- **Monologue-specific additions:**
-`character` (enum): historically `smuggler`, `detective` — removed. Per [D-032 SUPERSEDED] and the development cascade (CLAUDE.md), character-partitioned monologue is Phase 6 and has been stripped from the codebase (Sprint 37, #878). Field is unused; do not reintroduce without a confirmed Phase 6 design.
-`character` (enum): historically `smuggler`, `detective` — removed. Per [D-032 SUPERSEDED] and the development cascade (CLAUDE.md), character-partitioned monologue is Phase 6 and has been stripped from the codebase (Sprint 37, T-878). Field is unused; do not reintroduce without a confirmed Phase 6 design.
@@ -93,7 +93,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Amendment (Sprint 14):** Mood vocabulary renamed to match voice guide (monologue-voice-guide.md). Old → new: `fond`→`warm`, `comfortable`→`content`, `worried`→`anxious`, `concerned`→`frustrated`. Dropped: `analytical` (merged into `focused`), `conflicted` (modeled as `suspicious`+`warm` collision). Added: `hostile`. Final 8 moods: `anxious`, `frustrated`, `content`, `suspicious`, `warm`, `hostile`, `relieved`, `focused`. Neutral = untagged.
- **Amendment (Sprint 24):** `triangle_activated` added as 15th situation (fires post-TriangleActivated when player observes anchor NPCs). Two freeform tags registered as conventions: `triangle-signal` (line is part of the triangle activation sequence) and `tell-observation` (line observes a behavioral tell without naming its cause). `npc_in_los` prerequisite added for LOS-gated monologue lines. Schema updated to match.
- **Amendment (Sprint 15):** Line ID namespace changed from location-scoped to NPC-scoped. Old scheme: `{location_slug}_{d|m}_{###}` (e.g., `the-terminal_d_039`) — all NPCs at a location share one ID sequence, requiring cross-file coordination and causing collisions at scale. New scheme: `{npc-slug}_{d|m}_{###}` for dialogue, `{npc-slug}_m_{s|d}_{###}` for monologue (e.g., `kael-davan_d_001`, `dock-worker_d_001`). Each NPC's IDs are independent — no cross-file coordination needed. Auto-generated NPCs use their generated slug. Schema regex patterns unchanged (prefix is still `^[a-z][a-z0-9-]*`), only the `description` field and convention documentation update. Migration: mechanical rename of all existing line IDs across ~20 dialogue files and monologue pools.
- **Amendment (2026-04-22, Sprint 37, PR #137):** The `character` monologue-specific tag (historically `smuggler | detective`) is retired pending a Phase 6 character-model design — not deferred in place with a stub enum. Per the development cascade (CLAUDE.md), character-partitioned monologue is Phase 6 detail-coloring; the codebase has been stripped of the `CharacterArchetype` enum and the `MonologueState.character` field that keyed pool selection (#878). Pool selection is now archetype-independent. Authoring files that carry historical `character:` tags are content artifacts and will be re-evaluated when the Phase 6 character system is designed; engine consumption of the field is gone. Reintroduction gate: a confirmed Phase 6 character-model design is a prerequisite. See `docs/architecture/sprint-37-878-audit.md` (lead override section) for the cascade rationale.
- **Amendment (2026-04-22, Sprint 37, PR #137):** The `character` monologue-specific tag (historically `smuggler | detective`) is retired pending a Phase 6 character-model design — not deferred in place with a stub enum. Per the development cascade (CLAUDE.md), character-partitioned monologue is Phase 6 detail-coloring; the codebase has been stripped of the `CharacterArchetype` enum and the `MonologueState.character` field that keyed pool selection (T-878). Pool selection is now archetype-independent. Authoring files that carry historical `character:` tags are content artifacts and will be re-evaluated when the Phase 6 character system is designed; engine consumption of the field is gone. Reintroduction gate: a confirmed Phase 6 character-model design is a prerequisite. See `docs/architecture/sprint-37-878-audit.md` (lead override section) for the cascade rationale.
### D-036: Sova Transit District / Van Maanen's Star as v0.1 setting
- **Date:** 2026-02-11
@@ -104,7 +104,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- Atmosphere: "quotidian-with-undertow" — comfortable enough to be complacent, tight enough that extra income is tempting
- **Raised by:** Miri (Sova setting brief, Round 1; Van Maanen's Star profile, Round 2), project lead (confirmed as worldbuilding milestone, directive #6)
- **Raised by:** Miri (Sova setting brief, Round 1; Van Maanen's Star profile, Round 2), project lead (confirmed as worldbuilding milestone, directive T-6)
- **Dissent:** None
- **Amendment (2026-03-05, Where's the Fun? Workshop):** Station Sova / Van Maanen's Star confirmed as the v0.2 setting. [D-128](#d-128-culture-implicit-in-starting-location--van-maanens-star-system-equals-van-maanens-star-culture) makes Van Maanen's Star culture the cultural context for the tycoon bookmark — Van Maanen's Star IS Van Maanen's Star culture by default. The setting details (naming conventions, atmosphere, sensory palette) survive as generator inputs and culture profile content. However, the Sova Transit District spatial layout (D-093) was designed for the v0.1 hand-built slice. v0.2 generates the location via the generator ([D-114](scope.md#d-114-v02-proof-of-life--generator--graphics-not-hand-built-slice)); the Van Maanen's Star culture profile (Miri prerequisite for [D-119](scope.md#d-119-generator-spike-confirmed-for-sprint-25--critical-path)) captures the setting identity as generator inputs. Sova remains the canonical example system and the first culture profile to author.
@@ -185,12 +185,12 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
1.*Separation of concerns.* Access (social position) and trust (relationship depth x knowledge depth) answer different questions. Collapsing them into one axis would require rewriting D-028's four-layer model and D-035's tag taxonomy — both confirmed and implemented.
2.*Minimal code change.* The only implementation change is adding a `KnowledgeConfidence` parameter to `relationship_to_trust()` in `server/src/simulation/dialogue.rs`. The caller already has access to the observer's KnowledgeGraph. No new components, no new tags, no content format changes.
3.*Emergent archetype distinction.* Hardcoding archetype tags creates a maintenance burden (new character = new tag = new content variant) and reduces the "two keyholes on the same world" experience. When the detective and smuggler experience different dialogue from the same NPC, it should be because they have different *relationships* and *knowledge*, not because a tag excluded them.
- **Implementation change to #305:** `relationship_to_trust()` gains a `confidence` parameter. Mapping: `(Friendly, KnowsDetails+) → Secret`, `(Friendly|Known, KnowsOf+) → Real`, `(_ , _) → Surface`. Caller in `process_talk_interaction` passes `observer_kg.confidence_of(&target_sid)` to the updated function.
- **Implementation change to T-305:** `relationship_to_trust()` gains a `confidence` parameter. Mapping: `(Friendly, KnowsDetails+) → Secret`, `(Friendly|Known, KnowsOf+) → Real`, `(_ , _) → Surface`. Caller in `process_talk_interaction` passes `observer_kg.confidence_of(&target_sid)` to the updated function.
- **Resolves:** OQ-18
- **Amends:** [D-041](architecture.md#d-041-knowledge-graph-data-model) (confirms confidence-to-trust mapping; supersedes the preliminary 1:1 sketch in D-041 "Key design choices" bullet 3 with the layered model above), [D-028](#d-028-dialogue-architecture--tagged-line-pools-with-four-relational-layers) (Layer 3 trust now requires both relationship AND confidence)
- **Cross-reference:** [D-028](#d-028-dialogue-architecture--tagged-line-pools-with-four-relational-layers), [D-035](#d-035-converged-tag-taxonomy-for-dialogue-and-monologue-line-pools), [D-041](architecture.md#d-041-knowledge-graph-data-model), [D-062](#d-062-invisible-locked-dialogue-options) (confidence progression naturally unlocks new trust tiers, creating the "new options appearing" reward)
- **Raised by:** Tyre (technical analysis, architecture synthesis)
- **Dissent:** Gestalt endorses D-075 (reviewed 2026-02-19). The emergent archetype distinction is sufficient: access tier tags already encode "authority figure lines" vs "insider lines" in content; starting knowledge differentials produce different dialogue gate timings per character; adding an archetype filter would create per-character content maintenance burden and dilute the "two keyholes on the same world" experience (D-027). Knowledge vocabulary doc (#368) confirms this works in practice — same fact IDs, different starting confidence levels, different gate-open timing per character. *Nigel's input still pending.*
- **Dissent:** Gestalt endorses D-075 (reviewed 2026-02-19). The emergent archetype distinction is sufficient: access tier tags already encode "authority figure lines" vs "insider lines" in content; starting knowledge differentials produce different dialogue gate timings per character; adding an archetype filter would create per-character content maintenance burden and dilute the "two keyholes on the same world" experience (D-027). Knowledge vocabulary doc (T-368) confirms this works in practice — same fact IDs, different starting confidence levels, different gate-open timing per character. *Nigel's input still pending.*
### D-084: Dual-namespace line ID scheme — role pool + instance override
- **Date:** 2026-02-25
@@ -211,7 +211,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Hand-authored NPCs:** Unchanged. `kael-davan`, `sera-venn`, and all named authored NPCs keep their current slugs and ID sequences. No migration.
- **Resolves:** Q-028
- **Cross-reference:** Line ID scheme ([D-035](#d-035-converged-tag-taxonomy-for-dialogue-and-monologue-line-pools)), population model ([D-029](#d-029-population-entanglement-ratio--305020)), NPC generation ([D-024](#d-024-npc-generation-model--10-axes--combat-component))
- **Raised by:** Gestalt (Sprint 18, #544). Endorsed by Tyre pending implementation review.
- **Raised by:** Gestalt (Sprint 18, T-544). Endorsed by Tyre pending implementation review.
- **Dissent:** None.
### D-090: PC voice registers — smuggler and detective speech patterns `[SUPERSEDED]`
@@ -244,7 +244,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Raised by:** Full team — Gestalt (gameplay constraints), Miri (worldbuilding/lore), Araminta (visual/spatial), Tyre (technical), Paula (narrative), Ozzie (player experience). Compiled by Qatux.
- **Dissent:** Araminta preferred 32×32 visual chunk size (overruled by lead and team majority). No other dissent.
- **Source:** Station District Layout Workshop, Ticket #153, Sprint 20. Round document: `docs/discussions/round-20-station-district-layout.md`
- **Source:** Station District Layout Workshop, Ticket T-153, Sprint 20. Round document: `docs/discussions/round-20-station-district-layout.md`
@@ -252,7 +252,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
### D-095: Horizon Stations and Gate Infrastructure — Transport Lore
- **Date:** 2026-02-25
- **Decision:** Span gates are human-built structures with a single aperture enabling near-instantaneous transit. Operating schedule uses dual-use windows: freight (bulk of hours) and passenger (scheduled slots). Physical layout: aperture chamber → freight staging / passenger arrival → customs lanes → gate concourse. Horizon stations are alien-built installations (no identified builder species), self-maintaining, located at Oort-cloud distance, with **1–8 apertures per station** (revised from 4–8; single-aperture stations are valid and produce isolated dead-end systems with one way in and one way out). Per-system canonical name: "The Ring." Travel is sequential-hop only (A→B→C through intermediate systems; no direct long-range transit). Per-system access tiers vary (4 tiers — some systems allow single-hop to orbital customs; no direct planetary span gate). Station Sova's horizon gates are located at The Ring (Oort-cloud orbital); the Administrative Hub contains booking offices only (not the gates themselves — correction to prior station profile text). "The Loop" is Sova's internal tram network: 6 districts, 4-minute run from Residential Core to Transit District. Workers arrive at the transit platform (bar-side) and disperse to Terminal or bar.
- **Rationale:** Resolves transport lore questions Q-040, Q-041, Q-043, Q-044 raised during Workshop #153. Miri's Round 3 contribution. Horizon station as alien-built infrastructure adds worldbuilding depth without requiring a named builder species. Sequential-hop travel creates natural story hooks (layover locations, transit records, smuggling route complexity). Aperture count revised to 1–8 (from 4–8) to allow single-aperture stations, which produce the most isolated and narratively interesting dead-end systems — and leave open the question of what a single-aperture station pointing at an unknown destination means.
- **Rationale:** Resolves transport lore questions Q-040, Q-041, Q-043, Q-044 raised during Workshop T-153. Miri's Round 3 contribution. Horizon station as alien-built infrastructure adds worldbuilding depth without requiring a named builder species. Sequential-hop travel creates natural story hooks (layover locations, transit records, smuggling route complexity). Aperture count revised to 1–8 (from 4–8) to allow single-aperture stations, which produce the most isolated and narratively interesting dead-end systems — and leave open the question of what a single-aperture station pointing at an unknown destination means.
- **Raised by:** Miri (worldbuilding), confirmed by team.
- **Amendment (2026-03-13):** Aperture count range revised from 4–8 to **1–8** per project lead direction. Single-aperture horizon stations are valid; they are the mechanism for the most isolated dead-end systems, and for any system whose single gate points to a destination that is not (or not yet) part of the Reach. This change has downstream implications for the Earth question — see note in systems-framework-miri.md §7.
- **Amendment (2026-03-14):** Clarified the inter-system / intra-system gate distinction and technology lineage:
@@ -260,7 +260,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Intra-system gates (span gates):** Human-built technology, derived from the Veil Institute's study of alien horizon station architecture. The Institute reverse-engineered the principles governing short-range aperture formation and licensed the technology to a gate construction corporation, which now owns and operates span gate infrastructure across the Reach (and owns an entire system as its industrial base). Span gates are well-understood engineering: buildable, modifiable, commercially operated.
- **Key constraint for wiki authors:** No system's aperture count or inter-system connections are the result of human decisions. Systems cannot "vote for" or "apply for" additional horizon gate apertures. A system's gate topology is an alien-determined fact that humans have adapted to, not chosen.
- **Dissent:** None.
- **Source:** Station District Layout Workshop, Ticket #153, Sprint 20. Round document: `docs/discussions/round-20-station-district-layout.md`
- **Source:** Station District Layout Workshop, Ticket T-153, Sprint 20. Round document: `docs/discussions/round-20-station-district-layout.md`
@@ -269,7 +269,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Date:** 2026-02-27
- **Decision:** Social triangles carry `Vec<TrianglePurpose>` — a multi-tag set for playstyle accessibility. Enum values: `Investigation, Economic, Social, Political, Tactical, Mundane`. `Tactical` encodes the assassination contract in spatial form (target + protector + informant/witness). Multiple purpose tags per triangle: a smuggling operation can be `Economic + Tactical` simultaneously. Purpose tags ensure the right drama is surfaced to the player whose active lens matches — they add no generation cost, categorizing existing output.
- **Rationale:** Purpose tags are the bridge between the generator's social structure and the player's active playstyle. The generator doesn't build for one playstyle — it tags what's already there.
- **Raised by:** Gestalt (triangle purpose model). Full team sign-off.
- **Dissent:** None.
- **Cross-reference:** D-025 (social site / functional cluster — triangles populate social sites), D-097 (guarantee tier system — triangles feed the audit)
@@ -278,7 +278,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Date:** 2026-02-27
- **Decision:** Data-driven `HeritageGrammarOverlay` structs (10 per heritage root). Loaded once at generator startup, applied at Phase 2 chunk fill by weighted blending. Blend rules: continuous fields (decorative_density, repair_visibility, etc.) use weighted average; categorical fields (boundary_character, open_space_character) use dominant heritage weight; object tag lists use union of preferred/accent tags and intersection-exclusion of excluded tags. Phase 1 exception: `gathering_probability` evaluated at block planning for quarter pre-assignment. Authoring domain separation: **Miri** authors organizational principles, boundary character, spacing, social grammar (HeritageGrammarOverlay Rust struct / authored data). **Araminta** authors visual expression — object sets, arrangement algorithms, floor surface variants, overhead flora density and character, wall/structure material character, boundary material type, lighting temperature (TOML modifier files, one per heritage root). Shared: `ObjectTag` vocabulary must be co-maintained (see Q-049).
- **Rationale:** Heritage roots must be spatially legible — the visual grammar of a Frost community versus a Tide community must be apparent to the observant player without a label.
@@ -289,7 +289,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Date:** 2026-02-27
- **Decision:** Informal zones (spaces outside the community's social field) are defined by the type of social permission governing them, not by institutional absence. Three types: `physical_distance` — sparse objects, unmaintained floor; isolation is the visual. `social_permission` — normal zone palette; gathering infrastructure present; cover is about convention, not geography. `utilitarian_cover` — functional work objects; space reads as work space; unofficial use invisible to casual observation. Heritage root correlations: Frost/Stone → `physical_distance`; Tide/Vine/Dust → `social_permission`; Iron/Salt → `utilitarian_cover`. (Dust = maximum communal observation, privacy is negotiated not physical; Iron = labor function covers presence.) Location within terrain seeded independently. Visual grammar per type: `docs/workshops/generator-architecture/araminta-round4.md`.
- **Rationale:** Privacy mechanics emerge from community culture. How you hide in a Frost community (physical distance) is architecturally different from how you hide in a Dust community (social agreement). The typology makes privacy mechanics culturally legible.
@@ -300,7 +300,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Date:** 2026-02-27
- **Decision:** Trauma events are a subtype of `EraModification` with dual-track effects. Five subtypes: `PhysicalDestruction, EconomicDisruption, PoliticalShock, ViolenceEvent, MigrationShock`. Separate tracks: (1) structural damage via `StructuralChange` in ChunkMutations (applied as `LocalOverlay` per D-109); (2) cultural response via NPC weight distribution shift in `DistrictRuntimeState.npc_pattern_weights`. Decay rate seeded per-community with variation around heritage-root baseline (`trauma_visual_decay_rate: slow | medium | fast`, default medium). Design principle: **Trauma intensifies culture, it does not transform it.** A stressed community becomes a more concentrated version of itself — Frost communities close harder, Tide communities grief more publicly, Iron communities organize more collectively. Decay is toward the community's pre-trauma baseline, not toward a new equilibrium. Players who have learned a heritage root's trust model can predict community behavior in the aftermath.
- **Rationale:** Cultural response must be legible — and predictable to a player who has invested in understanding the heritage root. Trauma as amplifier (not transformer) rewards prior observation.
@@ -457,9 +457,9 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Rationale:** The current model has `typical_behaviors: Vec<String>` on RoleSpec — flat strings per culture×zone×role. At ~50 behaviors × 4 roles × N zones × M cultures, this is O(roles × zones × cultures) custom content. Decomposition to primitives + modifiers reduces to O(roles + cultures) authored content. Assembly is deterministic via SimRng, so behavior output is reproducible for a given seed. The three-layer model maps directly to the existing CultureProfile/RoleSpec/NpcBlueprint data flow — no new serialization formats or pipeline stages.
- **Migration path:** Copy team populates `behavior_primitives` on zone spec RON files and `behavior_modifiers` on culture RON files. Generator switches from `typical_behaviors` to `assemble_behaviors()` when primitives are present. Once verified equivalent for seed 42, legacy field can be removed.
- **Dissent:** None (design resolves the scaling problem identified in #630 sprint review)
- **Dissent:** None (design resolves the scaling problem identified in T-630 sprint review)
- **Resolves:** Q-057 (composable behavior generation — data structure definition)
- **Cross-reference:** [D-138](#d-138-llm-re-voicing-pipeline-for-npc-voice) (resolved pipeline, this resolves data format), [D-121](#d-121-voice-is-culture-driven--job-as-modifier) (culture-primary voice), [D-122](#d-122-all-npcs-generated--named-npcs-deferred) (all NPCs generated)
@@ -528,7 +528,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Layer 3 grammar constraint:** When Gemma appends environmental flavor to Layer 1+2 assembled text, the flavor clause must grammatically attach as a prepositional phrase or coordinating conjunction. Absolute and participial phrases (e.g., "tool already in hand") require a leading comma separator. Culture-specific modifier clauses that use absolute-phrase construction must include the separator (e.g., ", tool already in hand") or be rewritten as prepositional form ("with tool in hand") to ensure clean assembly. This constraint applies to all culture profile modifier authoring.
- **Source:** Sprint 26 zone-type taxonomy discussion (2026-03-13); behavior review (#634); user requirements for 300-system scalability
- **Source:** Sprint 26 zone-type taxonomy discussion (2026-03-13); behavior review (T-634); user requirements for 300-system scalability
- **Raised by:** Paula (zone-type taxonomy and POI system); Gestalt (assembly engine); Mellanie (primitive content); Tyre (architectural review)
- **Dissent:** None
- **Supersedes:** Per-location behavior pool approach from current zone spec format (`behavior_primitives` arrays on location RON files). Location files retain metadata but behavior content migrates to zone-type template files.
@@ -566,21 +566,21 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
### D-147: Aesthetic taste as character personality trait — shared root for cosmetic and environmental expression
- **Date:** 2026-03-17
- **Decision:** Characters have an `aesthetic_taste` personality trait (preferred palette, style leanings) that naturally expresses in both personal appearance and living space. This is NOT a cosmetic→apartment pipeline — hair color does not map to wall color. Instead, taste is a shared root: someone who gravitates toward teal will pick teal clothing AND own a teal vase. The apartment generator reads the character's taste trait (alongside economic position from #615 and cultural context from #679); the character customisation screen lets the player express that same taste through appearance choices. Neither system depends on the other — both read the same underlying trait.
- **Rationale:** The naive approach (pipe cosmetic choices into apartment generator) creates a crude "team color" mapping that feels artificial. The elegant approach: taste is a personality attribute, like traits or skills. It manifests independently in multiple contexts. This keeps #619 (customisation) and #617 (apartment) architecturally independent while producing coherent identity expression. The taste trait is also reusable: NPC aesthetic preferences, gift preferences, "does this room feel like home" comfort checks, cultural style variations.
- **Dependency implication:** #619 does NOT block #617. Both read the character's taste trait independently. The taste trait definition belongs to the character data model (server), not to either consuming system.
- **Decision:** Characters have an `aesthetic_taste` personality trait (preferred palette, style leanings) that naturally expresses in both personal appearance and living space. This is NOT a cosmetic→apartment pipeline — hair color does not map to wall color. Instead, taste is a shared root: someone who gravitates toward teal will pick teal clothing AND own a teal vase. The apartment generator reads the character's taste trait (alongside economic position from T-615 and cultural context from T-679); the character customisation screen lets the player express that same taste through appearance choices. Neither system depends on the other — both read the same underlying trait.
- **Rationale:** The naive approach (pipe cosmetic choices into apartment generator) creates a crude "team color" mapping that feels artificial. The elegant approach: taste is a personality attribute, like traits or skills. It manifests independently in multiple contexts. This keeps T-619 (customisation) and T-617 (apartment) architecturally independent while producing coherent identity expression. The taste trait is also reusable: NPC aesthetic preferences, gift preferences, "does this room feel like home" comfort checks, cultural style variations.
- **Dependency implication:** T-619 does NOT block T-617. Both read the character's taste trait independently. The taste trait definition belongs to the character data model (server), not to either consuming system.
- **Information boundary (D-010):** `aesthetic_taste` is a public trait — visible to other entities and the client. NPCs can observe the player's taste (e.g. gift relevance, comfort assessment). This is not private knowledge behind the information boundary.
- **Raised by:** Project lead (taste-as-shared-root framing), Gestalt (ColorHint concept evolved into trait), Paula (culture + economics as primary apartment drivers)
- **Dissent:** Ozzie argued for direct cosmetic→apartment pipeline. Tyre and Paula argued for no connection at all. The taste trait is the synthesis: connection through personality, not through data pipeline.
- **Cross-reference:** [D-125](#d-125-world-is-quietly-responsive--gradient-of-caring-by-social-proximity) (quiet responsiveness), [D-134](architecture.md#d-134-full-character-customisation--hair-clothing-colors-at-tile-scale) (full character customisation), [D-136](architecture.md#d-136-first-settled-reach-moment-auto-generated-apartment--insert-activation) (first game moment), ticket #617 (auto-generated apartment), ticket #619 (full character customisation)
- **Cross-reference:** [D-125](#d-125-world-is-quietly-responsive--gradient-of-caring-by-social-proximity) (quiet responsiveness), [D-134](architecture.md#d-134-full-character-customisation--hair-clothing-colors-at-tile-scale) (full character customisation), [D-136](architecture.md#d-136-first-settled-reach-moment-auto-generated-apartment--insert-activation) (first game moment), ticket T-617 (auto-generated apartment), ticket T-619 (full character customisation)
- **Superseded:** 2026-04-10. NPC ambient interaction model scrapped. See [R-012](../rejected/perception.md#r-012-overheard-npc-conversation-system-d-078--scrapped).
---
@@ -607,7 +607,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Decision:** The planet name "Iserlohn" (GJ-532c, in the Kettenschmied system) is **retained**. The name derives from Iserlohn, a real industrial city in the Sauerland region of North Rhine-Westphalia, Germany — historically a centre of wire-drawing, chain manufacturing, and metal fabrication. This origin is directly coherent with Kettenschmied's identity as a west_reach German-heritage metalworking community. The real-city reading is the intended and primary reading.
- **IP note:** Iserlohn Fortress is an iconic location in *Legend of the Galactic Heroes* (Ginga Eiyuu Densetsu). The name collision is acknowledged. The decision to retain is deliberate: the real-world German city predates the anime by centuries; real geographic names are not protectable; and the concept diverges fundamentally — a cold marginal mining/fabrication planet (1,000 population, component fabrication, asteroid mining) is the structural opposite of a strategic military megastructure. No reader can point at GJ-532c and say "that's the LoGH fortress" — the contexts share only a name.
- **Preferred alternative on record (if future team prefers a rename):** **Altena** — a Sauerland city at the confluence of Lenne and Volme rivers, historically the actual birthplace of German wire-drawing and chain manufacture. The Altena castle became the site of the first industrial wire-drawing operation in the German states. Altena has no known SF franchise association and carries stronger historical precision for chain/cable fabrication. It would require an atlas rename of `GJ532c` and corresponding wiki edits.
- **Raised by:** Miri (Sprint 31, ticket #773)
- **Raised by:** Miri (Sprint 31, ticket T-773)
- **Dissent:** None.
### D-193: Lattice Commission — canonical long-form of the Commission
- **Sol** — Earth legacy currency. Finite pool, no active issuer, no formal exchange rate. Used for crime and untraceable transactions, and as payment for premium Earth luxury imports (wines, foods, exclusive goods). Sol is NOT a currency zone — it is modeled as a shadow economy commodity, not a simulation numeraire.
- Commission fines and certification fees are Tractus-denominated (the Commission is Assembly-funded). Compact members paying Commission costs face currency conversion friction — this is a structural driver of the shadow economy in the Compact zone, not a coincidence.
- **Rationale:** Three currencies create structural economic bloc tension without requiring event generation. The Tractus/Mark divide maps directly to the Assembly vs. Compact political divide. Sol's untraceable nature makes it the natural medium for crime — it has lore grounding and mechanical function without requiring a formal exchange rate.
- **Raised by:** Full planning team, Sprint 32 Workshop #796. Lead directive on political framing.
- **Raised by:** Full planning team, Sprint 32 Workshop T-796. Lead directive on political framing.
- **Dissent:** Burnelli-Sheldon (economist) recommended single currency for Phase 2 — see [R-011](../rejected/economics.md#r-011-single-currency-for-phase-2-economics). Overruled by lead.
- 21 production chains including 2 substitution routes
- Schema: 14 fields per commodity including 3 political sub-flags (`commission_certifiable`, `compact_contested`, `shadow_viable`), `production_ubiquity`, `demand_model`, `panic_threshold_weeks`
- **Rationale:** The catalog is the concrete realization of the D-173 taxonomy. 36 types is rich enough to produce meaningful geographic specialization while remaining authorable. The 14-field schema captures economic, political, and behavioral properties needed by the tâtonnement simulation. Two substitution routes prevent hard lock-outs in chains where one raw source is geographically constrained.
- **Raised by:** Full planning team, Sprint 32 Workshop #801.
- **Raised by:** Full planning team, Sprint 32 Workshop T-801.
- **Dissent:** None.
- **Cross-reference:** [D-173](#d-173-commodity-taxonomy) (taxonomy), [D-182](#d-182-toml-source-of-truth-for-economics-data) (TOML pipeline), [D-185](#d-185-brands-are-not-commodities) (what is excluded from this catalog)
- The commodity chain terminates at abstract generic finals (e.g., "premium spirits", "luxury textiles")
- Brand premium lives in the corporate behavioral layer — it is not modeled in Phase 2
- **Rationale:** Adding 30+ named brands to the commodity catalog would require per-brand pricing models, cultural preference curves, aging pipeline tracking, and star-system terroir logic — a separate simulation in its own right. Phase 2 must deliver a working generic commodity simulation first. Brands consuming generic commodities as inputs correctly captures the economic relationship without requiring brand simulation code.
- **Raised by:** Lead directive, Sprint 32 Workshop #801.
- **Raised by:** Lead directive, Sprint 32 Workshop T-801.
- **Dissent:** None.
- **Cross-reference:** [D-184](#d-184-commodity-catalog-36-types) (the catalog that excludes brands), [D-177](#d-177-productivity-constraints-lore-derived) (lore ceilings on brand production — still authoritative), [D-175](#d-175-corporation-taxonomy-and-prerequisite) (brand corporations exist as Tier 1 entities)
- **`MARK_PRIMARY` zones default to `gate_energy_connected = false`.** The Compact refused Gate Corp energy dependency as a deliberate political choice — it preserves Compact energy sovereignty and economic independence.
- **Gate Corp cutoff scenario:** If Gate Corp cuts energy to a dependent node, `fusion_fuel` demand spikes. Compact surplus capacity (from their independence) becomes the emergency supply source. The Compact's refusal of dependency is an economic asset with concrete mechanics.
- **Rationale:** Energy-over-gate gives the gate topology an additional economic dimension beyond freight routing. Opt-in model prevents mandatory complexity for every node. The Compact's structural refusal creates the primary counterplay scenario Gestalt was concerned about — without opt-in, no interesting asymmetry. Gate Corp as private monopoly (not Assembly policy) makes it an independent economic actor, enabling corporate storylines.
- **Dissent:** Gestalt initially recommended Level 2 (no energy transmission) due to counterplay concerns. Resolved by the opt-in model — the Compact's refusal to connect provides the counterplay without requiring adversarial defaults.
- **Cross-reference:** [D-172](#d-172-currency-zone-initialization) (`MARK_PRIMARY` zone flag), [D-173](#d-173-commodity-taxonomy) (`fusion_fuel` as a commodity), [D-187](#d-187-fusion-fuel-as-intermediate-81-water-yield) (fuel chain), [D-175](#d-175-corporation-taxonomy-and-prerequisite) (Gate Corp as Tier 1 corporation)
- Creates a **structural frontier energy cost premium**: frontier nodes pay more for fusion fuel because water transport costs accumulate in the refining input cost, and the chain demand from smelting/alloys/electronics cascades that cost into the manufacturing sector
- This premium arises from geography and chain structure — no event generation required
- **Rationale:** Raw-material fuel would make energy cost geography flat (water is everywhere, therefore fuel is everywhere at the same price). Intermediate fuel with a high water yield ratio creates a conversion cost that multiplies transport costs — frontier refineries are expensive to run because fuel production itself consumes large volumes of low-value bulk water. The cascade effect through 3 industrial chains means frontier manufacturing is structurally more expensive, which is consistent with real-world frontier economics and the lore of the Compact vs. Core divide.
- **Raised by:** Lead directive, validated by Burnelli-Sheldon (economics), Sprint 32 Workshop #801.
- **Raised by:** Lead directive, validated by Burnelli-Sheldon (economics), Sprint 32 Workshop T-801.
- **Dissent:** None.
- **Cross-reference:** [D-184](#d-184-commodity-catalog-36-types) (fuel and water are catalog entries), [D-186](#d-186-gate-transmission-levels-mass--data--energy) (energy-over-gate reduces fuel demand), [D-178](#d-178-economic-model-architecture) (Leontief production cascade)
- **Rationale:** Administered pricing is the correct model because brands violate all three tâtonnement assumptions: heterogeneity (Calloway ≠ VGV ≠ generic spirits), supply inelasticity (terroir production cannot respond to price signals per D-177), and Veblen demand effects (prestige goods can have upward-sloping demand). The one-way interface (commodity prices → brand input costs; brand output prices do NOT feed back into tâtonnement) is architecturally clean and matches the D-178 layer model. The identity/exotic split in cultural premium is the minimal structural addition needed to produce all three observed pricing curves (Scarcity-Distance, Dual-Peak, Aspirational Gradient). Population asymmetry (~80B total Reach population, systems ranging from 10B to <50k) makes the halo/volume tier pattern structurally necessary — brands from tiny worlds are astronomically exclusive and need volume derivatives to be economically relevant.
- **Raised by:** Full planning team workshop, Sprint 34 (#811).
- **Raised by:** Full planning team workshop, Sprint 34 (T-811).
- **Authoring rule:** wiki volume figures for media brands and any good with `brand_category = cultural` must include a `reference_population` annotation alongside the count. "40M" is incomplete; "40M (west_reach corridor, ~10B addressable)" is correct. This applies to corporation production ceilings, media distribution figures, and market penetration estimates in wiki pages and TOML files.
- **Structural scarcity principle:** for physical brand goods, production volume only has meaning relative to addressable demand. The `structural_scarcity_base` parameter in the brand pricing layer (D-189) is derived from this ratio: `1.0 - min(1.0, annual_volume / (addressable_population × demand_rate))`. A 12,000-unit/year artisan product against 100M addressable consumers yields structural_scarcity_base ≈ 0.88 — perpetually near-maximum scarcity regardless of local stockpile state. This scarcity floor is permanent, not situational.
- **Rationale:** The Reach's population asymmetry (core systems 10B+, frontier systems under 50K) makes absolute volume numbers meaningless without a reference population. Without an explicit calibration rule, brand and media content significance will be systematically miscalibrated across all authoring. The structural scarcity principle connects volume calibration to the administered pricing model: a brand's Veblen premium floor is derived from the same population-relative ratio, ensuring that pricing and authoring are grounded in the same underlying reality.
@@ -105,7 +105,7 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
- **Key principle:** Red means danger TO YOUR CHARACTER, not danger in the abstract. The detective might see amber (flagged in case file) where the smuggler sees green (trusted colleague). This IS asymmetric information rendered visually.
- **Transition behavior:** Color shifts smoothly (0.5s fade) when relationship state changes. THE FRIEND's first color shift (green → amber) should be the first relationship color change in the session — maximum emotional impact.
- **Cross-reference:** Internal monologue ([D-016](#d-016-internal-monologue-as-core-perceptionatmosphere-system)), THE FRIEND pattern ([D-034](content.md#d-034-the-friend--production-level-npc-pattern))
- **Raised by:** Araminta (Round 1 proposal, color palette design), project lead (approved, directive #2)
- **Raised by:** Araminta (Round 1 proposal, color palette design), project lead (approved, directive T-2)
- **Dissent:** None
- **Amendment (2026-03-17, Sprint 28 workshop):** Entity relationship color is NOT displayed via character outlines in normal gameplay. The D-033 color palette (#4a9ebb, #6bc9a6, etc.) is valid within the insert/perception mode overlay only. Normal gameplay renders all characters with uniform dark outlines. See [D-154](scope.md#d-154-character-outline-is-not-a-relationship-indicator--uniform-dark).
@@ -220,7 +220,7 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
- **Source:** Control & Interaction Workshop (2026-02-13)
- **OQ-07 resolution (2026-02-19, #522):** Insert-off behavior is **option (a): cursor shape still changes, verb labels suppressed.**
- **OQ-07 resolution (2026-02-19, T-522):** Insert-off behavior is **option (a): cursor shape still changes, verb labels suppressed.**
- Cursor state machine fires normally (entity hover → bracket shape, object hover → X-shape) — the character's body physically orients toward targets as a subconscious/spatial response.
- Insert does not process targets into actionable data: `should_show_interactions()` returns false when `insert_active == false`, and interaction labels (z-layer 6) are hidden via `set_insert_active(false)` on `InteractionList` and `InteractionPrompt`.
-`GameState.insert_active` is the source of truth (defaults true in v0.1; wired from snapshot field `insert_active`).
@@ -228,7 +228,7 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
- **Supersession note (2026-04-21 / amended 2026-04-22, Sprint 37, #878, PR #137):** The character-archetype verb variation portion of this decision is retired pending a Phase 6 character-model design — not deferred in place with a stub. Per the development cascade (CLAUDE.md), archetype-driven verb relabeling is Phase 6 detail-coloring and has been stripped from the server. Vertical-list structure, Phase 1/Phase 2 split, POI priority flips, and contradiction markers remain live. Relabeling (Open→"Move"/"Stash" vs "Scan"/"Flag") is deleted; container verb labels are now identical across all player states, and uniform labeling is the intended pre-Phase-6 end state, not a regression. Reintroduction gate: a confirmed Phase 6 character-model design is a prerequisite. See `docs/architecture/sprint-37-878-audit.md` (lead override section) for the cascade rationale.
- **Supersession note (2026-04-21 / amended 2026-04-22, Sprint 37, T-878, PR #137):** The character-archetype verb variation portion of this decision is retired pending a Phase 6 character-model design — not deferred in place with a stub. Per the development cascade (CLAUDE.md), archetype-driven verb relabeling is Phase 6 detail-coloring and has been stripped from the server. Vertical-list structure, Phase 1/Phase 2 split, POI priority flips, and contradiction markers remain live. Relabeling (Open→"Move"/"Stash" vs "Scan"/"Flag") is deleted; container verb labels are now identical across all player states, and uniform labeling is the intended pre-Phase-6 end state, not a regression. Reintroduction gate: a confirmed Phase 6 character-model design is a prerequisite. See `docs/architecture/sprint-37-878-audit.md` (lead override section) for the cascade rationale.
- **Decision:** Entity interactions use a compact vertical list (not radial). 2-4 options max, anchored to entity position. Insert-styled with Araminta's geometric aesthetic. New options unlocked by knowledge changes are highlighted with a gradient glow background. Radial menu reserved for world menu only ([D-058](#d-058-world-menu--radial-4-spokes)). Max 3 visible response options in dialogue context.
- **Server architecture:** Two-phase verb computation. Phase 1 (simulation, no KG): compute maximum possible verb set from ObjectType component (Readable, Container, Terminal, Door, Pickup, Furniture — each with specific verb sets). Phase 2 (observer, reads KG): filter by character's knowledge (Confront requires KnowsDetails+ per [D-041](architecture.md#d-041-knowledge-graph-data-model)), apply POI priority flips, add contradiction markers. ~~Character-archetype verb variation implemented as Phase 2 observer filter rules (same crate: smuggler sees "Move/Stash", detective sees "Scan/Flag").~~*[Removed Sprint 37 — archetype verb relabeling deleted; see supersession note above.]*
- **Diegetic test:** Labels render on z-layer 6. If insert is off, labels disappear.
@@ -238,7 +238,7 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
- **Source:** Control & Interaction Workshop (2026-02-13)
- **Dissent:** Araminta argued for spoke radial (geometry transformation signals qualitative knowledge change — new spoke growing). Lead rejected: items moving under cursor when knowledge changes is a moving goalpost (bad UX while aiming at an option).
- **OQ-07 resolution (2026-02-19, #522):**
- **OQ-07 resolution (2026-02-19, T-522):**
- When `insert_active == false`, the interaction list hides completely (`set_insert_active(false)` → `_hide()`).
- Cursor shape changes still occur per D-056 OQ-07 — list suppression is independent of cursor state.
-`GameState.insert_active` drives this at runtime, wired via `main.gd` on each snapshot.
@@ -285,12 +285,12 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
### D-061: Dialogue box — unified conversation log, bottom screen, max 20% height, no portraits
- **Date:** 2026-02-13
- **Decision:** Dialogue occupies the bottom of the screen, max 20% height, max-width 1200px ([D-076](#d-076-dialogue-box-max-width--1200px-oq-29-resolution)). NO portraits — the NPC is on screen, a portrait is redundant. Monologue floats ABOVE the dialogue box on z-layer 7 — spatial separation allows monologue to contradict dialogue visually (character thinks one thing while NPC says another). Walk-away via WASD, dialogue fades over 300ms, no close button ([D-064](content.md#d-064-walk-away--three-phase-consequences)). Auto-pause in single-player when implant UI is open; overlay design for multiplayer readiness.
- **Unified conversation log (Sprint 14, #535):** The dialogue box is a single chronological log for player-NPC conversations. Each entry shows `Speaker → Target: text` with per-character name colours (hash-indexed from configurable palette in `data/dialogue-theme.yaml`). Player response options render below the log; max 3 visible. Locked options invisible ([D-062](content.md#d-062-invisible-locked-dialogue-options)).
- **Unified conversation log (Sprint 14, T-535):** The dialogue box is a single chronological log for player-NPC conversations. Each entry shows `Speaker → Target: text` with per-character name colours (hash-indexed from configurable palette in `data/dialogue-theme.yaml`). Player response options render below the log; max 3 visible. Locked options invisible ([D-062](content.md#d-062-invisible-locked-dialogue-options)).
- **Entry lifecycle:** All entries share the same timeout (15s + 3s fade, configurable via theme YAML). Walk-away clears response options but preserves log entries — earned information is fair game. Panel auto-hides when all entries expire and no active conversation is in progress.
- **Passive overheard lines:** D-078 (overheard NPC conversations) was scrapped per R-012. Passive dialogue display will be redesigned after Phase 5 walkable environment.
- **Rationale:** Game world stays live above the dialogue box — player sees NPC body language while talking. Monologue above + dialogue below = the character can think one thing while saying another. Max 3 options + invisible locks = player never knows what they're missing. No portrait because the NPC IS on screen. A single unified log avoids a separate UI element for overheard content and makes the flow of conversation feel natural — active and passive dialogue interleave chronologically.
- **Dissent:** Stig initially proposed 25% height and 50% width centered. Lead constrained to 20% height and max-width.
@@ -380,7 +380,7 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
- Protocol version bumped to 11.
- **Client contract:** Client maintains a `zone_id → { name, temperature_tint, ambient_layer }` lookup table. Deep fog shader (layer 3) reads `zone_id` from the last-seen `VisibleTile` data to apply the ~10% temperature tint. AudioManager reads `zone_id` to trigger crossfade between ambient layers ([D-073](architecture.md#d-073-zone-crossfade-approach--hard-boundary-soft-audio-transition)).
- **Cross-reference:** Fog layers ([D-059](#d-059-fog--shader-based-five-layers-knowledge-graph-driven)), zone crossfade ([D-073](architecture.md#d-073-zone-crossfade-approach--hard-boundary-soft-audio-transition)), ObserverSnapshot ([D-020](architecture.md#d-020-engine-and-architecture-selection--godot-client--rust-simulation-via-subprocessipc)), deterministic simulation ([D-010](architecture.md#d-010-multiplayer-ready-architectural-baseline))
- **Raised by:** Tyre (architecture review, #523)
- **Raised by:** Tyre (architecture review, T-523)
@@ -415,7 +415,7 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
- **Rationale:** Unified event type keeps the knowledge system composable. Grant timing at line selection (not client display) ensures D-010 determinism — tick-stamped and server-authoritative. Entity grants are required for contradiction detection: testimony must create `EntityKnowledge` with `ToldBy` source so that a subsequent `DirectObservation` can detect a discrepancy. Physical evidence uses the same mechanism with `DirectObservation` source, which is treated as higher-confidence and cannot be contradicted by the NPC-denial path.
- **Raised by:** Knowledge Flow & NPC Information Boundaries Workshop — unanimous on grant timing and unified event type. Entity grants in Sprint 17 per team lead decision (2026-02-24), overriding Paula's Round 2 acceptance of Dudley's FactId workaround.
- **Dissent:** Paula (Round 2) accepted the FactId workaround with 3 binding conditions; team lead overrode in favour of entity grants path championed by Tyre, Gestalt, and Dudley. Paula's conditions honoured where applicable: (1) `ToldBy` source flows through `ContradictionDetected` event payload — satisfied by D-083; (2) Sprint 18 entity grants are committed scope; (3) formal record of this commitment.
@@ -425,9 +425,9 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
- **Resolves:** Q-024
- **Raised by:** Workshop — unanimous
- **Dissent:** None
- **Implements:** Ticket #548
- **Implements:** Ticket T-548
- **Cross-reference:** [D-041](architecture.md#d-041-knowledge-graph-data-model), [D-083](#d-083-contradiction-detection-pipeline), [D-078](#d-078-overheard-npc-conversation--passive-dialogue-panel-with-occlusion-filter) (D-078 scrapped per R-012; `transfer_npc_knowledge` retained for Phase 5 rewire — see Amendment 2026-04-19)
- **Amendment (2026-04-19, R-012 / #848):** D-078 was scrapped (R-012) and the `run_npc_conversations` system was deleted in #848. The `transfer_npc_knowledge` system is **retained in-tree for Phase 5 rewire** but no longer fires in production — its `Added<NpcConversation>` trigger is now only inserted by test fixtures. The design (trust-gated transfer, dual-mutable KG access, `KnowsOf` confidence cap, `disclosure_blocked` honoring) is preserved; Phase 5 will wire a new proximity/dialogue trigger in its place. Until then, treat the system as dormant and guard against assuming it runs.
- **Amendment (2026-04-19, R-012 / T-848):** D-078 was scrapped (R-012) and the `run_npc_conversations` system was deleted in T-848. The `transfer_npc_knowledge` system is **retained in-tree for Phase 5 rewire** but no longer fires in production — its `Added<NpcConversation>` trigger is now only inserted by test fixtures. The design (trust-gated transfer, dual-mutable KG access, `KnowsOf` confidence cap, `disclosure_blocked` honoring) is preserved; Phase 5 will wire a new proximity/dialogue trigger in its place. Until then, treat the system as dormant and guard against assuming it runs.
### D-081: Unprompted Disclosure Design
- **Date:** 2026-02-24
@@ -435,7 +435,7 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
- **Rationale:** NPCs checking only their own KG (Option A) is both diegetically correct and mechanically richer: NPCs can say things the player already knows, which creates dramatic irony. Option B (NPC checks player's KG) would collapse this asymmetry at exactly the moments their speech would be most dramatically charged. Separate `DisclosureCandidates` component prevents DerivedTellState serialization bloat. The location privacy gate creates learnable spatial behavior patterns. The per-fact `per_fact_history` is the primary narrative quality gate.
- **Raised by:** Workshop — unanimous on architecture; Paula and Gestalt on trigger gates; Tyre and Dudley on implementation structure
- **Dissent:** Gestalt (Round 1) opposed global rate limit as creating invisible NPC competition. Resolved in Round 2 via deterministic StableId selection. Included.
@@ -444,7 +444,7 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
- **Rationale:** The minimum viable boundary produces maximum gameplay-visible difference for minimum risk. Pathfinding from KG has an unresolvable failure mode (NPC forgets path nodes = stuck NPCs = undefined movement behavior) and must not be implemented. Self-knowledge always comes from axis components because the NPC is the continuous observer of its own state. The `Option<&KnowledgeGraph>` addition is backward-compatible.
- **Raised by:** Workshop — unanimous
- **Dissent:** None. Paula adds that `tell_state` should eventually incorporate KG-derived secret-exposure intensity — deferred to a future sprint as enhancement.
@@ -454,7 +454,7 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
- **Resolves:** Q-026
- **Raised by:** Workshop — unanimous on architecture and downstream chain; Tyre and Gestalt on struct approach; Dudley conceded Option B (string encoding).
- **Dissent:** Dudley (Round 1): Option B string encoding as Sprint 17 workaround. Conceded in Round 2. No standing dissent.
### D-086: Insert icon system — custom SVG, no icon font
@@ -467,7 +467,7 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
- **Godot integration:** Godot 4 handles SVG natively (AtlasTexture, importable SVGs). Runtime color via ShaderMaterial. No font rendering pipeline or bitmap caching needed.
- **Icon vocabulary (v0.1):**
- Inventory item silhouettes: 3–4 items at 40×40px (manifest, access token, comm log)
- Perception mode indicators: 2–3 icons at 20×20px (deferred until #315 specs the modes)
- Perception mode indicators: 2–3 icons at 20×20px (deferred until T-315 specs the modes)
- Stance indicators: 4 icons at 20×20px (WALK, CAREFUL, SPRINT, CROUCH) — delivered Sprint 32 alongside text labels. Icons supplement labels; text remains primary affordance.
- Interaction prompt icons: 4 icons at 16×16px (talk, observe, follow, examine) — delivered Sprint 32. Supplement interaction verb text per insert HUD wireframe §9.
- Border arrows and geometric markers (diamonds, dots, squares): drawn as primitives, not icons
@@ -59,7 +59,7 @@ How the team works: composition, naming, workflow.
- **Design principles:** (1) Game-first, not encyclopedia-first — every entry serves the content pipeline. (2) Hierarchical with cross-references via IDs, not file paths. (3) Metadata-rich YAML frontmatter enabling programmatic queries. (4) Template-driven — new entries follow category templates. (5) Every entry has a `status` field: `proposed` → `draft` → `canonical`.
- **Governance:** Miri owns wiki structure (new categories, templates, structural decisions). Anyone can create entries within their domain following templates. Qatux maintains the index and cross-references.
- **Hybrid audio generation approach:** Stable Audio Open (SAO) for sounds >200ms with organic character (footsteps, ambient layers). Manual synthesis for sounds <200ms with precise/digital character (UI clicks, scanner beeps). SAO ceiling ~47s; accept 45s loops with crossfade for ambient layers. The insert-tech/organic split ([D-074](content.md#d-074-audio-aesthetic-identity--insert-tech-vs-organic)) maps to synthesis/generation split.
- **Sprint 7 scope expansion:** Ticket #440 expanded from 6 to 8 assets, adding monologue chimes (assets 7-8) as deliberate placeholders with Sprint 8 redo mandate. Chimes are critical for [D-067](perception.md#d-067-recognition-chime-fires-at-onset-of-cognitive-delay) cognitive delay feel but acknowledged as difficult to generate correctly — manual synthesis required for production quality.
- **Sprint 7 scope expansion:** Ticket T-440 expanded from 6 to 8 assets, adding monologue chimes (assets 7-8) as deliberate placeholders with Sprint 8 redo mandate. Chimes are critical for [D-067](perception.md#d-067-recognition-chime-fires-at-onset-of-cognitive-delay) cognitive delay feel but acknowledged as difficult to generate correctly — manual synthesis required for production quality.
- **Raised by:** Ozzie (Round 1 minimum viable proposal, Round 2 full spec), project lead (confirmed, directives #3 and #9). Amendment raised by Inigo (hybrid approach), endorsed by Tyre.
- **Raised by:** Ozzie (Round 1 minimum viable proposal, Round 2 full spec), project lead (confirmed, directives T-3 and T-9). Amendment raised by Inigo (hybrid approach), endorsed by Tyre.
- **Dissent:** Mellanie and Araminta both proposed deferring audio; project lead overruled. Visual sound indicators remain complementary to audio (not replacement).
### D-039: v0.1 wow moment scope — all 6 moments [SUPERSEDED]
@@ -135,7 +135,7 @@ What we're building: game concept, design pillars, prototype definition, map spe
5. **The News Ticker Gut-Punch**: Same ticker, opposite monologue reactions per character. "The asymmetry in three words." Content: ticker + dual monologue reactions.
6. **The Quiet Moment**: Idle in a corridor, unprompted reflective monologue. "I care about this person." Content: 2 monologue lines + time_idle trigger.
- **Cross-reference:** Vertical slice criteria ([D-027](#d-027-vertical-slice--smuggler--detective-two-character-proof)), THE FRIEND ([D-034](content.md#d-034-the-friend--production-level-npc-pattern))
### D-051: "Settling is placement" — design principle
@@ -291,12 +291,12 @@ What we're building: game concept, design pillars, prototype definition, map spe
### D-146: Character creation preview — tile-scale sprite with heavy zoom
- **Date:** 2026-03-17
- **Decision:** The character creation screen shows the player character's game sprite at heavy zoom — the same top-down tile-scale rendering used in gameplay, rendered at high resolution so it holds up when zoomed in. No separate portrait rendering system. Cosmetic choices (hair, clothing, colors) update the zoomed sprite in real time. Rimworld uses this exact approach: the character customisation screen shows the same pawn sprite rendered larger, with clothing/equipment layers visible.
- **Rationale:** The tile-scale sprite IS the player's character for the entire game. Showing it zoomed in character creation: (1) sets accurate expectations for in-game appearance, (2) reuses the production sprite/animation pipeline — zero new rendering infrastructure, (3) validates that cosmetic customisation (#619) reads correctly at game scale, (4) keeps #618 as a single client ticket with no visual-team split. A separate portrait system would be a one-off rendering context (different camera, lighting, asset pipeline) that serves no other game feature. "CK3-style" in the bookmark concept refers to the bookmark structure, not the portrait aesthetic. Portraits can be added later as an additive feature if needed.
- **Team implication:** Ticket #618 (CK3-style character creation screen) stays as a single `client` team ticket. No split required.
- **Rationale:** The tile-scale sprite IS the player's character for the entire game. Showing it zoomed in character creation: (1) sets accurate expectations for in-game appearance, (2) reuses the production sprite/animation pipeline — zero new rendering infrastructure, (3) validates that cosmetic customisation (T-619) reads correctly at game scale, (4) keeps T-618 as a single client ticket with no visual-team split. A separate portrait system would be a one-off rendering context (different camera, lighting, asset pipeline) that serves no other game feature. "CK3-style" in the bookmark concept refers to the bookmark structure, not the portrait aesthetic. Portraits can be added later as an additive feature if needed.
- **Team implication:** Ticket T-618 (CK3-style character creation screen) stays as a single `client` team ticket. No split required.
- **Raised by:** Tyre (architecture), Paula (narrative), Gestalt (systems), confirmed by project lead
- **Dissent:** Ozzie argued for portrait render (emotional attachment). Overruled: tile-scale at heavy zoom provides sufficient character identity without a second rendering pipeline.
- **Cross-reference:** [D-115](#d-115-character-creation-scoped-to-skills--bookmark-for-v02) (creation scope), [D-134](architecture.md#d-134-full-character-customisation--hair-clothing-colors-at-tile-scale) (full character customisation), [D-136](architecture.md#d-136-first-settled-reach-moment-auto-generated-apartment--insert-activation) (first game moment), ticket #618 (CK3-style character creation screen)
- **Cross-reference:** [D-115](#d-115-character-creation-scoped-to-skills--bookmark-for-v02) (creation scope), [D-134](architecture.md#d-134-full-character-customisation--hair-clothing-colors-at-tile-scale) (full character customisation), [D-136](architecture.md#d-136-first-settled-reach-moment-auto-generated-apartment--insert-activation) (first game moment), ticket T-618 (CK3-style character creation screen)
- **Resolves:** Q-WTF-039
---
@@ -378,7 +378,7 @@ What we're building: game concept, design pillars, prototype definition, map spe
- **(b) Atlas highest-level only.** The Atlas renders Sol at its system-level pregenerated map and **nothing more** — no zoom-in, no per-body click-through, no in-world rendering of Sol bodies. The Sol map is a leaf node in the Atlas hierarchy.
- **(c) Deeper Sol is a future DLC.** Any treatment that makes Sol *feel like actual Sol* (real Earth/Mars/Luna geography, real history, real political textures) is reserved for a separate, future DLC with its own scope. Explicitly NOT current work.
- **Rationale:** Sol carries enormous authorial expectation (real Earth/Mars/Luna geography, real human history). Fudging it would land worse than leaving it sealed. Keeping Sol out of scope keeps the Reach the focus and the realism debt manageable; reserving Sol for a hypothetical DLC keeps the door open without committing to the (massive) job now.
- **Implementation:** generation pipelines filter out bodies whose system has `settlement_wave = 'origin'`; the Atlas zoom UI gates Sol at the system map (no body-level navigation panes). Effective immediately on the Phase-4 fill-seam cascade (#1000 founding_age backfill explicitly excludes Sol).
- **Implementation:** generation pipelines filter out bodies whose system has `settlement_wave = 'origin'`; the Atlas zoom UI gates Sol at the system map (no body-level navigation panes). Effective immediately on the Phase-4 fill-seam cascade (T-1000 founding_age backfill explicitly excludes Sol).
- **Raised by:** Jeroen, 2026-05-26 — formalising what was previously implicit in D-223's naming exemption and GJ-0's wiki frontmatter into a single canonical scope rule.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.