docs(decisions): formalize D-194 through D-218 generation cascade records

25 D-records defining the full generation pipeline from heightmap to
walkable tile: WorldTier taxonomy (D-218), settlement classification
(D-196), city generation context (D-200), drainage routing (D-208),
attractor matching (D-211), district mix (D-194), and supporting
enums/types. Produced by workshop #897, formalized from ticket specs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-02 18:40:02 +02:00
co-authored by Claude Opus 4.6
parent 38f9002844
commit 8abb8e4ec1
+477 -1
View File
@@ -761,4 +761,480 @@ Technical foundation decisions that constrain implementation: engine, client-ser
---
*54 decisions. Last updated: 2026-04-21 (D-192 — drop PROTOCOL_VERSION lockstep handshake, sprint 36 client triage)*
### D-194: Three-Component District Mix Algorithm for City District Type Distribution
- **Date:** 2026-05-01
- **Decision:** District type distribution for a generated city is computed from three components combined at generation time:
1. **Population tier guarantees** — minimum district counts enforced by city size. Population tier is `floor(log10(pop / 1_000_000))`, capped at 5. Larger populations guarantee minimum counts of Transit, Commercial, and Residential districts.
2. **10×9 economic multiplier table** — rows are 10 `economic_role` values (manufacturing, financial, agricultural, extraction, service_mixed, institutional, transit_hub, research, military, residential); columns are 9 `DistrictType` variants. Each cell is a weight multiplier (0.03.0) applied to that district type's base probability for cities of that economic role.
3. **Political archetype modifiers** — `PoliticalArchetype` shifts weights for Institutional, Restricted-access, and Civic district types. Corporate archetype boosts Commercial + Restricted. Commission archetype boosts Institutional + Administrative. Pioneer archetype boosts Mixed-use + Organic residential.
- **Founding age character** is applied as a post-mix adjustment to `BlockIrregularity` (see D-216), not to the district type distribution itself.
- The mix is self-contained per city: two cities with the same economic role, population tier, and political archetype produce the same district type distribution (modulo seed-driven noise). No city-to-city state dependency.
- Integer weights throughout — no f32 for D-010 determinism.
- **Rationale:** Economic role should visibly shape a city's physical form. A financial hub looks different from a mining hub. Population tier prevents cities from being too small to sustain their economic function. Political archetype encodes power structure in spatial form — Corporate settlements are commercially dense, Commission settlements are institutionally heavy. The three-component model is the minimum set to produce legible variety; adding more inputs risks over-constraining the generator.
- **Ticket:** #920
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-191 (atlas pipeline), D-213 (DistrictType enum), D-214 (PoliticalArchetype), D-216 (BlockIrregularity)
### D-195: Attractor-Matching Compatibility Matrix for Generative City Placement
- **Date:** 2026-05-01
- **Decision:** City placement on a planetary surface uses an attractor-matching model. A `GeographicAttractor` is a terrain feature that increases city placement score at nearby positions. Seven `AttractorType` variants: `RiverMouth`, `CoastalAccess`, `RiverCrossing`, `ValleyFloor`, `PassEntrance`, `LakeShore`, `PlainCenter`. A `CompatibilityMatrix` is a 10×7 scoring table (10 `economic_role` values × 7 attractor types) whose cells are float weights (0.03.0) representing how strongly that economic role favors that terrain feature. Examples: manufacturing → RiverMouth 2.8, ValleyFloor 2.1; financial → CoastalAccess 2.5, PlainCenter 1.8; agricultural → ValleyFloor 3.0, PlainCenter 2.5. The matrix is authored data (not computed at runtime). Attractor extraction from heightmaps is defined in D-209. Matching algorithm is defined in D-211.
- **Rationale:** Terrain-naive city placement produces spatially incoherent worlds. The compatibility matrix gives different city types different terrain affinities, so financial hubs appear on coasts and agricultural cities appear in fertile valleys — without hard-coding placement rules per city type. The float weight matrix gives graduated preference, not binary requirement.
- **Ticket:** #919, #925
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-191 (atlas pipeline), D-208 (D8 drainage — attractor extraction), D-209 (feature tag extraction), D-211 (attractor-matching pipeline)
### D-196: SettlementClass Enum and Latent Settlement Active/Ghost Logic
- **Date:** 2026-05-01
- **Decision:** Every settlement (city marker in `atlas_city_names`) has a `SettlementClass` that determines how it enters and exits active simulation:
```rust
enum SettlementClass {
NameLocked, // Named in wiki; always active regardless of population
PopulationBudget, // Active if pop > threshold; ghost if below
EconomicTriggered, // Active only while economic role condition is met
OrganicGrowth, // Emergent; generated by simulation, no prior wiki record
}
```
- **Active threshold** (applies to `PopulationBudget`): population ≥ 50,000 for a city to receive full Phase 1 district skeleton generation. Below threshold: 1-district stub with Minimal ComplexityTier.
- **Ghost threshold** (applies to `PopulationBudget`): population < 5,000. Settlement is present in atlas data but receives no NPC population; structures are generated as abandoned (Worn/Derelict condition baseline).
- `NameLocked` settlements bypass both thresholds — they are always simulated regardless of population (handles narrative-significant small towns).
- `EconomicTriggered` settlements collapse to ghost state when their triggering economic condition lapses (e.g., a mining outpost depopulates when the mine is exhausted).
- `OrganicGrowth` settlements are not in `atlas_city_names` at generation time; they are written to the table during simulation when a settlement emerges organically.
- **Rationale:** Not every named location needs full generation, and not every simulated location is named. The classification separates authorial intent (NameLocked) from economic reality (PopulationBudget, EconomicTriggered) and simulation emergence (OrganicGrowth). Ghost settlements are important for world texture — abandoned mining towns and depopulated frontier outposts are as legible as thriving hubs.
- **Ticket:** #913
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-191 (atlas pipeline), D-200 (CityGenerationContext), D-203 (BodyWorldState), D-207 (atlas_city_names)
### D-197: prosperity_baseline Derivation Formula with Topographic Gradient
- **Date:** 2026-05-01
- **Decision:** Each city's `prosperity_baseline` (f32, 0.01.0, used as economic pressure state seed) is derived at generation time from four components:
1. **Economic role base** — lookup per `economic_role` value: manufacturing=0.55, financial=0.70, agricultural=0.50, extraction=0.45, service_mixed=0.60, institutional=0.65, transit_hub=0.60, research=0.65, military=0.55, residential=0.50.
2. **Population log-scale bonus** — `0.04 × floor(log10(pop / 1_000_000 + 1))`, capped at +0.12. Larger cities are generally more prosperous.
3. **Topographic gradient bonus** — terrain features that historically correlate with prosperity add to the baseline: river mouth +0.08, coastal access +0.06, valley floor +0.04, pass entrance +0.03. At most one terrain bonus applies (the highest-scoring attractor at the city's position).
4. **Seed noise** — ±0.05 uniform noise applied last (integer-seeded per city, D-010 determinism).
- Formula: `base + pop_bonus + terrain_bonus + noise`, clamped to [0.1, 0.95].
- `prosperity_baseline` is not the current prosperity level — it is the simulation's starting point and decay/growth target. The live pressure simulation (D-026) drifts from this value based on trade flows, events, and faction pressure.
- **Rationale:** A flat random baseline produces economically incoherent worlds. Terrain-informed prosperity encodes real-world patterns: port cities are wealthy, river-mouth cities are strategic. The log-scale population bonus prevents megacities from dominating without eliminating small-city character. Clamping to [0.1, 0.95] prevents degenerate all-thriving or all-collapsing starting states.
- **Ticket:** #920 (consumer of prosperity_baseline)
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-194 (district mix — consumes prosperity_baseline), D-195 (attractor types that produce terrain bonus)
### D-198: Economic Simulation Independence from Layer 12 Spatial Data
- **Date:** 2026-05-01
- **Decision:** The economics simulation (Phase 2, D-026 background tier) runs independently of Layer 1 (galaxy graph) and Layer 2 (location profiles / planetary topography). Economic state is seeded at game start from `systems.db` data (economic roles, trade flows, corporate presence) and then drifts via the pressure simulation. The generator (Layer 7 district skeleton) reads economic pressure state as an input but does not feed back into the simulation model. The two layers communicate one-way: simulation → generator (pressure state used to set district condition and density), never generator → simulation.
- **Prohibited:** Generator code must not modify `PressureState`. Generator code must not query live simulation state during async background generation tasks (race condition risk). Generator reads a snapshot of pressure state taken at generation dispatch time.
- **Allowed:** The generator reads `economic_health`, `prosperity_baseline`, `industries`, and `faction_influence` from the snapshot. These are read-only inputs to Phase 1 skeleton classification and Phase 2 condition application.
- **Rationale:** Bidirectional coupling between generator and simulation creates initialization order dependencies and potential circular references. The one-way data flow (simulation → generator snapshot → generator) keeps both systems independently testable and avoids race conditions in the Rayon thread pool (D-206). The generator is a consumer of economic state, not a participant in economic evolution.
- **Ticket:** #915 (CityGenerationContext reads economic snapshot)
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-026 (simulation tiers), D-200 (CityGenerationContext), D-206 (background generation queue)
### D-199: 6-Field Minimum Economic Read Set for City Generation Context
- **Date:** 2026-05-01
- **Decision:** When building a `CityGenerationContext` (D-200), the generator reads exactly 6 fields from the economic pressure snapshot per city. Reading more fields is permitted but these 6 are the minimum required for correct Phase 1 skeleton classification:
1. `economic_role` — primary function of the city (determines DistrictType distribution via D-194)
2. `prosperity_baseline` — starting economic health (0.01.0, see D-197)
3. `population` — city population (determines ComplexityTier ceiling, BlockSkeleton density)
4. `dominant_faction` — faction with highest `faction_influence` at this location (affects Institutional and Restricted district bias)
5. `founding_age_years` — years since settlement founding (drives BlockIrregularity via D-216, era distribution)
6. `settlement_class` — `SettlementClass` enum value (D-196, determines whether to generate at all)
- Fields 15 are read from `systems.db` (bodies table + economics tables). Field 6 is derived at generator dispatch time.
- All 6 fields must be present before a generation task is dispatched. Missing fields abort the task with a logged error; generation does not proceed with partial context.
- **Rationale:** A fixed minimum read set prevents generators from accumulating unbounded dependencies on simulation state. The 6 fields cover the minimum information needed to produce a correctly-classified skeleton. The abort-on-missing-fields rule ensures generator output is always deterministic from a complete context, never silently degraded from a partial one.
- **Ticket:** #915 (CityGenerationContext implementation)
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-196 (SettlementClass), D-197 (prosperity_baseline), D-198 (economic simulation independence), D-200 (CityGenerationContext struct)
### 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_body_heightmaps`, `atlas_city_names`, `atlas_province_boundaries`, `body_radius_km`. Output is a static artifact committed to the repo. Never runs during gameplay.
2. **Runtime-background (Rayon thread pool, D-206):** Triggered by content-spidering events (player approaches a system, NPC names a location, news ticker references a place). Runs D8 drainage analysis (D-208), attractor extraction (D-209), settlement placement, and Phase 1 district skeleton generation. Output goes into `BodyWorldState` cache (D-203). Transparent to main tick thread.
3. **Runtime-on-demand (main tick thread):** Triggered when the player crosses a chunk boundary. Runs Phase 2 chunk fill for the approaching chunk. Must complete within 5ms. Reads from `BodyWorldState` cache (always populated before this tier runs).
- **Tier boundary rules:**
- Build-time outputs are read-only at runtime.
- Runtime-background tasks read from `systems.db` and write to `BodyWorldState` only.
- Runtime-on-demand reads from `BodyWorldState` and writes to the active ECS world (chunk tile data, NPC spawns).
- No tier may write to a higher tier's outputs. No circular dependencies.
- `CityGenerationContext` struct (see below) is the data contract between tiers 1→2.
```rust
struct CityGenerationContext {
city_id: u64,
political_archetype: PoliticalArchetype,
prosperity_baseline: f32,
surrounding_biome: SettingType,
road_entry_directions: Vec<u8>, // compass octants (07)
footprint_radius_km: f32,
founding_orientation: FoundingOrientation,
world_tier: WorldTier,
}
```
- **Rationale:** Three tiers with explicit boundaries eliminates the "where does this code run?" question. Build-time is deterministic and committable. Runtime-background is parallelizable. Runtime-on-demand has strict latency budgets. Cross-tier mutation would create race conditions between the Rayon thread pool and the main tick thread.
- **Ticket:** #915
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-026 (simulation tiers), D-194 (district mix), D-203 (BodyWorldState), D-206 (background generation queue), tyre-sw1r3.md Layer 5/6 architecture
### D-201: Spatial Hierarchy — Eight Tiers with Locked Dimensions
- **Date:** 2026-05-01
- **Decision:** The generation pipeline has eight spatial tiers from galaxy to tile. Dimensions are locked and cannot be changed without amending this decision:
| Tier | Name | Dimensions | Purpose |
|------|------|------------|---------|
| 1 | Galaxy | 300 systems | Galaxy graph, gate topology, cultural corridors |
| 2 | System | — | Orbital mechanics, body catalog |
| 3 | Body | ~512×256 pixels (equirectangular heightmap) | Planetary topography, climate zones |
| 4 | Region | ~50500km | Province boundaries (watershed-derived, D-205), biome zones |
| 5 | Settlement | ~130km radius | City footprint, district layout |
| 6 | District | 512×512 sim tiles (256m) | Phase 1 skeleton, 4×4 block grid (D-094) |
| 7 | Block | 128×128 sim tiles (64m) | Generator planning unit, 2×2 chunks (D-094) |
| 8 | Chunk | 64×64 sim tiles (32m) | Streaming/serialization unit (D-094) |
- Tiers 68 are locked by D-094 (district spatial hierarchy). This decision formalizes Tiers 15 with equivalent lock status.
- Tier 3 heightmap resolution (512×256 equirectangular at 1024×512 PNG) is the canonical format. Deviation requires amending D-191.
- 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 Tier 6 (district generation).
- **Rationale:** Locking spatial dimensions prevents the generative layers from drifting in incompatible directions. The heightmap pipeline, atlas pipeline, and district generator all assume these dimensions and would need coordinated migration if they changed. Formalization prevents silent per-system variation.
- **Ticket:** #912 (WorldTier enum), #913 (SettlementClass)
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-094 (district hierarchy — Tiers 68), D-191 (atlas pipeline — Tier 3), D-205 (province boundaries — Tier 4), D-208 (D8 drainage — Tier 3 analysis)
### D-202: Heightmap BLOB Storage Schema (atlas_body_heightmaps)
- **Date:** 2026-05-01
- **Decision:** Heightmap elevation data is stored in `systems.db` as a BLOB in the `atlas_body_heightmaps` table. Schema:
```sql
CREATE TABLE atlas_body_heightmaps (
body_id INTEGER PRIMARY KEY REFERENCES bodies(id),
width INTEGER NOT NULL, -- pixel columns (canonical: 512)
height INTEGER NOT NULL, -- pixel rows (canonical: 256)
data BLOB NOT NULL, -- float32 little-endian, row-major, width×height floats
sea_level REAL NOT NULL DEFAULT 0.0,
imported_at TEXT NOT NULL DEFAULT (datetime('now'))
);
```
- `data` is a `float32` little-endian BLOB. Size: `width × height × 4` bytes. Canonical: 512×256×4 = ~512KB per body.
- Values are normalized elevation in [0.0, 1.0]. `sea_level` is the fraction below which terrain is underwater (default 0.0 = no ocean, overridden per body).
- The Rust loader reads the BLOB via `bytemuck::cast_slice::<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.
- **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.
- **Ticket:** #901 (schema), #906 (import), #916 (Rust loader)
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-191 (atlas pipeline — heightmap source), D-203 (BodyWorldState — consumer), D-208 (D8 drainage — reads this table)
### D-203: BodyWorldState Bevy Resource with LRU Cache
- **Date:** 2026-05-01
- **Decision:** `BodyWorldState` is a Bevy `Resource` holding the Layer 12 output for each recently-accessed planetary body. It functions as an LRU (Least Recently Used) cache:
- **Cache capacity:** 50 bodies.
- **Memory budget:** ~5MB total (50 bodies × ~100KB per entry average). A single body's Layer 12 data includes: processed heightmap (float32 grid, ~512KB pre-downsampled to ~8KB working resolution), river network (`RiverNetwork` struct: river cells, confluences, mouths), drainage basin polygons, attractor list, province boundary references.
- **Eviction policy:** On cache overflow, evict the body with the oldest `last_accessed` timestamp. Bodies that are the current player location or adjacent-system neighbors are pinned (not evicted).
- **Population:** The runtime-background tier (D-200) populates cache entries via Rayon tasks. Main thread reads are always from the cache; main thread code must never perform blocking DB reads for heightmap data.
- **Struct:**
```rust
struct BodyWorldState {
body_id: u64,
heightmap: Vec<f32>, // downsampled working grid
river_network: RiverNetwork, // D-208 output
drainage_basins: Vec<DrainageBasin>,
attractors: Vec<GeographicAttractor>, // D-195 types
last_accessed: SimTick,
}
```
- The resource is initialized empty and populated on demand. Accessing a body not in the cache triggers a background generation task (D-206).
- **Rationale:** The D8 drainage analysis (D-208) and attractor extraction (D-209) are expensive (target: ~50ms/body). Running them on the main tick thread would cause frame drops. The LRU cache ensures the main thread only reads pre-computed data. 50-body capacity covers the typical gameplay scenario (player in one system, neighboring system pre-cached) with margin.
- **Ticket:** #917
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-200 (three-tier execution model), D-202 (heightmap BLOB — input), D-206 (background generation queue — populates cache), D-208 (D8 drainage — produces river network)
### D-204: body_radius_km Column on bodies Table
- **Date:** 2026-05-01
- **Decision:** A `body_radius_km REAL` column is added to the `bodies` table in `systems.db`. This value is the mean radius of the planetary body in kilometers, used to:
- Compute `area_count` (number of districts a settlement can contain, scales with surface area)
- Convert province boundary pixel coordinates to real-world km distances
- Derive the `footprint_radius_km` field on `CityGenerationContext` (D-200)
- Schema change: `ALTER TABLE bodies ADD COLUMN body_radius_km REAL` (nullable, populated by import step)
- **Fallback derivation** (applied when `body_radius_km IS NULL`): `planet_class` lookup table with canonical radii:
- `super_earth`: 8,000 km
- `earth_like`: 6,371 km
- `sub_earth`: 4,500 km
- `ocean_world`: 6,500 km
- `arid`: 5,800 km
- `ice_world`: 3,000 km
- `gas_giant`: 50,000 km (no settlements)
- `moon`: 1,737 km
- `other` / unknown: 6,371 km (Earth default)
- Fallback is applied at query time, not stored back. The column remains NULL until authoritative data is available.
- **Rationale:** Surface area scales with radius squared; a body twice Earth's radius has four times the potential settlement density. Without this field the generator must use a flat default for all planets, producing physically implausible city counts on super-earths and moons alike. The fallback ensures the generator works before all bodies have explicit radius data.
- **Ticket:** #905 (schema), #910 (populate from planet_class fallback)
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-191 (atlas pipeline — body catalog), D-200 (CityGenerationContext — footprint_radius_km)
### D-205: Province Boundary Pre-Computation (atlas_province_boundaries)
- **Date:** 2026-05-01
- **Decision:** Province boundaries (drainage basin divides) are pre-computed at build time from the D8 drainage analysis (D-208) and stored in `atlas_province_boundaries`:
```sql
CREATE TABLE atlas_province_boundaries (
body_id INTEGER NOT NULL REFERENCES bodies(id),
basin_id INTEGER NOT NULL,
path TEXT NOT NULL, -- JSON array [[row, col], ...] pixel-space polyline
area_pct REAL NOT NULL, -- fraction of body surface area in this basin
PRIMARY KEY (body_id, basin_id)
);
```
- Boundaries are stored as pixel-space polylines in the same `[row, col]` convention as `markers.json` (D-191 §8 canonical format).
- `area_pct` is the fraction of the body's total surface area contained within this drainage basin.
- Province boundaries are the basis for district-level political zoning and cultural corridor assignment at Tier 4 (Region) in D-201.
- **Province count target:** 412 provinces per inhabited body, derived naturally from watershed analysis. Bodies with less topographic relief (plains worlds, ocean worlds) produce fewer, larger provinces.
- **At runtime:** Province boundaries are read from `atlas_province_boundaries` at generation dispatch time and cached in `BodyWorldState` as `drainage_basins` (D-203). They are not re-computed at runtime.
- **Rationale:** Province boundaries define the cultural geography of a world — the mountain ranges and river systems that separated civilizations and produced distinct regional identities. Pre-computing them at build time keeps the runtime-background tier focused on city placement and district generation rather than watershed analysis. Storing as polylines (not rasterized masks) keeps the table compact and human-readable.
- **Ticket:** #904 (schema), #907 (populate from watershed analysis)
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-191 (atlas pipeline), D-201 (spatial hierarchy — Tier 4 Region), D-203 (BodyWorldState — caches province data), D-208 (D8 drainage — source of basin divides)
### D-206: Background Generation Priority Queue and Rayon Thread Infrastructure
- **Date:** 2026-05-01
- **Decision:** All non-urgent generator work runs through a prioritized Rayon thread pool:
- **Thread count:** `available_parallelism - 2`, minimum 1. Reserves 2 cores for the main tick thread and Bevy scheduler.
- **Priority queue:** Four levels: `Immediate` (player will arrive within 1 game-minute), `High` (player will arrive within 5 minutes), `Medium` (player is in the same system), `Low` (player has seen or heard of this location via NPC or news). Work items at higher priority pre-empt lower-priority items.
- **Work item types:** `AnalyzeBody(body_id)` (D8 drainage + attractor extraction), `GenerateSkeleton(city_id, context)` (Phase 1 DistrictSkeleton), `FillChunk(district_id, block_pos)` (Phase 2 chunk fill for pre-loading).
- **Event-driven pre-generation:** A `SystemNameIndex` (Aho-Corasick automaton over all body/system names from `systems.db`) scans NPC dialogue output and news ticker text. When a scan match hits, the referenced body is queued at `Low` priority if not already cached. This is the mechanism by which "NPC mentions a place → player travels there → world is already generated on arrival."
- **Completion notification:** Completed tasks send a `GenerationComplete` event to the main tick thread via a `crossbeam` channel. The main thread drains this channel once per tick.
- **Rationale:** The Rayon thread pool handles the D-200 runtime-background tier. The priority queue prevents low-priority speculation from blocking urgent work (player approaching). The Aho-Corasick name index enables cheap always-on scanning — NPC dialogue is low-bandwidth enough that scanning every output line has negligible cost. Pre-generation triggered by narrative content (NPC mentions a place) is the mechanism for making the world feel pre-existing rather than loading-on-demand.
- **Ticket:** #924 (background queue), #926 (SystemNameIndex)
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-200 (three-tier execution model — runtime-background tier), D-203 (BodyWorldState — output of background tasks), D-208 (D8 drainage — enqueued as AnalyzeBody)
### D-207: Fully Generative Placement — markers.json Stripped to Topographic Features
- **Date:** 2026-05-01
- **Decision:** The `atlas_city_names` table replaces the authored city positions in `markers.json`. Going forward, `markers.json` files contain only topographic features (rivers, oceans, mountain ranges — per D-191 §8 canonical format). City positions, road networks, and rail networks are NOT authored in `markers.json`; they are generated from the terrain data and stored in `atlas_city_names` and derived tables.
```sql
CREATE TABLE atlas_city_names (
id INTEGER PRIMARY KEY,
body_id INTEGER NOT NULL REFERENCES bodies(id),
name TEXT NOT NULL,
economic_role TEXT NOT NULL,
population INTEGER NOT NULL,
corp_id INTEGER REFERENCES corporations(id), -- nullable, corp HQ if applicable
reserved INTEGER NOT NULL DEFAULT 0, -- 1 = reserved for authored scenario use
kind TEXT NOT NULL DEFAULT 'city' -- 'capital' | 'city'
);
```
- `name` and position in the markers.json come from different sources: position is generated by the city placement algorithm; name is either authored (wiki), LLM-generated (Gemma 2 naming pipeline), or reserved (corp HQ name). The split allows position generation and naming to run independently.
- `corp_id` links to the `corporations` table when a city is a corporation's headquarters or major hub city.
- `reserved = 1` rows are scenario-specific cities that must not be relocated by the generation algorithm; the generator places other cities around them.
- **`markers.json` authored city data** (hand-written city center positions in the 6 hand-authored templates: Lendel, Edict, Vuurkloof, Røros, Cairnside, Estrade) is migrated to `atlas_city_names` and treated as `reserved = 1` rows. The markers.json files for these templates then have their city arrays cleared.
- **Rationale:** Authored city positions in markers.json created a split between hand-authored content and procedurally generated content that was impossible to query, diff, or validate consistently. Moving city identity to a table allows: SQL joins against economic data, corp HQ cross-references, scenario reservations, and attractor-matching validation. The topographic features (rivers, mountains) remain in JSON because they are polygon/polyline geometry better suited to JSON than relational rows.
- **Ticket:** #902 (schema), #908 (populate from wiki), #909 (corp HQ cross-reference)
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-191 (atlas pipeline — markers.json canonical format), D-196 (SettlementClass — column in atlas_city_names), D-200 (CityGenerationContext — reads from this table)
### D-208: D8 Priority-Flood Drainage Routing — Layer 1 Empty World
- **Date:** 2026-05-01
- **Decision:** Drainage routing (the computation of flow direction, flow accumulation, and river network extraction) uses the **D8 priority-flood** algorithm on the body's float32 heightmap. This is the first Layer 1 computation run on a body before any city placement or attractor extraction.
- **Algorithm:** D8 assigns each cell's flow direction to one of 8 neighbors based on the steepest descent. Priority-flood fills depression cells before routing to avoid spurious sinks. Flow accumulation is the count of upstream cells draining through each cell.
- **River threshold:** A cell is classified as a river cell when `flow_accumulation > 200`. This threshold produces river networks of realistic density on canonical 512×256 heightmaps.
- **Outputs** stored in `BodyWorldState.river_network`:
- `river_cells: Vec<(u16, u16)>` — pixel positions of all river cells
- `confluences: Vec<(u16, u16)>` — positions where two or more rivers merge
- `mouths: Vec<(u16, u16)>` — positions where rivers reach sea level or the heightmap edge
- **Province/basin output:** Cells that divide adjacent drainage basins become province boundary candidates (D-205). Boundaries are traced as polylines after flow accumulation is complete.
- **Performance target:** ~50ms per body on a single Rayon thread for canonical 512×256 resolution.
- **Determinism:** Integer-only arithmetic throughout. No f32 in the priority-flood comparisons (use integer-scaled elevation). D-010 compliant.
- **Rationale:** D8 is the standard GIS drainage routing algorithm and produces the river networks that drive attractor scoring (river mouths, confluences = high-value `RiverMouth` attractors). The flow accumulation threshold of 200 was chosen empirically against the Lendel heightmap to produce ~815 named rivers per inhabited body — enough for cultural geography without over-fragmenting the landscape.
- **Ticket:** #918
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-195 (attractor types — river mouth is highest-scoring), D-203 (BodyWorldState — output stored here), D-205 (province boundaries — derived from drainage divides), D-209 (feature tag extraction — reads river network)
### D-209: Geographic Feature Tag Extraction (7 Settlement Attractor Tags)
- **Date:** 2026-05-01
- **Decision:** After D8 drainage analysis, 7 `AttractorType` tags are extracted from the heightmap + river network and stored as `Vec<GeographicAttractor>` in `BodyWorldState`. Each attractor has a position `[row, col]` and a `strength: f32` (0.01.0) derived from local terrain quality.
- **Extraction rules per type:**
- `RiverMouth`: cells in `river_network.mouths`. Strength = `flow_accumulation[cell] / max_flow_accumulation` (normalized). Always high-value.
- `CoastalAccess`: cells within 3 pixels of a sea/ocean polygon (from `oceans[]` in markers.json), not already `RiverMouth`. Strength = 0.6 baseline + coast length bonus.
- `RiverCrossing`: cells at confluences or where a river crosses a topographic saddle. Strength = `flow_accumulation / max_flow_accumulation × 0.7`.
- `ValleyFloor`: local elevation minima in non-river cells with positive habitability score (slope < 5°, elevation 1060% of range). Strength = habitability score.
- `PassEntrance`: local saddle points between adjacent drainage basins. Strength = inverse of elevation percentile (lower passes score higher).
- `LakeShore`: cells adjacent to `lake` polygons in markers.json. Strength = 0.5 baseline.
- `PlainCenter`: cells in flat terrain (slope < 2°) away from all other attractors. Strength = habitability score × 0.4.
- Sub-biome classification (vegetation, aridity, temperature zones) is derived in parallel and stored as `SubBiomeVariant` on the attractor for use by the ZonePalette modifier system (D-101).
- **Rationale:** The 7 attractor types cover the terrain features that historically determine city placement. Their extraction from the heightmap is deterministic and cheap given the D8 analysis is already complete. The strength normalization ensures attractor scores are comparable across bodies with different elevation ranges.
- **Ticket:** #925 (types), #919 (matching pipeline that consumes these)
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-195 (CompatibilityMatrix — attractor types), D-203 (BodyWorldState — storage), D-208 (D8 drainage — prerequisite), D-211 (attractor-matching pipeline — consumer)
### D-210: Sub-Biome Variant Classification and terrain_modification_cost
- **Date:** 2026-05-01
- **Decision:** Each `GeographicAttractor` (D-209) carries a `sub_biome: SubBiomeVariant` tag that classifies the local terrain more finely than the top-level `SettingType`. This drives two systems: ZonePalette modifier selection (which visual variant to use) and `terrain_modification_cost` (how expensive it is to build infrastructure at this location).
- `SubBiomeVariant` values: `TropicalWet`, `TemperateForest`, `TemperateGrassland`, `BorealForest`, `Tundra`, `Desert`, `Savanna`, `Alpine`, `Wetland`, `CoastalLowland`, `Volcanic`.
- `terrain_modification_cost: f32` (1.0 = baseline, higher = more expensive): derived from sub-biome + local slope. Flat grassland = 1.0. Volcanic = 4.5. Wetland = 3.2. Alpine = 3.8. Coastal lowland = 1.4. Used by the attractor-matching pipeline (D-211) to penalize high-cost terrain for economically marginal cities.
- Sub-biome classification uses: elevation percentile (of body total), local slope, moisture proxy (distance to nearest river mouth or coast), and temperature proxy (latitude of the equirectangular pixel).
- Sub-biome data is stored in `BodyWorldState` alongside the attractors; it is not a separate DB table.
- **Rationale:** Two cities on coastal terrain feel different when one is a tropical lowland port and the other is a cold Nordic fjord. Sub-biome tags enable the ZonePalette to select the correct visual register (T6 beach/coastal with tropical modifier vs T7 mountain/high with coastal modifier). The `terrain_modification_cost` gives the generator a principled reason to prefer some attractor positions over others for lower-prosperity cities.
- **Ticket:** #919 (attractor matching — uses terrain_modification_cost)
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-101 (ZonePalette modifier system — consumes sub_biome), D-195 (attractor types), D-209 (feature tag extraction — assigns sub_biome)
### D-211: Attractor-Matching Five-Phase Pipeline for Settlement Placement
- **Date:** 2026-05-01
- **Decision:** Given a body's `Vec<GeographicAttractor>` and a set of cities from `atlas_city_names`, settlement placement runs a five-phase matching pipeline:
1. **Score matrix build:** Compute a `city_count × attractor_count` score matrix. Each cell = `CompatibilityMatrix[economic_role][attractor_type] × attractor.strength × (1.0 / terrain_modification_cost)`.
2. **Tier A greedy assignment:** For each city with `SettlementClass::NameLocked` or population ≥ 1,000,000, assign the highest-scoring unoccupied attractor using greedy selection. These cities must be placed first to anchor the spatial layout.
3. **Hungarian algorithm for Tier B+C:** Apply the Hungarian algorithm to the remaining cities (population 50,000999,999) and remaining attractors. Produces optimal global assignment maximizing total score.
4. **Synthetic attractor overflow:** Cities that cannot be matched to a real attractor (attractor pool exhausted) receive a synthetic `PlainCenter` attractor generated at a position that respects minimum city spacing (15 pixels minimum on 512×256 grid = ~50km minimum separation).
5. **Name fulfillment check:** After placement, verify that all `atlas_city_names` entries for this body have been assigned a position. Log a warning for any unplaced city.
- **Two-tier mismatch flagging:** If a matched city-attractor pair has score < 0.35, log a `WARNING` (below expected quality). If score < 0.15, log an `ERROR` and flag for manual review. Generation proceeds in both cases; the flags are for content auditing, not hard blockers.
- **Output:** `Vec<CityPlacement { city_id, position: [row, col], attractor: AttractorType, score: f32 }>` written to `atlas_city_positions` at build time.
- **Rationale:** Greedy-first for large/locked cities ensures anchor cities (capitals, corp HQs, wiki-named cities) are placed at terrain features that match their lore role. Hungarian for medium cities finds the globally optimal assignment, not just locally optimal. Synthetic overflow prevents the algorithm from failing on bodies where city count exceeds natural attractor count (dense, flat worlds). The two-tier warning system enables content QA without blocking generation.
- **Ticket:** #919, #925
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-195 (CompatibilityMatrix), D-207 (atlas_city_names), D-209 (GeographicAttractor — input), D-210 (terrain_modification_cost — input)
### D-212: TerritorialStatus Priority-Ordered Derivation Algorithm
- **Date:** 2026-05-01
- **Decision:** Each `Province` (watershed-derived drainage basin, D-205) receives a `TerritorialStatus` value derived by priority-ordered classification at generation time:
```rust
enum TerritorialStatus {
CommissionControlled, // Commission faction_influence ≥ 0.6 in this province
CorpTerritory, // Single corporation faction_influence ≥ 0.5
ContestedZone, // Two or more factions each ≥ 0.3, no dominant faction
FrontierUnclaimed, // No faction with influence ≥ 0.2
IndigenousHeld, // Cultural corridor has indigenous autonomy flag
Derelict, // population_density < 0.01 AND no faction ≥ 0.1
}
```
- Classification applies checks in priority order: `CommissionControlled` checked first, `Derelict` last. The first condition that is true sets the status.
- `placed_at_generation: bool` flag on `Province` distinguishes classification at build time (true) from runtime re-classification during simulation (false). Build-time status is the starting state; simulation can change it, and the flag ensures the original classification is recoverable for reset/new-game scenarios.
- Faction influence values are read from `systems.db` (economics tables) at build time using the same D-199 economic read pattern.
- **Rationale:** Territory status is a high-level descriptor visible to the player on the Atlas overlay (D-191 §7, political zones overlay). It must be derivable from the generation inputs without runtime simulation state. The priority-ordered algorithm ensures clear, predictable classification — no ambiguous provinces. The `placed_at_generation` flag enables the game to show "how this province was at settlement time" vs. "how it is now."
- **Ticket:** #921
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-191 (atlas overlay — political zones), D-199 (economic read set), D-205 (Province — this status is a field on it)
### D-213: FoundingOrientation Enum and Spatial Grid Rotation
- **Date:** 2026-05-01
- **Decision:** `FoundingOrientation` describes the primary spatial axis of a city's original street grid, derived from the terrain feature that anchored the founding settlement. It controls the rotation of the district grid skeleton.
```rust
enum FoundingOrientation {
Coastal { facing_degrees: u16 }, // street grid perpendicular to coastline
RiverAligned { bearing_degrees: u16 }, // street grid parallel to founding river
TerrainFollowing, // grid rotated to follow local contours
Cardinal, // grid aligned to N/S/E/W (commission-planned)
Free { bearing_degrees: u16 }, // arbitrary bearing (pioneer settlements)
}
```
- `facing_degrees` and `bearing_degrees` are integer degrees 0359 (0 = North, clockwise). Integer to preserve D-010 determinism.
- The founding orientation is derived from the matched attractor type (D-211): `RiverMouth` → `Coastal`; `RiverAligned`; `CoastalAccess` → `Coastal`; `ValleyFloor` → `TerrainFollowing`; `PlainCenter` + Commission-controlled province → `Cardinal`; `PlainCenter` + other → `Free`.
- The district skeleton generator (Phase 1) applies `FoundingOrientation` as the base rotation for the outermost district ring. Interior districts inherit the orientation unless overridden by a `PoliticalArchetype` modifier.
- **Hard constraint:** Maximum ±45° deviation from the parent orientation per district (same limit as D-096 `BlockPlacement.rotation_steps`). Beyond ±45°, tile-based pathfinding produces movement artifacts.
- **Rationale:** Street grids reflect the terrain and founding logic of the original settlement. Roman camps faced cardinal directions. River towns align with the river. Coastal cities face the water. Encoding this as a named enum rather than a raw angle makes the orientation legible in the data model and debuggable during generation.
- **Ticket:** #914
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-096 (DistrictLayoutMode — inherits orientation), D-211 (attractor-matching — derives orientation), D-214 (PoliticalArchetype — may override orientation), D-215 (spatial arrangement patterns — uses orientation)
### D-214: PoliticalArchetype Enum and Settlement Spatial Character
- **Date:** 2026-05-01
- **Decision:** `PoliticalArchetype` classifies a settlement's dominant power structure and its physical expression in district layout:
```rust
enum PoliticalArchetype {
Commission, // Top-down Commission planning; rectilinear, institutional core
Corporate, // Corp-dominated; commercial density, restricted zones, campus blocks
Pioneer, // Self-organized; organic growth, mixed use, ad-hoc infrastructure
Military, // Garrison or fortification origin; defensible geometry, restricted perimeter
Academic, // University or research origin; campus-quad structure, green space
Industrial, // Factory-first; large-footprint industrial blocks, worker residential rings
}
```
- `PoliticalArchetype` is derived at generation time from `TerritorialStatus` (D-212) + `economic_role`: `CommissionControlled` province → `Commission`; `CorpTerritory` → `Corporate`; `FrontierUnclaimed` → `Pioneer`; military economic role → `Military`; research economic role → `Academic`; manufacturing + extraction → `Industrial`.
- When multiple signals conflict (e.g., Commission-controlled manufacturing hub), `TerritorialStatus` takes precedence over `economic_role` for archetype derivation.
- **Spatial effect on district mix:** See D-194. Each archetype applies weight multipliers to district type selection.
- **`AttractorAssignment` disambiguation:** `OrganicGrowth` (a `DistrictType` value and also an `EraCause` value) is always unambiguous in context. On `DistrictType`, it means the district grew without a planning mandate. As `EraCause`, it means the era tag was acquired through organic settlement expansion rather than a discrete historical event. Both usages are permitted; the type system distinguishes them.
- **Rationale:** Power structure should be legible in a city's spatial form without the player reading a wiki entry. Commission cities look different from Corporate cities look different from Pioneer cities — not just in palette, but in street geometry, district type distribution, and building scale. Encoding this as a named enum ensures the distinction is consistent across all generation code.
- **Ticket:** #914
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-194 (district mix — archetype modifiers), D-212 (TerritorialStatus — primary input), D-213 (FoundingOrientation — archetype may override), D-215 (spatial arrangement patterns)
### D-215: Five Explicit Political Archetype Spatial Arrangement Patterns
- **Date:** 2026-05-01
- **Decision:** Each `PoliticalArchetype` maps to one of five spatial arrangement patterns that govern district adjacency and the placement of landmark multi-block reservations:
1. **Radial core** (Commission, Academic): Central landmark (civic square, institutional plaza, or university quad) surrounded by mixed-use rings. Transit spokes radiate outward. Districts are denser near center.
2. **Campus grid** (Corporate): Restricted campus block occupies 24 blocks in the district interior. Commercial districts ring the exterior. Worker residential on periphery.
3. **Ribbon development** (Pioneer, Industrial): Districts string along a linear feature (river, road, industrial rail). No dominant center. Mixed adjacency at every edge.
4. **Fortified perimeter** (Military): Restricted and Secured districts at the edge of the footprint. Open access in the interior core. Single controlled access point per district edge.
5. **Hub-and-spoke** (transit_hub economic role, any archetype): Transit district at center, all other district types accessible via direct corridors. Maximum 2-district travel between any two districts.
- The arrangement pattern constrains block adjacency during Phase 1 skeleton generation. Specifically: the first 23 districts placed in a settlement follow the pattern. Later districts are constrained only by the road network, not by the pattern.
- Arrangement patterns must **vary in angular orientation** per seed (not just position) — the same archetype's radial core must not always face the same direction across seeds.
- **Rationale:** The 14 D-ready items from the generator-architecture workshop established that spatial arrangement should encode power structure. These five patterns are the minimal set to cover the 6 archetypes (Pioneer and Industrial share ribbon development; hub-and-spoke is a cross-archetype pattern for transit-primary cities). Pattern variation in angular orientation prevents players from pattern-matching settlement layout after the first playthrough.
- **Ticket:** #914 (types), #899 (implementation — Phase 1 skeleton generator)
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-094 (spatial hierarchy — district sizes), D-194 (district mix — archetype modifiers), D-214 (PoliticalArchetype — pattern assignment)
### D-216: BlockIrregularity from founding_age — Layout Age Character
- **Date:** 2026-05-01
- **Decision:** `block_irregularity: f32` is a derived value on each block (range 0.01.0) that controls how much a block deviates from the district's canonical grid. It is computed from `founding_age_years` and `PoliticalArchetype`. The formula:
```
base_irregularity = (founding_age_years / 1000.0).min(1.0)
archetype_step = match archetype {
Commission | Military => -0.3, // suppresses organic deviation
Corporate | Academic => -0.1,
Industrial => 0.0,
Pioneer => +0.3,
}
block_irregularity = (base_irregularity + archetype_step).max(0.05).min(1.0)
```
- Minimum 0.05 is enforced — no block is perfectly regular, even new Commission-planned settlements.
- `block_irregularity` feeds the `BlockPlacement.offset` magnitude in `DistrictLayoutMode::Organic`: `max_offset_sim_tiles = (block_irregularity × 16.0) as i16`.
- An old Pioneer settlement (age 800+ years) can have `block_irregularity ≈ 1.0`, producing maximum ±16 sim tile offsets and ±45° rotations. A new Commission district (age < 50 years) will have `block_irregularity ≈ 0.05`.
- All arithmetic uses integer-scaled intermediates wherever possible (age is integer years; archetype_step is stored as integer basis points internally). The f32 in the formula above is for documentation clarity only.
- **Rationale:** Age is the single most reliable predictor of urban irregularity in the real world. Old cities that grew organically have crooked streets; new planned cities have grids. Encoding this as a formula rather than a lookup table allows continuous variation along the age axis while preserving the political meaning of the archetype modifier.
- **Ticket:** #922
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-096 (DistrictLayoutMode::Organic — consumes block_irregularity), D-194 (district mix — founding_age is an input), D-214 (PoliticalArchetype — archetype_step source)
### D-217: Tile Condition Thresholds (0.63 / 0.43 / 0.23)
- **Date:** 2026-05-01
- **Decision:** A tile's visual condition is derived from the district's `prosperity_score` (live pressure simulation value, 0.01.0) using four threshold bands:
| Band | Condition | prosperity_score range | Tile visual state |
|------|-----------|----------------------|-------------------|
| 1 | Intact | > 0.63 | Clean, undamaged, well-maintained |
| 2 | Worn | 0.43 0.63 | Scuff marks, minor discoloration, partial repairs |
| 3 | Cracked | 0.23 0.43 | Visible damage, incomplete repair, graffiti |
| 4 | Broken | < 0.23 | Structural damage, debris, derelict appearance |
- **Cache invalidation:** A tile's condition only changes when `prosperity_score` crosses a threshold boundary (from band N to band N±1). This avoids per-tick visual updates. The simulation checks threshold crossings once per game-minute (D-031 day-phase tick rate).
- **Baseline floor:** The block's `EraCause` sets a minimum condition floor:
- `Decay` era: minimum Cracked (no tile in a Decay-era block is ever Intact or Worn without an active renovation event)
- `EmergencyExtension` era: minimum Worn
- All other eras: no floor (condition follows prosperity_score freely)
- **Phase 2 application:** Chunk fill applies the baseline condition at fill time. Subsequent condition updates from simulation crossing thresholds are applied as `ChunkMutations.tile_overrides`.
- Condition thresholds are authored constants, not computed. Any change to the thresholds (0.63 / 0.43 / 0.23) requires amending this D-record.
- **Rationale:** Threshold-crossing invalidation is a standard visual LOD technique that avoids expensive per-frame recalculation. The four bands (Intact/Worn/Cracked/Broken) match the visual fidelity budget for the current art direction — more bands require more tile variants per palette. The era-based floor ensures that historical context is always visible: a Decay-era block cannot spontaneously look pristine from a prosperity spike alone.
- **Ticket:** #923
- **Raised by:** Generation cascade workshop (#897)
- **Cross-reference:** D-100 (DamageOverlay — post-condition modification), D-194 (district mix — prosperity_baseline is the seed for prosperity_score)
### D-218: WorldTier Enum Canonical Values (Epicenter/Regional/Backwater/Passage/Waypoint)
- **Date:** 2026-05-01
- **Decision:** The canonical `WorldTier` enum values are:
```rust
enum WorldTier {
Epicenter, // Hub system. Full simulation. High faction pressure. Multi-district cities.
Regional, // Regional hub. 14 districts per city. Partial full-budget districts.
Backwater, // Small community. Dense isolated settlement. Full sim budget — NOT capped.
Passage, // Transit stop. Pass-through. ComplexityTier ceiling: Moderate.
Waypoint, // Not simulated until player approaches. ComplexityTier ceiling: Minimal.
}
```
- The values `Peripheral`, `Connected`, and `Core` used in generator.rs prior to Sprint 38 are **incorrect** — they were stubbed values that do not match the workshop design (workshop-outcomes.md §WorldTier and ComplexityTier). They must be replaced with the five canonical values above.
- **ComplexityTier ceiling per WorldTier:**
- `Epicenter` → Full
- `Regional` → Full
- `Backwater` → Full (critical: `Backwater` is network-insignificant, NOT budget-capped; isolated communities can be socially complex)
- `Passage` → Moderate
- `Waypoint` → Minimal
- **Source of truth:** workshop-outcomes.md §WorldTier and ComplexityTier table (generator-architecture workshop, lead decision L-3).
- All code referencing `WorldTier::Peripheral`, `WorldTier::Connected`, or `WorldTier::Core` must be updated to the canonical values. This includes generator.rs, any tests, and any serialized data that references these variants.
- **Rationale:** The three-value stub (Peripheral/Connected/Core) was authored before the generator architecture workshop established the five-value canonical model. The mismatch between the code and the design means any generator code built against the stub types would need rewriting anyway. Correcting it now before the Phase 1 implementation work begins eliminates that rework. The `Backwater` full-budget exception is architecturally significant: dense isolated communities (mining towns, research outposts) should be as socially rich as regional hubs — their isolation is their drama, not their limitation.
- **Ticket:** #900 (bug fix), #912 (full enum implementation)
- **Raised by:** Generation cascade workshop (#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)
---
*79 decisions (D-001 through D-218, excluding gaps). Last updated: 2026-05-02 (D-218 — WorldTier canonical values, generation cascade workshop Sprint 38)*