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>
39 KiB
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:
- Terrain heightmap — renders the ridgelines and basin shapes naturally as topographic relief
- River network overlay — rivers flow through Province interiors toward confluence points
- 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.pyreads wiki data and populatesatlas_city_namesrows- 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:
- Score matrix + H1-H4 hard zeros (maritime/coastal/resource/slope/orbital constraints)
- Sort into constraint tiers: A (extraction, most constrained) → B (manufacturing) → C (service)
- Tier A greedy assignment (fewest valid attractors first)
- Hungarian maximum-weight bipartite matching on Tier B+C
- 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):
- AbandonedZone: placed_at_generation && !active
- WildernessBuffer: !placed_at_generation && settlement_count == 0
- ExtractiveZone: primary_economic_activity == Extraction && corporate_presence_score > 0.4
- ContestZone: jurisdiction_overlap_score > 0.3
- CoreTerritory: infrastructure_quality > 0.6 && corporate_road_maintenance > 0.5
- 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,000–9,999 | Residential, Commercial, Mixed |
| 10,000–99,999 | + Entertainment |
| 100,000–499,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.pywrites heightmap data to this table - Meta stamp update for ARCH-1
- Effort: ~1d
- Dependencies: none
T-02: BodyWorldState Bevy Resource (ARCH-2)
GenerationCachestruct with LRU(50)BodyWorldStatestruct matching D-C22 outputsArc<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(idempotentCREATE TABLE IF NOT EXISTS) - Update
generate_atlas.pyto 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 | CorewithEpicenter | 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_orientationfields (from D-C25) - Add
founding_age_yearsfield (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.pywould 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_citiesfor 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:
- A complete algorithm specification for each of the four layers — pseudocode-level detail sufficient for implementation without further design work
- 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)
- 17 new D-record candidates (D-C18 through D-C34)
- An implementation ticket dependency chain (11 tickets, ~13.5 dev-days critical path)
- 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.