Planet-down cascade workshop (3 rounds, 5 agents): layer-by-layer generation from empty world through population overlay, city planning, and street rendering. Includes consultant review by Troblum. Also commits: pre-Sprint-35 DB backup, Claude Code team-mode tmux test log (team-test.md). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
33 KiB
title, author, workshop, round, created
| title | author | workshop | round | created |
|---|---|---|---|---|
| Tyre Round 3 — Planet-Down Cascade Workshop | tyre | planet-down-cascade | 3 | 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 logscore < 0.15:MismatchSeverity::Hard— assignment overridden toSynthetic { reason: PoliticalDecision }; city placed at Province centroid;FoundingOrientation = AdminFacing; generation proceeds without panic
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.
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:
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
boundaryBLOBs forbody_id; render each as a polyline in atlas UV space - No dependency on
BodyWorldStategeneration 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.
# 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:
- Preserve existing
reserved = truerows for the body across reruns - Delete existing
reserved = falserows (regenerated fresh each run) - Import authored rows from
wiki/worlds/*.toml→reserved = true - 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 <domain> "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.
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)
);
fn load_heightmap(conn: &Connection, body_id: i64) -> Result<Vec<f32>> {
let data: Vec<u8> = 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.
#[derive(Resource)]
struct GenerationCache {
entries: LruCache<i64, Arc<BodyWorldState>>,
}
struct BodyWorldState {
body_id: i64,
seed: u64,
heightmap: Vec<f32>, // 512×256
river_network: RiverNetwork, // D8 drainage output (regional summary, not full grid)
attractors: Vec<GeoAttractor>,
settlements: Vec<GeneratedSettlement>,
provinces: Vec<ProvinceWorldState>,
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<BodyWorldState> 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.
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.
ALTER TABLE bodies ADD COLUMN body_radius_km REAL;
fn body_radius_km(row: &Row) -> f64 {
row.get::<_, Option<f64>>("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.
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:
- Load heightmap from
BodyWorldState(ARCH-2) - D8 single-direction flow: each cell drains to lowest adjacent neighbor (8 directions)
- Priority-flood fills sinks:
BinaryHeap<(Reverse<f32>, (usize, usize))>processes in elevation order - Accumulate drainage area per cell
- High-accumulation cells →
RiverNetworksegments - 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 | ResourceExtractioneconomic 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:
- Population tier guarantees — minimum required district counts by population band
- Economic role multiplier table — integer weights (sum 100, min 3) × 8 economic roles × 8 district types
- Founding age character modifier — affects
prosperity_baselineand 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.
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.
struct GenerationQueue {
pending: BinaryHeap<Reverse<GenerationRequest>>,
in_flight: HashSet<i64>,
completed: HashSet<String>,
}
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).
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
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<CorpPresence>,
// 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:
// New field — derived at Layer 3 from founding_age_years + spatial_archetype
block_irregularity: BlockIrregularity,
New Enum Types
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<bool> must become Vec<TileEntry>. TileEntry needs at minimum:
struct TileEntry {
tile_type: TileType,
walkable: bool,
spawn_category: Option<PropCategory>, // 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<PropCategory>hook preserved inTileEntry - 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.