Files
settled-reach/docs/workshops/planet-down-cascade/tyre-round3.md
T
jpmschweitzerandClaude Opus 4.6 b9fd75b840 docs(workshops): planet-down cascade workshop + misc stray files
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>
2026-05-03 20:18:30 +02:00

33 KiB
Raw Blame History

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::Softflagged_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
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 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 1030 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:

  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/*.tomlreserved = true
  4. Generate corp-derived name rows for bodies below corp_city_quotareserved = false

Paula's Stage 03 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 03 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:

  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<f32>, (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() (H1H4) 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.

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 (35 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 13 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 12, 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 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 thresholdsprosperity_current > 0.63 = Intact, 0.430.63 = Worn, 0.230.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.