Files
settled-reach/docs/workshops/planet-down-cascade/gestalt-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

39 KiB
Raw Blame History

title, description, type, status, workshop, agent, round, created
title description type status workshop agent round created
Gestalt — Round 3: Convergence, D-Records, Ticket Chain Final convergence positions, D-record candidates D-C18 through D-C34, implementation ticket dependency chain, and remaining open questions for lead. workshop active planet-down-cascade gestalt 3 2026-05-01

Planet-Down Cascade — Round 3 (Gestalt)

Role: Convergence, D-record candidates, ticket dependency chain.

Lead decision incorporated: Two-tier mismatch flagging — score < 0.35 = warning (logged, no block), score < 0.15 = error (blocks generation, requires manual review).


Part 1: Convergence — Remaining Open Items

Q1: Mismatch Flag Threshold — RESOLVED (Lead Decision)

Two-tier system:

  • Warning (score < 0.35): Logged to generation log. Generation proceeds. Flags that this placement is geographically suboptimal but physically valid.
  • Error (score < 0.15): Blocks generation. Requires manual review. Indicates a corp HQ is being placed in a location that is physically incompatible by Paula's H1-H4 hard constraints.

The two-tier system correctly handles the disagreement between Tyre (0.15 only catches outright incompatibilities) and Paula (0.35 catches borderline cases). Both are right — they're describing different severity levels. The lead's resolution names them explicitly.

Implementation note: Warning-level placements still generate. The generation log accumulates warning records that can be reviewed in bulk to catch systematic issues (e.g., a body where 80% of placements are at warning level suggests the body's economic profile doesn't match its heightmap — a systems.db authoring problem, not a generation bug).

Q2: founding_age → layout_mode — EVALUATED

Ozzie's proposal: Old settlements get irregular layout modes; young settlements get grid layout.

My evaluation:

The proposal is mechanically correct in intent but wrong in formulation. A deterministic old = irregular, young = grid rule produces visual noise at edge cases (an ancient CompanyTown facility block shouldn't be irregular; a brand-new frontier town shouldn't be perfect grid) and removes interesting variance.

My recommendation: founding_age as a probability weight on layout_mode selection, not a deterministic assignment.

The three layout modes are Grid, Organic, and Mixed. The current model selects layout_mode from district_type + political_archetype. Founding_age should add a prior that shifts the probability distribution:

fn layout_mode_weights(
    district_type: DistrictType,
    political_archetype: PoliticalArchetype,
    founding_age_years: u32,
) -> [f32; 3] {  // [Grid, Organic, Mixed]
    
    // Base weights from archetype (unchanged)
    let base = archetype_layout_weights(district_type, political_archetype);
    
    // Age prior: older → shifts probability toward Organic
    let age_organic_bonus = match founding_age_years {
        0..=49   => 0.0,
        50..=199  => 0.1,
        200..=499 => 0.25,
        _         => 0.40,   // Ancient: strong irregular prior
    };
    
    // But CompanyTown districts near facility resist age influence
    // (the facility's grid layout locks the surrounding blocks)
    let age_resistance = archetype_age_resistance(district_type, political_archetype);
    let effective_bonus = age_organic_bonus * (1.0 - age_resistance);
    
    [
        (base[0] - effective_bonus * 0.5).max(0.05),  // Grid: loses weight with age
        (base[1] + effective_bonus).min(0.90),          // Organic: gains weight with age
        base[2],                                         // Mixed: unchanged
    ]
}

fn archetype_age_resistance(dt: DistrictType, pa: PoliticalArchetype) -> f32 {
    match (dt, pa) {
        (DistrictType::Industrial, PoliticalArchetype::CompanyTown) => 0.9,  // grid lock
        (DistrictType::Administrative, PoliticalArchetype::AdminCapital) => 0.6,
        (_, PoliticalArchetype::FreePort) => 0.0,  // freePorts absorb age fully
        _ => 0.3,  // default mild resistance
    }
}

What this produces for gameplay:

  • Ancient mining town: Industrial districts near the mine are still grid (facility planned it); residential and mixed districts have grown organically over centuries
  • New research hub: everything is clean grid — it was planned and built recently
  • Old FreePort: every district has grown organically — no planning authority was strong enough to maintain grid
  • Ancient AdminCapital: central administrative zone stays grid (government maintained it); outer residential rings are deeply organic

This is interesting spatial variety, not visual noise, because the variance has an in-world rationale the player can read. Adopting Ozzie's proposal with this probability-weight formulation.

Q3: Province Boundary Legibility — ADDRESSED

Ozzie's requirement: Province boundaries must be visible on the planetary map as natural features, not arbitrary grid lines.

This requirement is already satisfied by the architecture. Here's why:

Province boundaries are drainage basin divides — the ridgelines and watershed boundaries that separate one river basin from another. The Layer 1 D8 drainage algorithm produces the drainage network; Province boundaries are the Voronoi-like regions around drainage confluences, bounded by the highest-elevation cells between them.

On the planetary map, the player sees:

  1. Terrain heightmap — renders the ridgelines and basin shapes naturally as topographic relief
  2. River network overlay — rivers flow through Province interiors toward confluence points
  3. Settlement density variation — CoreTerritory Provinces have visible settlement clusters; WildernessBuffer Provinces are empty

The Province boundary is visible as the ridge between two river valleys. The player doesn't need a political border drawn — they see a mountain range or watershed divide that happens to also be the Province boundary. This is the geography-is-political-history principle made visible.

TerritorialStatus overlay is a separate implant UI layer (togglable by the player). When active, it color-codes Provinces by TerritorialStatus. When inactive, the player reads the same information from terrain + settlement density + road quality. Both modes should be consistent — a player who understands the geography should be able to predict TerritorialStatus without activating the overlay.

Implementation note for the client team (post-this-workshop): The planetary map renderer needs to know Province boundaries, which come from the BodyWorldState drainage data. This means the planetary map can't render full Province detail until the Layer 1-2 generation for that body completes. Ozzie's diegetic placeholder tiers (from Round 2 notes) handle the interim state.

Q4: atlas_city_names Population Path — ADDRESSED

How do city names get into atlas_city_names?

Answer: The same authoring process that currently populates atlas_cities. The wiki content (planet and city articles) is the source. The generate_atlas.py pipeline reads wiki data and populates bodies, systems, and city data into systems.db. When the Phase 3 generator (generate_regional.py) was designed, it was going to add city attributes. Under Amendment 3, city positions are now runtime-generated, so atlas_cities needs restructuring into atlas_city_names.

Proposed workflow:

  • Wiki author documents a city with name, population, and any corp HQ references
  • generate_atlas.py reads wiki data and populates atlas_city_names rows
  • No new authoring tool needed — the wiki content is already the source

This is a scope clarification, not a design question. The atlas_city_names schema (from ARCH-3) accepts the data; the existing wiki import pipeline provides it.

Q5: Full Multiplier Table — CONFIRMED

Burnelli-Sheldon's table from Round 2 notes covers the primary economic roles. Confirming the full table including missing roles, consolidated to match the bodies.economic_role enum values:

The table is confirmed at the values in the Round 2 notes. The lead will confirm the full table in the D-record at Round 3. The generation code should treat this table as a runtime-configurable data asset, not hardcoded constants — enabling tuning without recompilation.


Part 2: D-Record Candidates

This workshop adds D-C18 through D-C34 to the candidate register. D-C1 through D-C17 are from the generation-cascade workshop and carried forward as Given Facts.


D-C18: Three-Tier Execution Model

Decision: Layer 1-2 generation runs as Rust runtime-background threads per body (not Python build-time). Layer 3-4 runs as Rust runtime-on-demand triggered by player proximity. Python generators (generate_atlas.py, generate_regional.py) remain build-time for system-level economics data only.

Rationale: The economic simulation runs on system-level aggregate specs already in systems.db. It does not need Layer 1-2 spatial detail. Layer 1-2 is consumed only by Layer 3-4, both proximity-triggered. Therefore Layer 1-2 does not need precomputation for all ~400-500 bodies at build time. Runtime background generation completes in ~136ms per body — well within the single-digit-second performance budget.

Inputs: systems.db (heightmaps, city names, economics), world_seed
Outputs: BodyWorldState in LRU GenerationCache (in-memory, reproducible from seed)
Execution tier: Architecture decision spanning all tiers
Maps to ticket: Background Thread Infrastructure (T-10)


D-C19: Background Generation Priority Queue

Decision: Layer 1-2 generation requests are queued by priority: Immediate (player's current body) > High (bodies referenced in player-facing text, detected by Aho-Corasick scan) > Medium (gate-adjacent bodies) > Low (all others, breadth-first along gate graph).

Rationale: The moment a body name appears in news ticker, dialogue, or corporate records, the player may travel there. Pre-generating before the player decides avoids a visible loading state. Aho-Corasick is O(N) text scanning across all body names.

Inputs: Player position, recently-rendered text buffers, gate graph adjacency
Outputs: Priority-ordered generation queue feeding the rayon thread pool
Execution tier: Runtime-background infrastructure
Maps to ticket: Background Thread Infrastructure (T-10)


D-C20: Fully Generative Placement (Amendment 3)

Decision: markers.json retains topographic features (mountain ranges, seas, coastlines) only. All city positions, river polylines, and road paths are generator outputs. atlas_city_names stores name reservations (identity); the generator produces positions.

Rationale: Authored positions become stale as geography generation parameters change. Fully generative positions stay consistent with generated terrain. The constraint set is minimal: corporate cross-references in systems.db must resolve; everything else is generator output.

Inputs: atlas_city_names (name reservations + HQ constraints), Layer 1 geographic features
Outputs: GeneratedSettlement positions in BodyWorldState
Execution tier: Runtime-background (Layer 2)
Maps to ticket: atlas_city_names Schema (T-03), Layer 2 Population Overlay (T-06)


D-C21: Spatial Hierarchy — Eight Tiers with Locked Dimensions

Decision: The spatial vocabulary for all generation work uses eight named tiers:

Tier Name Dimensions Notes
0 Chunk 64×64 sim tiles (~32m×32m) Immutable from prior architecture
1 Block 128×128 sim tiles (~64m×64m) 2×2 chunks
2 District 512×512 sim tiles (~256m×256m) 4×4 blocks
3 Region City district grid bounding box Semantic tier; not a storage tier
4 Province 1 regional grid cell (~540km×270km) Drainage basin; watershed-bounded
5 Area 1 contiguous terrain feature zone Atlas-level; body size absorbed here
6 Body Full surface Varies by body_radius_km
7 System Star system Gate graph

Area count = round(sqrt(body_surface_area_km2 / 6_000_000)). Reference body (radius ~5,000km): 7 areas. Small moon (radius ~500km): 1 area.

Rationale: Consistent vocabulary prevents scope confusion across implementation teams. Tiers 0-4 have fixed dimensions; body size variation is absorbed at Area tier.

Inputs: body_radius_km from bodies table
Outputs: Tier naming conventions, area_count field per body
Execution tier: Build-time (area_count computed by generate_atlas.py)
Maps to ticket: body_radius_km Column (T-04)


D-C22: D8 Drainage Routing for River Networks

Decision: Layer 1 river network generation uses D8 drainage routing (8-directional steepest descent) on the 512×256 heightmap. River threshold: cells with flow_accumulation > 200 are river cells. Confluence nodes = cells with ≥2 incoming river tributaries above threshold. River mouths = river cells adjacent to sea-level boundary.

Rationale: D8 is the standard efficient drainage algorithm for grid-based heightmaps. It is fully deterministic from the heightmap + seed. The 200-cell threshold produces rivers visible at regional scale without excessive fragmentation. Algorithm is portable from planet_simulation.py Python reference.

Inputs: atlas_body_heightmaps BLOB (float32 LE, 512×256), sea_level
Outputs: RiverNetwork (river_cells, confluence_nodes, river_mouths, flow_dir, accumulation) in BodyWorldState
Execution tier: Runtime-background (Layer 1)
Maps to ticket: Layer 1 Empty World Generator (T-05), Heightmap BLOB Storage (T-01)


D-C23: Geographic Feature Tag Extraction

Decision: Seven geographic feature tags are extracted per regional cell (64×32 grid) from Layer 1 drainage and heightmap data. Tags are the inputs to Layer 2 attractor-matching.

Tag Derivation rule
RiverConfluence Any confluence node within cell's heightmap bbox
CoastalHarbor is_coastal && terrain_roughness < 0.3
MountainPass terrain_roughness > 0.65 && lower cells on two opposing sides
ArablePlain terrain_roughness < 0.25 && terrestrial && arable biome class
ResourceConcentration economic_role-weighted biome prior (L1-Q4 included)
Defensible terrain_roughness > 0.5 && approach_vectors ≤ 2
NaturalBarrier ocean

Rationale: These seven tags capture the geographic configurations that historically produce settlements. They are the algorithmic reification of "geography is political history." Including L1-Q4 (economic role as biome prior) is low-cost and prevents self-contradictory worlds.

Inputs: RiverNetwork (from D-C22), atlas_regional_biomes, bodies.economic_role
Outputs: Vec<(RegionalCellId, Vec)> in BodyWorldState
Execution tier: Runtime-background (Layer 1)
Maps to ticket: Layer 1 Empty World Generator (T-05)


D-C24: Sub-Biome Variant Classification

Decision: Each regional cell receives a sub-biome variant tag (3-4 variants per biome class), seed-derived using FNV-1a child_seed, with economic role as a probability prior (L1-Q4 included). terrain_modification_cost: f32 computed analytically per cell (roughness × 0.6 + biome_clearing_cost, clamped [0,1]).

Rationale: Sub-biome variants give regional cells distinct character without authoring. Terrain modification cost is the Layer 2 input that governs hinterland shape — cheap terrain becomes farmland, expensive terrain stays wilderness.

Inputs: atlas_regional_biomes (biome_class, terrain_roughness), bodies.economic_role, world_seed
Outputs: sub_biome_variant, terrain_modification_cost per cell in BodyWorldState
Execution tier: Runtime-background (Layer 1)
Maps to ticket: Layer 1 Empty World Generator (T-05)


D-C25: Attractor-Matching Algorithm — Five-Phase Pipeline

Decision: Settlement placement uses a five-phase pipeline:

  1. Score matrix + H1-H4 hard zeros (maritime/coastal/resource/slope/orbital constraints)
  2. Sort into constraint tiers: A (extraction, most constrained) → B (manufacturing) → C (service)
  3. Tier A greedy assignment (fewest valid attractors first)
  4. Hungarian maximum-weight bipartite matching on Tier B+C
  5. Synthetic attractor overflow for cities exceeding attractor supply

Mismatch flagging (lead decision): Warning (logged, no block) at score < 0.35; Error (blocks generation) at score < 0.15.

FoundingOrientation derived post-assignment: geographically_triggered cities derive from attractor_type; synthetic placements = AdminFacing.

Rationale: Priority ordering for the most-constrained cities prevents geographic mismatches from cascading. Hungarian algorithm guarantees optimal total compatibility score at N≤30, O(N³) ≈ negligible (~15ms). Synthetic attractors maintain the determinism principle (all positions placed at generation time).

Inputs: NamedCity records from atlas_city_names, GeographicAttractor list from D-C23, corp_presence from systems.db
Outputs: Vec (position, founding_orientation, geographically_triggered, political_tether) in BodyWorldState
Execution tier: Runtime-background (Layer 2)
Maps to ticket: Layer 2 Population Overlay (T-06)


D-C26: SettlementClass Enum

Decision: All generated settlements have a SettlementClass field determining their activation conditions:

enum SettlementClass {
    NameLocked,          // Named in atlas_city_names; always generated; activation = always active
    PopulationBudget,    // Unnamed; active if body_population_density > threshold
    EconomicTriggered,   // Unnamed; active if route_traffic_score > threshold
    OrganicGrowth,       // Unnamed; geographically_triggered = false
}

active: bool on GeneratedSettlement is the runtime economic sim state. placed_at_generation: bool on ProvinceWorldState is immutable.

Rationale: The distinction between "never settled" (WildernessBuffer) and "settled then abandoned" (AbandonedZone) requires the immutable generation-time flag. SettlementClass generalizes the latent settlement principle across all settlement types.

Inputs: NamedCity records (NameLocked), economic simulation state (EconomicTriggered), population density (PopulationBudget)
Outputs: SettlementClass per GeneratedSettlement
Execution tier: Layer 2 (class assignment); runtime simulation (active flag updates)
Maps to ticket: Layer 2 Population Overlay (T-06)


D-C27: TerritorialStatus Priority-Ordered Derivation

Decision: TerritorialStatus per Province is derived by priority-ordered classification (first match wins):

  1. AbandonedZone: placed_at_generation && !active
  2. WildernessBuffer: !placed_at_generation && settlement_count == 0
  3. ExtractiveZone: primary_economic_activity == Extraction && corporate_presence_score > 0.4
  4. ContestZone: jurisdiction_overlap_score > 0.3
  5. CoreTerritory: infrastructure_quality > 0.6 && corporate_road_maintenance > 0.5
  6. FrontierTerritory: default

Rationale: Priority ordering makes the algorithm explicit and debuggable. The single continuous threshold (infrastructure_quality > 0.6) separates Core from Frontier. Economic proxy conditions (corporate_presence_score, jurisdiction_overlap_score) reflect the underlying political economy without requiring per-cell authored data.

Province boundary visualization: Province boundaries are drainage basin divides (ridgelines between river basins from D8 routing) — they are topographic terrain features, legible as such on the planetary map without explicit border rendering. A TerritorialStatus overlay in the implant UI provides explicit color-coding when the player activates it.

Inputs: ProvinceWorldState (settlements, road_edges, geographic_features, corp_presence)
Outputs: TerritorialStatus per Province in BodyWorldState
Execution tier: Runtime-background (Layer 2, post-settlement placement)
Maps to ticket: Layer 2 Population Overlay (T-06)


D-C28: Three-Component District Mix Algorithm

Decision: District type allocation uses three components in sequence:

Component 1 — Population tier guarantees (minimum required types):

Population Guaranteed types
< 1,000 Residential, Mixed
1,0009,999 Residential, Commercial, Mixed
10,00099,999 + Entertainment
100,000499,999 + Administrative
500,000+ + Industrial/Civic if role-appropriate

Entertainment is guaranteed at 10,000+ (City tier), not Town tier. Below 10k, entertainment exists inside Commercial/Mixed character.

Component 2 — Economic role integer multiplier table (min value 3, row sums to 100): Full table from Burnelli-Sheldon Round 2, with political archetype modifiers stacked on top (CompanyTown: Industrial +10, Administrative 10; AdminCapital: Administrative +15; FreePort: Mixed +8, Administrative 20).

Component 3 — Founding age character modifier: Age modifies prosperity_baseline and character tags. Ages: New (0-49yr), Established (50-199yr), Mature (200-499yr), Ancient (500yr+). Applies only at Town tier and above, and only at Backwater WorldTier and above.

Self-containment: Algorithm references only local city/body/system fields. No neighboring city queries.

Rationale: Every settlement with people feels inhabited (no zero-weight types; guaranteed floors). Economic role determines character and proportion, not presence. Age produces accumulated variety without destroying economic identity.

Inputs: CityGenerationContext (population, economic_role, founding_age_years, world_tier, political_archetype)
Outputs: Vec allocations per city
Execution tier: Runtime-on-demand (Layer 3)
Maps to ticket: Layer 3 City Planning (T-07)


D-C29: FoundingOrientation Spatial Grid Rotation

Decision: FoundingOrientation modifies district placement positions (spatial grid rotation), not just the prosperity gradient direction. The district grid is oriented so the primary edge faces the founding geographic attractor.

Orientation → primary edge mapping:

  • PortFacing → South (harbor at south edge)
  • ResourceFacing → East
  • DefenseFacing → North (elevated/inland)
  • RailHeadFacing → West
  • AdminFacing → North (administrative at top)

Rationale: Directional legibility is a player mechanic — players approaching a PortFacing city from the sea should see the harbor face. Gradient-direction-only produces no approach-direction information. Spatial rotation makes city orientation readable from the planetary map.

Inputs: FoundingOrientation (from Layer 2 attractor-matching), district count, city footprint
Outputs: Rotated district position grid in city-local sim tiles
Execution tier: Runtime-on-demand (Layer 3)
Maps to ticket: Layer 3 City Planning (T-07)


D-C30: Five Explicit Political Archetype Spatial Patterns

Decision: All five political archetypes produce distinct, explicitly implemented district spatial arrangements:

Archetype Pattern Player experience
CompanyTown Spine: facility at primary edge, residential cascading back Player feels city's posture; geometry implies surveillance
AdminCapital Radial: administrative hub at center, prosperity gradient decreasing outward Power visible from everywhere; hub legible from any approach
FreePort Multi-node: 3-5 nodes with distinct character, no dominant center Productively disorienting; nodes must be visually distinct landmarks
Contested Dual-center overlay: two underlying geometries from competing powers, seams visible Player reads conflict in urban structure without text; both geometries must be legible
OrganicGrowth Irregular local density: center emerged, not declared; no spine, no hub, no nodes Most "lived-in" feel; reads as older and more authentic

Acceptance criterion (Ozzie): Player identifies archetype from 15 seconds of walking around, without consulting the implant UI.

Rationale: Emergent-only patterns produce inconsistent legibility and waste the investment in the archetype system. Explicit patterns create player knowledge that pays off over multiple cities. Same archetype = consistent navigation challenge = player skill progression.

Inputs: PoliticalArchetype, district_positions, district_types from D-C28
Outputs: Spatially arranged district positions conforming to pattern
Execution tier: Runtime-on-demand (Layer 3)
Maps to ticket: Layer 3 City Planning (T-07)


D-C31: founding_age → layout_mode Probability Weighting

Decision: Founding age shifts layout_mode selection probabilities (not deterministic assignment). Older settlements have higher Organic layout probability; younger settlements have higher Grid probability. Archetype-type combinations that resist age influence (e.g., CompanyTown Industrial districts) have reduced age effect.

Age organic bonus: 0.0 (New <50yr) / 0.1 (Established 50-199yr) / 0.25 (Mature 200-499yr) / 0.40 (Ancient 500yr+). Resistance: CompanyTown Industrial = 0.9, AdminCapital Administrative = 0.6, FreePort = 0.0, default = 0.3.

Rationale: Deterministic age-to-layout assignment produces edge case failures (ancient planned facilities shouldn't be irregular). Probability weighting encodes the design intent while preserving interesting variance. The in-world rationale is legible: ancient cities grew organically over time; planned industrial facilities maintain their layout regardless of age.

Inputs: founding_age_years, DistrictType, PoliticalArchetype
Outputs: layout_mode per DistrictSkeleton
Execution tier: Runtime-on-demand (Layer 3)
Maps to ticket: Layer 3 City Planning (T-07)


D-C32: Tile Condition Threshold Values

Decision: Tile condition derives from prosperity_current using offset thresholds:

  • Intact: prosperity_current ≥ 0.63
  • Worn: 0.43 ≤ prosperity_current < 0.63
  • Cracked: 0.23 ≤ prosperity_current < 0.43
  • Broken: prosperity_current < 0.23

Chunk invalidation: cached condition state regenerated when prosperity_current crosses a threshold boundary. Check per district per economic tick (one float comparison per district).

Rationale: Offset values (0.63/0.43/0.23 rather than 0.60/0.40/0.20) prevent oscillation when prosperity fluctuates near a boundary. Threshold-crossing invalidation avoids per-frame recomputation while keeping condition state current. Most districts are economically stable — invalidation is rare.

Inputs: prosperity_current per district (runtime economic sim)
Outputs: TileCondition enum per chunk (cached in ChunkConditionState)
Execution tier: Runtime-on-demand (Layer 4, cached)
Maps to ticket: Layer 4 Street Rendering (T-08)


D-C33: ARCH-1 Heightmap BLOB Storage Schema

Decision: Heightmap stored as atlas_body_heightmaps(body_id, data BLOB) where data is float32 little-endian, 512×256 entries = 524,288 bytes per body (~512KB). No compression. bytemuck::cast_slice for zero-copy deserialization in Rust.

Rationale: Storage ~200MB for 400 bodies — acceptable. No compression needed at this size. Zero-copy deserialization is O(1) and avoids allocation.

Maps to ticket: Heightmap BLOB Storage (T-01)


D-C34: Background Thread Infrastructure

Decision: Layer 1-2 generation runs on a rayon thread pool via GenerationQueue (BinaryHeap by priority). Results stored in GenerationCache (LRU, 50 bodies, Arc). Text scanning for High-priority trigger uses Aho-Corasick on system/body names. Diegetic placeholder tiers (four levels from terrain-only down to blinking cursor) handle incomplete generation states on the planetary map.

Rationale: Rayon provides work-stealing thread pool without requiring a custom executor. LRU at 50 bodies keeps memory bounded (~5MB) while covering all realistic player proximity scenarios. Aho-Corasick is O(text_length) regardless of pattern count — efficient for scanning all body names against rendered text.

Maps to ticket: Background Thread Infrastructure (T-10)


Part 3: Implementation Ticket Dependency Chain

Eleven tickets required. Three can start immediately in parallel; the rest are sequentially gated.

Ticket Definitions

T-01: Heightmap BLOB Storage (ARCH-1)

  • Schema: atlas_body_heightmaps(body_id, data BLOB)
  • Python: generate_atlas.py writes heightmap data to this table
  • Meta stamp update for ARCH-1
  • Effort: ~1d
  • Dependencies: none

T-02: BodyWorldState Bevy Resource (ARCH-2)

  • GenerationCache struct with LRU(50)
  • BodyWorldState struct matching D-C22 outputs
  • Arc<BodyWorldState> wiring
  • Effort: ~1d
  • Dependencies: T-01 (heightmap field type), T-03 (city names field type), T-04 (radius field)

T-03: atlas_city_names Schema (ARCH-3)

  • DDL: atlas_city_names(id, body_id, name, corp_id, tier_hint, reserved)
  • Index: idx_city_names_body
  • Migration in MIGRATION_SQL (idempotent CREATE TABLE IF NOT EXISTS)
  • Update generate_atlas.py to populate from wiki city data
  • Effort: ~1d
  • Dependencies: none

T-04: body_radius_km Column (ARCH-4)

  • DDL: ALTER TABLE bodies ADD COLUMN body_radius_km REAL
  • Migration entry + planet_class fallback in Rust read
  • Area count formula: round(sqrt(surface_area_km2 / 6_000_000)) derived field
  • Effort: ~0.5d
  • Dependencies: none

T-05: WorldTier Enum Fix

  • Replace Peripheral | Connected | Core with Epicenter | Regional | Backwater | Passage | Waypoint
  • Update all match arms downstream
  • Effort: ~0.5d
  • Dependencies: none (but blocks everything that uses WorldTier)

T-06: Layer 1 Empty World Generator

  • D8 drainage routing (port from planet_simulation.py)
  • Geographic feature tag extraction (D-C23)
  • Sub-biome variant classification (D-C24)
  • terrain_modification_cost computation
  • Outputs to BodyWorldState
  • Effort: ~2d
  • Dependencies: T-01 (heightmap read), T-02 (BodyWorldState), T-05 (WorldTier), T-10 (background thread infra)

T-07: Layer 2 Population Overlay

  • Attractor-matching five-phase pipeline (D-C25)
  • Road graph generation (MST + organic connectors + MaintenanceAuthority)
  • Sub-settlement placement
  • TerritorialStatus derivation (D-C27)
  • SettlementClass assignment (D-C26)
  • Outputs to BodyWorldState
  • Effort: ~3d
  • Dependencies: T-06 (Layer 1 output), T-03 (city names for attractor-matching)

T-08: CityGenerationContext Wiring

  • Populate CityGenerationContext from BodyWorldState + systems.db at session startup
  • Add body_id, city_name, founding_orientation fields (from D-C25)
  • Add founding_age_years field (wiki-authored or systems.db derived)
  • Add orbital_approach_direction: Option<CardinalDirection> (L3-Q7)
  • Effort: ~1d
  • Dependencies: T-03 (city names), T-04 (radius / area count), T-05 (WorldTier), T-07 (Layer 2 output)

T-09: Layer 3 City Planning

  • District count formula (D-C4, locked)
  • District grid layout with FoundingOrientation rotation (D-C29)
  • Political archetype spatial arrangement, all five patterns (D-C30)
  • Three-component district type allocation (D-C28)
  • founding_age → layout_mode weighting (D-C31)
  • DistrictSkeleton Stage 1 + Stage 2 generation
  • prosperity_baseline derivation (Burnelli-Sheldon formula + Paula topographic modifier)
  • Effort: ~3d
  • Dependencies: T-08 (CityGenerationContext), T-05 (WorldTier)

T-10: Background Thread Infrastructure

  • GenerationQueue (BinaryHeap by priority)
  • Rayon thread pool launcher
  • GenerationCache LRU wiring
  • SystemNameIndex with Aho-Corasick for text-triggered generation
  • Priority elevation hooks (player proximity, dialogue text scan, gate graph adjacency)
  • Diegetic placeholder state machine (four tiers) — stubs for client integration
  • Effort: ~2d
  • Dependencies: T-02 (BodyWorldState type)

T-11: Layer 4 Street Rendering

  • Street skeleton from BlockSkeleton door positions (minimum corridor widths from D-C29)
  • Building fill (density-driven rectangle placement)
  • TileEntry generation with tile_id + walkable + condition
  • ChunkConditionState with threshold-crossing invalidation (D-C32)
  • Pre-fetch interior generation trigger at N=32 tiles
  • Chunk streaming hookup: load_chunk()generate_chunk_tiles()
  • Effort: ~2d
  • Dependencies: T-09 (DistrictSkeleton output), T-05 (WorldTier)

Dependency Graph

Phase 1 — All can start immediately in parallel:
  T-01: Heightmap BLOB Storage         (~1d)
  T-03: atlas_city_names Schema        (~1d)
  T-04: body_radius_km Column          (~0.5d)
  T-05: WorldTier Enum Fix             (~0.5d)  ← BLOCKER for all Rust generation code

Phase 2 — After T-01, T-03, T-04 complete:
  T-02: BodyWorldState Bevy Resource   (~1d)
  
Phase 3 — After T-02 complete:
  T-10: Background Thread Infra        (~2d)
  
Phase 4 — After T-10 AND T-05 complete:
  T-06: Layer 1 Empty World Generator  (~2d)
  
Phase 5 — After T-06 AND T-03 complete:
  T-07: Layer 2 Population Overlay     (~3d)
  
Phase 6 — After T-07, T-03, T-04, T-05 complete:
  T-08: CityGenerationContext Wiring   (~1d)
  
Phase 7 — After T-08 AND T-05 complete:
  T-09: Layer 3 City Planning          (~3d)
  
Phase 8 — After T-09 AND T-05 complete:
  T-11: Layer 4 Street Rendering       (~2d)
  
WALKABLE WORLD: T-11 complete

Critical Path

T-05 (WorldTier fix) → T-06 (Layer 1) → T-07 (Layer 2) → T-08 (Context) → T-09 (Layer 3) → T-11 (Layer 4)

With parallel schema setup: T-01/T-03/T-04 → T-02 → T-10 feeds into the same critical path at Phase 4.

Minimum critical path duration: T-05 (0.5d) + T-10 (2d) + T-06 (2d) + T-07 (3d) + T-08 (1d) + T-09 (3d) + T-11 (2d) = 13.5 dev-days on a single agent. Schema work (T-01, T-02, T-03, T-04) can be parallelized off this path.

Rescoping Note on Prior Tickets

The brief mentions "#899 et al." that need rescoping against the new layer model. The primary impact:

  • Any ticket that assumed generate_regional.py would run at build-time for Layer 1-2 spatial data needs rescoping to the Rust runtime-background model (D-C18)
  • Any ticket that assumed authored city positions in markers.json needs rescoping to the attractor-matching model (D-C20)
  • Any ticket that referenced atlas_cities for position data needs redirecting to the new Layer 2 GeneratedSettlement output
  • Any ticket that uses the wrong WorldTier enum values is blocked until T-05 completes

The specific ticket numbers should be reviewed against this list and updated in the ticketing system.


Part 4: Remaining Open Questions for Lead

These are items the workshop identified but could not close without lead input.

Lead-Required Decisions

Item What's needed
Full multiplier table confirmation The integer weights table needs lead sign-off on all 8×7 values before it becomes the locked D-C28 table. Burnelli-Sheldon's Round 2 table covers primary roles; the full table should be confirmed as a D-record.
atlas_feature_names population path Paula's Stage 1 name fulfillment assigns names to geographic features (rivers, mountain ranges, bays). Who authors these names? The wiki content seems like the correct source — but this requires wiki authors to document feature names alongside city names.
L3-Q7 scope (orbital approach direction) orbital_approach_direction: Option<CardinalDirection> field on CityGenerationContext. Data is available (one systems.db query). Client rendering of the field is separate work. Include the field in T-08? My recommendation: yes, at zero implementation cost.
Scatter scope unlock timeline Prop spawn points were deferred (lead decision). The hook exists (TileEntry spawn_category field proposed in gestalt-round2.md). When does this become undeferred? This determines the minimum viable Layer 4 feel.

Items Closed by This Workshop (No Lead Action Required)

Item Closed by
Mismatch flag threshold Lead two-tier decision (Q1 above)
founding_age → layout_mode Probability weighting adopted (Q2 above)
Province boundary legibility Watershed-as-terrain-feature (Q3 above)
atlas_city_names population path Wiki import pipeline (Q4 above)
Self-contained settlements Confirmed by Burnelli-Sheldon + Gestalt
OrganicGrowth disambiguation geographically_triggered flag (Paula + Gestalt)
Session DB vs systems.db for Layer 1-2 BodyWorldState in-memory LRU (Tyre ARCH-2)

Part 5: D-Record Candidate Register Summary

ID Decision Status
D-C1 through D-C17 Generation-cascade workshop decisions Given Facts in brief (not re-opened)
D-C18 Three-tier execution model New — this workshop
D-C19 Background generation priority queue New — this workshop
D-C20 Fully generative placement New — this workshop
D-C21 Spatial hierarchy — eight tiers with dimensions New — this workshop
D-C22 D8 drainage routing New — this workshop
D-C23 Geographic feature tag extraction New — this workshop
D-C24 Sub-biome variant classification New — this workshop
D-C25 Attractor-matching five-phase pipeline New — this workshop
D-C26 SettlementClass enum New — this workshop
D-C27 TerritorialStatus priority-ordered derivation New — this workshop
D-C28 Three-component district mix algorithm New — this workshop
D-C29 FoundingOrientation spatial grid rotation New — this workshop
D-C30 Five explicit political archetype spatial patterns New — this workshop
D-C31 founding_age → layout_mode probability weighting New — this workshop
D-C32 Tile condition threshold values (0.63/0.43/0.23) New — this workshop
D-C33 ARCH-1 heightmap BLOB storage schema New — this workshop
D-C34 Background thread infrastructure New — this workshop

Part 6: What This Workshop Produced

The planet-down-cascade workshop set out to answer: given the four-layer cascade (Empty World → Population Overlay → City Planning → Street Rendering), what are the concrete algorithms at each layer?

The workshop produced:

  1. A complete algorithm specification for each of the four layers — pseudocode-level detail sufficient for implementation without further design work
  2. Data format handoffs per layer — what lives in systems.db (build-time), what lives in BodyWorldState (runtime-background), what is never stored (runtime-on-demand)
  3. 17 new D-record candidates (D-C18 through D-C34)
  4. An implementation ticket dependency chain (11 tickets, ~13.5 dev-days critical path)
  5. Resolved open questions: attractor-matching algorithm, TerritorialStatus thresholds, district mix algorithm, mismatch flagging thresholds, founding_age → layout_mode, Province boundary legibility

The architectural pivot from the consultant review (Amendment 1) was fully absorbed. The prior workshop's Phase 3/Phase 5 framing is replaced by a three-tier model that correctly separates build-time economics data from runtime spatial generation.

The three hardest design problems — which had four competing approaches each — converged:

  • Attractor-matching: Hungarian algorithm + priority ordering + synthetic overflow
  • TerritorialStatus: priority-ordered algorithm with single continuous threshold
  • District mix: three-component model, no zero weights, population guarantees

The cascade is algorithmically specified, dependency-ordered, and ready for implementation planning.


Gestalt — Round 3. Written 2026-05-01.