--- title: "Tyre Round 3 — Planet-Down Cascade Workshop" author: tyre workshop: planet-down-cascade round: 3 created: 2026-05-01 --- # Tyre Round 3 — Planet-Down Cascade Workshop ## Summary Round 3 convergence document. Contains D-record candidates for all locked technical decisions, resolution of four open questions from Round 2, finalized Rust types and SQL DDL, and the implementation ticket dependency chain. Lead-resolved before Round 3: two-tier mismatch flagging (`score < 0.35` = warning, `score < 0.15` = error/blocks). --- ## 1. Lead-Resolved Items ### Q1 — Mismatch Flag Threshold (LOCKED) Two-tier system: - `score < 0.35`: `MismatchSeverity::Soft` — `flagged_for_review = true`; placement proceeds with warning in generation log - `score < 0.15`: `MismatchSeverity::Hard` — assignment overridden to `Synthetic { reason: PoliticalDecision }`; city placed at Province centroid; `FoundingOrientation = AdminFacing`; generation proceeds without panic ```rust enum MismatchSeverity { None, // best attractor score >= 0.35 Soft, // 0.15 <= score < 0.35; placement proceeds with flag Hard, // score < 0.15; overridden to Synthetic } ``` `Hard` mismatch is not a generation failure — it's a policy decision. The city exists but was placed politically, not geographically. The flag is surfaced in developer tooling and the eventual city history log. --- ## 2. Convergence Items — Round 3 Resolutions ### Q2 — founding_age → layout_mode (Ozzie's Proposal) **Proposal:** founding age should influence block geometry, making old cities irregular and young cities grid-like. **Technical evaluation:** The five spatial archetypes (CompanyTown, AdminCapital, FreePort, Contested, OrganicGrowth) determine *macro geometry* — spine, radial, multi-node. These reflect how the city was *designed* and should not be modified by age. Block geometry is orthogonal: it reflects how the city *evolved after* being designed. A CompanyTown laid out as a rigid grid in Year 1 may have irregular blocks in Year 200 as organic infill accumulated around the original plan. **Resolution: ADOPT via `BlockIrregularity` field — do NOT modify `layout_mode`.** `layout_mode` (archetype) stays fixed. A new `block_irregularity: BlockIrregularity` field is added to `DistrictSkeleton`, derived from `founding_age_years` and `SpatialArchetype`. This is orthogonal to the archetype — CompanyTown can be Grid or Organic depending on age. ```rust enum BlockIrregularity { Grid, // Regular 64m × 64m blocks; planned geometry SlightlyWorn, // ~10% deviation from grid; some organic infill Irregular, // ~25% deviation; multiple planning generations visible Organic, // No regular grid; emerged rather than planned } impl BlockIrregularity { fn from_age_and_archetype(founding_age_years: u32, archetype: SpatialArchetype) -> Self { // Reference age (years to reach next irregularity tier) let step: u32 = match archetype { SpatialArchetype::CompanyTown => 40, // Company towns evolve quickly SpatialArchetype::AdminCapital => 100, // State capitals resist change SpatialArchetype::FreePort => 60, SpatialArchetype::Contested => 35, // Conflict accelerates irregularity SpatialArchetype::OrganicGrowth => 20, // Always evolving; reaches Organic fastest }; match founding_age_years / step.max(1) { 0 => Self::Grid, 1 => Self::SlightlyWorn, 2 => Self::Irregular, _ => Self::Organic, } } } ``` `block_irregularity` is a `DistrictSkeleton` field, not `CityGenerationContext` — it varies per district within a city. Derivation is fully local; no cross-city queries. **Phase implication:** This field is set at Layer 3 and consumed at Layer 4 (street rendering). It has no effect on Phase 2 or 3 deliverables. No rework to the existing convergence. --- ### Q3 — Province Boundary Legibility **Requirement (Ozzie, non-negotiable):** Province boundaries must render as natural watershed lines on the planetary map, not arbitrary grid edges. **Technical resolution:** Province boundaries are ridgelines between drainage basins. The D8 drainage simulation computes flow direction per cell; ridgelines are cells where no adjacent cell drains into the current cell from the "wrong side." Extracting boundary polylines from a D8 result is O(grid_size) — standard watershed delineation. **Two options evaluated:** Option A: Rust runtime computes drainage → extracts boundaries → stores in savegame DB. Option B: Python pipeline computes drainage at build time → extracts boundaries → stores in systems.db. **Resolution: Option B.** The planetary map must render Province boundaries immediately when the player opens it, before any Rust generation has run. Option A introduces a generation-status dependency in the renderer. Option B eliminates it: boundaries are pre-computed at `make regen-db` time and available on first map open. `planet_simulation.py` already computes a DEM per body. Watershed boundary extraction is a post-processing step over the same data. The Python implementation does not need the full D8 precision of the Rust runtime — it's a rendering hint, not game logic. **New schema:** ```sql CREATE TABLE atlas_province_boundaries ( body_id INTEGER NOT NULL REFERENCES bodies(id), province_x INTEGER NOT NULL, province_y INTEGER NOT NULL, boundary BLOB NOT NULL, -- float32 pairs [u0,v0, u1,v1, ...] in atlas UV space (0.0..1.0) -- Boundary polyline tracing the natural watershed outline of this province PRIMARY KEY (body_id, province_x, province_y) ); CREATE INDEX idx_province_bounds_body ON atlas_province_boundaries(body_id); ``` Storage estimate: ~20 float32 pairs per province boundary segment × ~64 segments per body → ~82KB/body → ~32MB for 400 bodies. Acceptable. **Renderer contract:** - On map open: load all `boundary` BLOBs for `body_id`; render each as a polyline in atlas UV space - No dependency on `BodyWorldState` generation status - The Rust L1 drainage computes the same basin topology independently for generation purposes; renderer never waits for it **Pipeline change:** `generate_atlas.py` adds a watershed extraction step after heightmap storage. The DDL addition goes into `import_economics.py`'s `MIGRATION_SQL` block (runs first; creates the table); `generate_atlas.py` populates it. --- ### Q5 — atlas_city_names Population Path **Question:** Who writes source rows to `atlas_city_names` before Stage 0 fulfillment? **Analysis:** The schema has `corp_id` and `tier_hint` — deliberate authoring fields. But with ~400 bodies and 10–30 cities each, hand-authoring all names is not tractable. **Resolution: Two-tier authorship.** **Tier 1 — Authored (`reserved = true`):** World designers write named cities for canon locations in `wiki/worlds/{body_slug}.toml`. These are named places with canonical identities — they appear in lore, faction text, and player-facing narrative. Imported by `import_economics.py`. All `corp_id` and `tier_hint` populated by the author. ```toml # wiki/worlds/nova-kassel.toml [[cities]] name = "Port Cassidy" corp_slug = "meridian-transit" # nullable tier_hint = 3 # nullable; expected WorldTier integer reserved = true ``` **Tier 2 — Generated (`reserved = false`):** `import_economics.py` generates additional name rows from the corporation brand files. Corp HQ worlds receive at least one corp-affiliated city name derived from brand name + city-name templates from the `generate_brands` output. These are placeholders — no specific lore attachment; placed by attractor-matching opportunistically. Source population order in `import_economics.py`: 1. Preserve existing `reserved = true` rows for the body across reruns 2. Delete existing `reserved = false` rows (regenerated fresh each run) 3. Import authored rows from `wiki/worlds/*.toml` → `reserved = true` 4. Generate corp-derived name rows for bodies below `corp_city_quota` → `reserved = false` Paula's Stage 0–3 fulfillment pipeline (runtime) operates on whatever rows exist at generation time, regardless of source. --- ### Q6 — Full Multiplier Table (Locked) Adding `Logistics` as the 8th district type (warehousing, distribution, freight staging). The 7-column table from Round 2 omits this type; it's essential for Transit/Port and Manufacturing cities and has minimum 3 weight across all rows. **Final locked table** (all rows sum to 100; minimum value across all cells: 3): | Economic Role | Res | Com | Ind | Adm | Ent | Civ | Mix | Log | |-------------------|-----|-----|-----|-----|-----|-----|-----|-----| | Mining/Extraction | 33 | 11 | 26 | 8 | 3 | 5 | 9 | 5 | | Manufacturing | 27 | 13 | 23 | 8 | 5 | 7 | 10 | 7 | | Research Hub | 26 | 17 | 9 | 15 | 8 | 12 | 9 | 4 | | Commercial Hub | 20 | 28 | 7 | 10 | 12 | 8 | 10 | 5 | | Administrative | 18 | 14 | 5 | 28 | 8 | 15 | 7 | 5 | | Transit/Port | 20 | 16 | 13 | 7 | 5 | 5 | 16 | 18 | | Energy | 30 | 9 | 27 | 8 | 3 | 6 | 10 | 7 | | Agricultural | 32 | 16 | 9 | 5 | 7 | 8 | 16 | 7 | Row sums: 100, 100, 100, 100, 100, 100, 100, 100. Minimum cell value: 3 (Mining/Ent and Energy/Ent). Floor invariant holds. **Political archetype modifiers (applied after role table; floor at 3):** All five archetypes need modifiers. Round 2 specified three (CompanyTown, AdminCapital, FreePort). The two missing ones are proposed here and require lead confirmation before locking. | Archetype | Adjustment | |-----------------|------------| | CompanyTown | Administrative −10, Industrial +10 | | AdminCapital | Administrative +15, Commercial −8, Entertainment −7 | | FreePort | Commercial +12, Mixed +8, Administrative −20 | | Contested | Mixed +10, Civic −5, Administrative −5 *(proposed — needs lead confirmation)* | | OrganicGrowth | Mixed +15, Commercial +5, Administrative −10, Industrial −10 *(proposed — needs lead confirmation)* | Note: modifier application must re-check the floor — if any cell drops below 3 after modifier, clamp to 3 and redistribute the deficit proportionally across the row. --- ## 3. D-Record Candidates These are proposed D-records. IDs must be claimed via `tooling/db/decision claim D "title"` before writing to `decisions/` domain files. All items require corresponding implementation tickets. --- ### D-candidate: Heightmap Storage Schema (ARCH-1) **Domain:** architecture **Decision:** Planetary heightmaps are stored as float32 LE BLOBs in `atlas_body_heightmaps` in systems.db; written by `generate_atlas.py`; loaded into `BodyWorldState` via `bytemuck::cast_slice`. ```sql CREATE TABLE atlas_body_heightmaps ( body_id INTEGER NOT NULL REFERENCES bodies(id), data BLOB NOT NULL, -- float32 LE, 512×256 = 524,288 bytes PRIMARY KEY (body_id) ); ``` ```rust fn load_heightmap(conn: &Connection, body_id: i64) -> Result> { let data: Vec = conn.query_row( "SELECT data FROM atlas_body_heightmaps WHERE body_id = ?1", [body_id], |row| row.get(0), )?; Ok(bytemuck::cast_slice(&data).to_vec()) } ``` Storage: ~512KB/body × 400 bodies = ~200MB. Coordinate system: row 0 = north pole, 512 columns (longitude), 256 rows (latitude). `bytemuck::cast_slice` is zero-copy on native endian architectures. --- ### D-candidate: BodyWorldState as Bevy Resource (ARCH-2) **Domain:** architecture **Decision:** Session-level generation state lives in a Bevy `Resource` (`GenerationCache`). Never serialized. Fully reproducible from `seed` + systems.db. LRU cap: 50 bodies. ```rust #[derive(Resource)] struct GenerationCache { entries: LruCache>, } struct BodyWorldState { body_id: i64, seed: u64, heightmap: Vec, // 512×256 river_network: RiverNetwork, // D8 drainage output (regional summary, not full grid) attractors: Vec, settlements: Vec, provinces: Vec, generated_at: std::time::Instant, } ``` Memory budget: ~5MB for 50 bodies (full 512×256 accumulation grid discarded post-extraction; only 64×32 regional summary retained). `Arc` for cheap cross-system sharing without cache lock contention. LRU eviction is safe — evicted bodies re-generate deterministically on next access. --- ### D-candidate: City Name Reservation Schema and Population Path (ARCH-3) **Domain:** architecture **Decision:** `atlas_city_names` stores name reservations, not positions. Rows come from two sources: authored TOML files (`reserved = true`) and generated corp-derived names (`reserved = false`). Both flow through `import_economics.py`. Runtime Stage 0–3 fulfillment assigns names to generated settlement positions. ```sql CREATE TABLE atlas_city_names ( id INTEGER PRIMARY KEY, body_id INTEGER NOT NULL REFERENCES bodies(id), name TEXT NOT NULL, corp_id INTEGER REFERENCES corporations(id), tier_hint INTEGER, reserved BOOLEAN NOT NULL DEFAULT 0 ); CREATE INDEX idx_city_names_body ON atlas_city_names(body_id); ``` Source TOML: `wiki/worlds/{body_slug}.toml` → `[[cities]]` arrays. Authoring format documented in Q5 resolution above. --- ### D-candidate: Body Radius Column (ARCH-4) **Domain:** architecture **Decision:** `bodies.body_radius_km` nullable REAL column; Rust reads with `planet_class` fallback. ```sql ALTER TABLE bodies ADD COLUMN body_radius_km REAL; ``` ```rust fn body_radius_km(row: &Row) -> f64 { row.get::<_, Option>("body_radius_km") .unwrap_or(None) .unwrap_or_else(|| default_radius_for_class( row.get("planet_class").unwrap_or("") )) } ``` --- ### D-candidate: Province Boundary Pre-Computation (ARCH-5) **Domain:** architecture **Decision:** Province boundary polylines are computed at build time by Python (watershed extraction from DEM) and stored in `atlas_province_boundaries` in systems.db. The planetary map renderer loads these directly; no dependency on Rust generation status. ```sql CREATE TABLE atlas_province_boundaries ( body_id INTEGER NOT NULL REFERENCES bodies(id), province_x INTEGER NOT NULL, province_y INTEGER NOT NULL, boundary BLOB NOT NULL, -- float32 pairs [u0,v0, u1,v1, ...] in atlas UV space (0.0..1.0) PRIMARY KEY (body_id, province_x, province_y) ); CREATE INDEX idx_province_bounds_body ON atlas_province_boundaries(body_id); ``` Schema added to `import_economics.py` MIGRATION_SQL (creates table). `generate_atlas.py` populates it after heightmap storage. Renderer loads by `body_id` and renders as polylines. --- ### D-candidate: D8 Priority-Flood Drainage — Layer 1 (GEN-1) **Domain:** architecture (generation) **Decision:** Layer 1 uses D8 priority-flood drainage in Rust to derive river networks and geographic attractors from heightmaps. Target: ~50ms at 512×256. Algorithm: 1. Load heightmap from `BodyWorldState` (ARCH-2) 2. D8 single-direction flow: each cell drains to lowest adjacent neighbor (8 directions) 3. Priority-flood fills sinks: `BinaryHeap<(Reverse, (usize, usize))>` processes in elevation order 4. Accumulate drainage area per cell 5. High-accumulation cells → `RiverNetwork` segments 6. Extract geographic attractors: `CoastalHarbor` (coastline × high drainage), `MountainPass` (low-elevation saddles), `ResourceConcentration` (tagged from systems.db), `ArablePlain` (low slope × high moisture) Seed: `drainage_seed = child_seed(body_seed, "drainage")` — tie-breaking in priority-flood. --- ### D-candidate: Five-Phase Attractor Assignment — Layer 2 (GEN-2) **Domain:** architecture (generation) **Decision:** City-to-attractor assignment: score matrix + hard zeros → tier sort → Tier A greedy → Tier B/C Hungarian → synthetic overflow. Key parameters: - Hard-zero filter: `is_physically_possible()` (H1–H4) runs before scoring; zeros are structural, not low scores - Minimum non-zero score: 0.10 (no compatible attractor scores below this floor) - Tier A: `MiningExtraction | ResourceExtraction` economic roles (highest geographic constraint) - Hungarian: O(N³) maximum-weight bipartite matching; N ≤ 30 cities; defensive fallback at N > 40 (greedy for Tier C cities with score > 0.5) - Synthetic overflow: `SyntheticPlacementReason`: `PopulationOverflow | PoliticalDecision | CorpExpansion` - Mismatch thresholds (lead-resolved): Soft at 0.35; Hard (→ Synthetic override) at 0.15 --- ### D-candidate: Three-Component District Mix — Layer 3 (GEN-3) **Domain:** architecture (generation) **Decision:** District type distribution uses three orthogonal components. Self-contained (no cross-city queries). Full locked multiplier table in Q6 resolution above. Components: 1. Population tier guarantees — minimum required district counts by population band 2. Economic role multiplier table — integer weights (sum 100, min 3) × 8 economic roles × 8 district types 3. Founding age character modifier — affects `prosperity_baseline` and character tags at Backwater+ WorldTier; does NOT change weights Political archetype modifiers applied post-table with floor at 3. --- ### D-candidate: BlockIrregularity from founding_age — Layer 3 (GEN-4) **Domain:** architecture (generation) **Decision:** Block geometry irregularity is a `DistrictSkeleton` field derived from `founding_age_years` × `SpatialArchetype`. Orthogonal to spatial arrangement archetype. Consumed at Layer 4. Full Rust type and derivation function: see Q2 resolution above. Phase 4 usage: Layer 4 tile placement uses `block_irregularity` to vary street width, corner treatments, and block subdivision patterns. `Grid` produces regular tile-aligned 64m blocks. `Organic` produces irregular boundaries with no aligned corners. --- ### D-candidate: TerritorialStatus Priority-Ordered Derivation (GEN-5) **Domain:** architecture (generation) **Decision:** `TerritorialStatus` derives from `ProvinceWorldState` via priority-ordered algorithm. `placed_at_generation: bool` is the sole differentiator between `AbandonedZone` and `WildernessBuffer`. Algorithm and enum values: locked in Round 2; see round-2-notes.md §3. --- ### D-candidate: SettlementClass Enum (GEN-6) **Domain:** architecture (generation) **Decision:** `SettlementClass` generalizes the latent/active distinction. `active: bool` is derived from class conditions at runtime. `placed_at_generation: bool` is set at Layer 2 and immutable. ```rust enum SettlementClass { NameLocked, // Has a name in atlas_city_names; always active PopulationBudget, // Active if body_population_density > threshold EconomicTriggered, // Active if route_traffic_score > threshold OrganicGrowth, // Placed by geographic probability; geographically_triggered = false } ``` --- ### D-candidate: Background Generation Queue (GEN-7) **Domain:** architecture **Decision:** Background body generation uses a rayon thread pool (not async Tokio). Priority ordering based on player position, travel routes, and body name mentions in dialogue text. ```rust struct GenerationQueue { pending: BinaryHeap>, in_flight: HashSet, completed: HashSet, } ``` Priority ordering: player-targeted body → adjacent bodies in travel route → bodies mentioned in dialogue → active corp supply chains → all others. Aho-Corasick `SystemNameIndex`: pattern-matches body/system names in player-facing text (news ticker, NPC dialogue, documents) to trigger pre-generation before the player travels there. --- ### D-candidate: WorldTier Enum Bug Fix (GEN-8) **Domain:** architecture (bug) **Decision:** `WorldTier` in `server/src/simulation/generator.rs` must be `{ Epicenter, Regional, Backwater, Passage, Waypoint }`. Current code `{ Peripheral, Connected, Core }` is wrong. This is a code bug, not a design question. Affected file: `server/src/simulation/generator.rs`, `enum WorldTier` and all match arms. Prerequisite for: every generation implementation ticket. --- ### D-candidate: atlas_feature_names Schema (GEN-9) **Domain:** architecture **Decision:** Geographic feature names (rivers, mountain passes, bays) are stored in `atlas_feature_names`, distinct from `atlas_city_names`. Assigned at Layer 1 name fulfillment (Stage 1 of Paula's four-stage pipeline). ```sql CREATE TABLE atlas_feature_names ( id INTEGER PRIMARY KEY, body_id INTEGER NOT NULL REFERENCES bodies(id), name TEXT NOT NULL, tag_hint TEXT -- nullable; expected FeatureTag for this name ); CREATE INDEX idx_feature_names_body ON atlas_feature_names(body_id); ``` --- ## 4. Final Type Specifications ### CityGenerationContext — Final Fields ```rust struct CityGenerationContext { // Core identity name: String, body_id: i64, seed: u64, // Location location: (u8, u8), // atlas grid cell region_size: (u8, u8), // cells the city region occupies // Population and economics population: u64, economic_role: EconomicRole, corp_presence: Vec, // Tier (enum values require GEN-8 bug fix) world_tier: WorldTier, // Epicenter|Regional|Backwater|Passage|Waypoint // Age and character founding_age_years: u32, spatial_archetype: SpatialArchetype, // Layer 2 outputs — attractor assignment attractor_assignment: AttractorAssignment, mismatch_severity: MismatchSeverity, // Layer 2 outputs — settlement class settlement_class: SettlementClass, active: bool, // derived from SettlementClass conditions // Layer 2 outputs — founding orientation geographically_triggered: bool, // false → FoundingOrientation::AdminFacing // Layer 3 inputs/outputs — prosperity prosperity_baseline: f32, // set at generation; founding_age modulated prosperity_current: f32, // economics variable; tile conditions derived from this // Layer 3 context territorial_context: TerritorialStatus, } // Note: prosperity_delta = prosperity_current - prosperity_baseline; derived, never stored // Note: block_irregularity lives on DistrictSkeleton (per-district), not here (per-city) ``` ### DistrictSkeleton — New Field Add to existing `DistrictSkeleton` in `generator.rs`: ```rust // New field — derived at Layer 3 from founding_age_years + spatial_archetype block_irregularity: BlockIrregularity, ``` ### New Enum Types ```rust enum SpatialArchetype { CompanyTown, // Spine pattern AdminCapital, // Radial pattern FreePort, // Multi-node pattern (3–5 nodes) Contested, // Dual-center overlay pattern OrganicGrowth, // Irregular local density pattern } enum EconomicRole { MiningExtraction, Manufacturing, ResearchHub, CommercialHub, Administrative, TransitPort, Energy, Agricultural, } enum DistrictType { Residential, Commercial, Industrial, Administrative, Entertainment, Civic, MixedUse, Logistics, // NEW: warehousing, distribution, freight staging } enum MismatchSeverity { None, // score >= 0.35 Soft, // 0.15 <= score < 0.35 Hard, // score < 0.15; overridden to Synthetic } ``` ### `GenerateChunkData` Upgrade (Prior Workshop Item, Still Required) `GeneratorChunkData = Vec` must become `Vec`. `TileEntry` needs at minimum: ```rust struct TileEntry { tile_type: TileType, walkable: bool, spawn_category: Option, // scatter hook; None until Phase 6 // tile condition derived at render time from prosperity_current thresholds } ``` --- ## 5. SeedChain Usage Per Layer All seeds: FNV-1a `child_seed(parent, discriminant)` per D-010. ``` system_seed = child_seed(world_seed, system_id) body_seed = child_seed(system_seed, body_id) // Layer 1 drainage_seed = child_seed(body_seed, "drainage") attractor_seed = child_seed(body_seed, "attractors") boundary_seed = child_seed(body_seed, "province_bounds") // Layer 2 — iterate cities in atlas_city_names id order placement_seed = child_seed(body_seed, "placement") for city_index in sorted order: city_seed = child_seed(placement_seed, city_index as u64) // Used for: synthetic attractor offsets; tie-breaking in assignment // Layer 3 — per city district_seed = child_seed(city_seed, "districts") founding_seed = child_seed(city_seed, "founding") prosperity_seed = child_seed(city_seed, "prosperity") // Layer 4 — per district tile_seed = child_seed(district_seed, tile_index as u64) // Tile LAYOUT is seed-locked (deterministic from tile_seed) // Tile CONDITIONS are economics-variable via threshold-crossing cache invalidation ``` --- ## 6. Performance Budget Per Layer Target: < 700ms at session start (synchronous cold generation); < 143ms on-demand. | Layer | Operation | Budget | Estimated | |-------|-----------|--------|-----------| | L1 | D8 drainage routing | 50ms | ~50ms | | L1 | Attractor extraction | 10ms | ~8ms | | L1 | Province boundary extraction | 5ms | ~5ms | | L2 | Hard-zero filter + score matrix | 5ms | ~3ms | | L2 | Tier A greedy assignment | 2ms | ~1ms | | L2 | Hungarian (Tier B/C, N ≤ 30) | 10ms | ~15ms | | L2 | Synthetic overflow | 2ms | ~1ms | | L2 | Name fulfillment Stages 1–3 | 5ms | ~5ms | | L3 | District mix (all cities) | 15ms | ~20ms | | L3 | TerritorialStatus (all provinces) | 25ms | ~25ms | | L3 | WorldTier assignment + prosperity | 5ms | ~10ms | | **Total** | | **134ms** | **~143ms** | | Session budget (cold start) | 700ms | | | | Headroom | | | 4.9× | **Defensive check:** Hungarian is O(N³). At N = 30: ~15ms. At N = 50: ~400ms (exceeds L2 budget alone). If any body has > 40 named cities: switch Tier C to greedy for cities where best-available-attractor score > 0.5, then run Hungarian only on the remainder. This bound has not been hit on any current body but needs the guard. --- ## 7. Implementation Ticket Dependency Chain Effort in dev-days. All `SCHEMA-*` tickets can be grouped into a single migration PR. ### Tier 0 — Prerequisites (No Dependencies) | Ticket | Work | Effort | File(s) | |--------|------|--------|---------| | BUG-WorldTier | Fix `WorldTier` enum; update all match arms | 0.5d | `server/src/simulation/generator.rs` | | SCHEMA-bodies | `body_radius_km REAL` column | 0.25d | `systems-schema.sql`, `import_economics.py` MIGRATION_SQL | | SCHEMA-heightmaps | `atlas_body_heightmaps` DDL | 0.25d | `systems-schema.sql`, `import_economics.py` MIGRATION_SQL | | SCHEMA-city-names | `atlas_city_names` DDL + index | 0.25d | `systems-schema.sql`, `import_economics.py` MIGRATION_SQL | | SCHEMA-feature-names | `atlas_feature_names` DDL + index | 0.25d | `systems-schema.sql`, `import_economics.py` MIGRATION_SQL | | SCHEMA-province-bounds | `atlas_province_boundaries` DDL + index | 0.25d | `systems-schema.sql`, `import_economics.py` MIGRATION_SQL | ### Tier 1 — Python Pipeline (Depends on Tier 0 schemas) | Ticket | Work | Effort | Depends on | |--------|------|--------|------------| | PY-heightmap-import | `generate_atlas.py`: BLOB-pack elevation float32; INSERT into `atlas_body_heightmaps` | 1d | SCHEMA-heightmaps | | PY-province-bounds | `generate_atlas.py`: watershed extraction from DEM; store boundary polylines | 2d | SCHEMA-province-bounds, PY-heightmap-import | | PY-city-names-authored | `import_economics.py`: read `wiki/worlds/*.toml` `[[cities]]`; INSERT with `reserved = true` | 1d | SCHEMA-city-names | | PY-city-names-corp | `import_economics.py`: generate corp-derived name rows for under-quota bodies | 1d | PY-city-names-authored | | PY-body-radius | `import_economics.py`: populate `body_radius_km` from planet_class defaults or authored values | 0.5d | SCHEMA-bodies | ### Tier 2 — Rust Type Definitions (Depends on Tier 0) | Ticket | Work | Effort | Depends on | |--------|------|--------|------------| | RS-types-worldtier | Fix `WorldTier` enum in Rust; update all match arms | 0.5d | BUG-WorldTier | | RS-types-settlement | `SettlementClass`, `MismatchSeverity`, `AttractorAssignment`, `SyntheticPlacementReason` | 0.5d | RS-types-worldtier | | RS-types-district | Add `BlockIrregularity` to `DistrictSkeleton`; `SpatialArchetype`, `EconomicRole`, `DistrictType::Logistics` | 0.5d | — | | RS-types-territorial | `TerritorialStatus` with correct variants | 0.5d | — | | RS-types-city-ctx | Finalize `CityGenerationContext` with all new fields | 1d | RS-types-settlement, RS-types-territorial | ### Tier 3 — Rust Core (Depends on Tier 1 + Tier 2) | Ticket | Work | Effort | Depends on | |--------|------|--------|------------| | RS-heightmap-load | `load_heightmap()` via bytemuck; integration into `BodyWorldState` init | 0.5d | PY-heightmap-import, RS-types-city-ctx | | RS-body-state | `BodyWorldState` struct + `GenerationCache` Bevy Resource + LRU cache | 1.5d | RS-heightmap-load | | RS-drainage | D8 priority-flood drainage; attractor extraction; RiverNetwork construction | 3d | RS-body-state | | RS-attractor-types | `GeographicAttractor`, `AttractorType`, `CompatibilityMatrix` | 0.5d | RS-types-settlement | ### Tier 4 — Generation Algorithms (Depends on Tier 3) | Ticket | Work | Effort | Depends on | |--------|------|--------|------------| | RS-attractor-assign | Five-phase attractor assignment + Hungarian + synthetic overflow + mismatch flags | 4d | RS-drainage, RS-attractor-types, PY-city-names-authored | | RS-district-mix | Three-component district mix + locked multiplier table + political archetype modifiers | 3d | RS-attractor-assign, RS-types-district | | RS-territorial | TerritorialStatus priority-ordered derivation; ProvinceWorldState population | 2d | RS-attractor-assign | | RS-block-irregularity | `BlockIrregularity::from_age_and_archetype()` + integration into district loop | 1d | RS-district-mix | | RS-tile-conditions | Threshold cache with invalidation for tile conditions (L4-Q1) | 1.5d | RS-district-mix | ### Tier 5 — Background + UI (Depends on Tier 4) | Ticket | Work | Effort | Depends on | |--------|------|--------|------------| | RS-bg-queue | `GenerationQueue` + rayon thread pool + priority ordering | 2d | RS-body-state | | RS-aho-corasick | `SystemNameIndex` + text scanning → generation trigger | 1d | RS-bg-queue | | UI-province-bounds | Godot planetary map: load `atlas_province_boundaries`; render as natural polylines | 2d | PY-province-bounds | ### Critical Path ``` SCHEMA-heightmaps → PY-heightmap-import → RS-heightmap-load → RS-body-state → RS-drainage → RS-attractor-assign → RS-district-mix → RS-tile-conditions ``` Critical path effort: 0.25 + 1 + 0.5 + 1.5 + 3 + 4 + 3 + 1.5 = **14.75 dev-days** Total effort (all tiers, parallel where possible): **~33 dev-days** Parallel acceleration: Tier 0 + Tier 1 + Tier 2 can all run simultaneously. On a two-agent split (Python team / Rust team), calendar time narrows to ~20 days. --- ## 8. Open Items Not Resolved in This Round ### Q4 — Port/Station as Special City Type Not addressed. Orbital stations lack terrain; no geographic attractors; no drainage. Technical sketch: `SpatialArchetype::SpaceStation` as a sixth variant, bypassing Layer 1 entirely. `body_id` points to orbital body. District mix: no Agricultural; Logistics and Industrial dominant. Defer to lead for whether this needs a Round 4 or a separate ticket. ### Contested + OrganicGrowth Archetype Modifiers My proposed modifiers (see Q6 above) are proposals, not locked. Lead or Burnelli-Sheldon should confirm or adjust before `RS-district-mix` is implemented. ### Province Watershed Algorithm Detail I've specified that `generate_atlas.py` adds watershed extraction. The exact algorithm (D8 ridgeline detection, smoothing kernel, UV coordinate normalization) needs a concrete spec before `PY-province-bounds` is assigned. Recommend a brief technical sidequest with Tyre or a consultant review before that ticket starts. --- ## 9. Items Confirmed — No Further Discussion Required Carried forward from Rounds 1–2, not reopened: - **SeedChain (FNV-1a)** — D-010; unchanged - **D8 drainage routing** — locked; ARCH-1 accepted by all agents - **Scatter deferred** — lead decision; `spawn_category: Option` hook preserved in `TileEntry` - **District = 256m, Block = 64m** (4×4 blocks per district) — confirmed - **Province = 1 regional grid cell (~540km×270km on reference body)** — confirmed - **Tile condition thresholds** — `prosperity_current` > 0.63 = Intact, 0.43–0.63 = Worn, 0.23–0.43 = Cracked, < 0.23 = Broken; Paula's offsets prevent boundary oscillation - **prosperity_delta = derived, never stored** — confirmed - **Self-contained district generation** — no cross-city queries; lead requirement - **L4-Q1: threshold-crossing cache invalidation** for tile conditions — ticket RS-tile-conditions - **L4-Q4: pre-fetch two ring cells ahead** of player movement — confirmed; handled in chunk_streaming.rs after RS-body-state lands - **Area = atlas layer, not navigation tier** — confirmed - **Background generation budget** — 700ms session start; ~143ms on-demand; within budget at 4.9× headroom --- *Round 3 complete from Tyre's side. Nine D-record candidates produced. Implementation dependency chain: 33 dev-days total, 14.75 critical path. WorldTier enum bug fix (BUG-WorldTier) is the hard prerequisite blocker — no generation ticket can land without it.*