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

739 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Tyre Round 3 — Planet-Down Cascade Workshop"
author: tyre
workshop: planet-down-cascade
round: 3
created: 2026-05-01
---
# Tyre Round 3 — Planet-Down Cascade Workshop
## Summary
Round 3 convergence document. Contains D-record candidates for all locked technical decisions, resolution of four open questions from Round 2, finalized Rust types and SQL DDL, and the implementation ticket dependency chain.
Lead-resolved before Round 3: two-tier mismatch flagging (`score < 0.35` = warning, `score < 0.15` = error/blocks).
---
## 1. Lead-Resolved Items
### Q1 — Mismatch Flag Threshold (LOCKED)
Two-tier system:
- `score < 0.35`: `MismatchSeverity::Soft``flagged_for_review = true`; placement proceeds with warning in generation log
- `score < 0.15`: `MismatchSeverity::Hard` — assignment overridden to `Synthetic { reason: PoliticalDecision }`; city placed at Province centroid; `FoundingOrientation = AdminFacing`; generation proceeds without panic
```rust
enum MismatchSeverity {
None, // best attractor score >= 0.35
Soft, // 0.15 <= score < 0.35; placement proceeds with flag
Hard, // score < 0.15; overridden to Synthetic
}
```
`Hard` mismatch is not a generation failure — it's a policy decision. The city exists but was placed politically, not geographically. The flag is surfaced in developer tooling and the eventual city history log.
---
## 2. Convergence Items — Round 3 Resolutions
### Q2 — founding_age → layout_mode (Ozzie's Proposal)
**Proposal:** founding age should influence block geometry, making old cities irregular and young cities grid-like.
**Technical evaluation:**
The five spatial archetypes (CompanyTown, AdminCapital, FreePort, Contested, OrganicGrowth) determine *macro geometry* — spine, radial, multi-node. These reflect how the city was *designed* and should not be modified by age.
Block geometry is orthogonal: it reflects how the city *evolved after* being designed. A CompanyTown laid out as a rigid grid in Year 1 may have irregular blocks in Year 200 as organic infill accumulated around the original plan.
**Resolution: ADOPT via `BlockIrregularity` field — do NOT modify `layout_mode`.**
`layout_mode` (archetype) stays fixed. A new `block_irregularity: BlockIrregularity` field is added to `DistrictSkeleton`, derived from `founding_age_years` and `SpatialArchetype`. This is orthogonal to the archetype — CompanyTown can be Grid or Organic depending on age.
```rust
enum BlockIrregularity {
Grid, // Regular 64m × 64m blocks; planned geometry
SlightlyWorn, // ~10% deviation from grid; some organic infill
Irregular, // ~25% deviation; multiple planning generations visible
Organic, // No regular grid; emerged rather than planned
}
impl BlockIrregularity {
fn from_age_and_archetype(founding_age_years: u32, archetype: SpatialArchetype) -> Self {
// Reference age (years to reach next irregularity tier)
let step: u32 = match archetype {
SpatialArchetype::CompanyTown => 40, // Company towns evolve quickly
SpatialArchetype::AdminCapital => 100, // State capitals resist change
SpatialArchetype::FreePort => 60,
SpatialArchetype::Contested => 35, // Conflict accelerates irregularity
SpatialArchetype::OrganicGrowth => 20, // Always evolving; reaches Organic fastest
};
match founding_age_years / step.max(1) {
0 => Self::Grid,
1 => Self::SlightlyWorn,
2 => Self::Irregular,
_ => Self::Organic,
}
}
}
```
`block_irregularity` is a `DistrictSkeleton` field, not `CityGenerationContext` — it varies per district within a city. Derivation is fully local; no cross-city queries.
**Phase implication:** This field is set at Layer 3 and consumed at Layer 4 (street rendering). It has no effect on Phase 2 or 3 deliverables. No rework to the existing convergence.
---
### Q3 — Province Boundary Legibility
**Requirement (Ozzie, non-negotiable):** Province boundaries must render as natural watershed lines on the planetary map, not arbitrary grid edges.
**Technical resolution:**
Province boundaries are ridgelines between drainage basins. The D8 drainage simulation computes flow direction per cell; ridgelines are cells where no adjacent cell drains into the current cell from the "wrong side." Extracting boundary polylines from a D8 result is O(grid_size) — standard watershed delineation.
**Two options evaluated:**
Option A: Rust runtime computes drainage → extracts boundaries → stores in savegame DB.
Option B: Python pipeline computes drainage at build time → extracts boundaries → stores in systems.db.
**Resolution: Option B.** The planetary map must render Province boundaries immediately when the player opens it, before any Rust generation has run. Option A introduces a generation-status dependency in the renderer. Option B eliminates it: boundaries are pre-computed at `make regen-db` time and available on first map open.
`planet_simulation.py` already computes a DEM per body. Watershed boundary extraction is a post-processing step over the same data. The Python implementation does not need the full D8 precision of the Rust runtime — it's a rendering hint, not game logic.
**New schema:**
```sql
CREATE TABLE atlas_province_boundaries (
body_id INTEGER NOT NULL REFERENCES bodies(id),
province_x INTEGER NOT NULL,
province_y INTEGER NOT NULL,
boundary BLOB NOT NULL,
-- float32 pairs [u0,v0, u1,v1, ...] in atlas UV space (0.0..1.0)
-- Boundary polyline tracing the natural watershed outline of this province
PRIMARY KEY (body_id, province_x, province_y)
);
CREATE INDEX idx_province_bounds_body ON atlas_province_boundaries(body_id);
```
Storage estimate: ~20 float32 pairs per province boundary segment × ~64 segments per body → ~82KB/body → ~32MB for 400 bodies. Acceptable.
**Renderer contract:**
- On map open: load all `boundary` BLOBs for `body_id`; render each as a polyline in atlas UV space
- No dependency on `BodyWorldState` generation status
- The Rust L1 drainage computes the same basin topology independently for generation purposes; renderer never waits for it
**Pipeline change:** `generate_atlas.py` adds a watershed extraction step after heightmap storage. The DDL addition goes into `import_economics.py`'s `MIGRATION_SQL` block (runs first; creates the table); `generate_atlas.py` populates it.
---
### Q5 — atlas_city_names Population Path
**Question:** Who writes source rows to `atlas_city_names` before Stage 0 fulfillment?
**Analysis:** The schema has `corp_id` and `tier_hint` — deliberate authoring fields. But with ~400 bodies and 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.
```toml
# wiki/worlds/nova-kassel.toml
[[cities]]
name = "Port Cassidy"
corp_slug = "meridian-transit" # nullable
tier_hint = 3 # nullable; expected WorldTier integer
reserved = true
```
**Tier 2 — Generated (`reserved = false`):** `import_economics.py` generates additional name rows from the corporation brand files. Corp HQ worlds receive at least one corp-affiliated city name derived from brand name + city-name templates from the `generate_brands` output. These are placeholders — no specific lore attachment; placed by attractor-matching opportunistically.
Source population order in `import_economics.py`:
1. Preserve existing `reserved = true` rows for the body across reruns
2. Delete existing `reserved = false` rows (regenerated fresh each run)
3. Import authored rows from `wiki/worlds/*.toml``reserved = true`
4. Generate corp-derived name rows for bodies below `corp_city_quota``reserved = false`
Paula's Stage 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`.
```sql
CREATE TABLE atlas_body_heightmaps (
body_id INTEGER NOT NULL REFERENCES bodies(id),
data BLOB NOT NULL, -- float32 LE, 512×256 = 524,288 bytes
PRIMARY KEY (body_id)
);
```
```rust
fn load_heightmap(conn: &Connection, body_id: i64) -> Result<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.
```rust
#[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.
```sql
CREATE TABLE atlas_city_names (
id INTEGER PRIMARY KEY,
body_id INTEGER NOT NULL REFERENCES bodies(id),
name TEXT NOT NULL,
corp_id INTEGER REFERENCES corporations(id),
tier_hint INTEGER,
reserved BOOLEAN NOT NULL DEFAULT 0
);
CREATE INDEX idx_city_names_body ON atlas_city_names(body_id);
```
Source TOML: `wiki/worlds/{body_slug}.toml``[[cities]]` arrays. Authoring format documented in Q5 resolution above.
---
### D-candidate: Body Radius Column (ARCH-4)
**Domain:** architecture
**Decision:** `bodies.body_radius_km` nullable REAL column; Rust reads with `planet_class` fallback.
```sql
ALTER TABLE bodies ADD COLUMN body_radius_km REAL;
```
```rust
fn body_radius_km(row: &Row) -> f64 {
row.get::<_, Option<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.
```sql
CREATE TABLE atlas_province_boundaries (
body_id INTEGER NOT NULL REFERENCES bodies(id),
province_x INTEGER NOT NULL,
province_y INTEGER NOT NULL,
boundary BLOB NOT NULL,
-- float32 pairs [u0,v0, u1,v1, ...] in atlas UV space (0.0..1.0)
PRIMARY KEY (body_id, province_x, province_y)
);
CREATE INDEX idx_province_bounds_body ON atlas_province_boundaries(body_id);
```
Schema added to `import_economics.py` MIGRATION_SQL (creates table). `generate_atlas.py` populates it after heightmap storage. Renderer loads by `body_id` and renders as polylines.
---
### D-candidate: D8 Priority-Flood Drainage — Layer 1 (GEN-1)
**Domain:** architecture (generation)
**Decision:** Layer 1 uses D8 priority-flood drainage in Rust to derive river networks and geographic attractors from heightmaps. Target: ~50ms at 512×256.
Algorithm:
1. Load heightmap from `BodyWorldState` (ARCH-2)
2. D8 single-direction flow: each cell drains to lowest adjacent neighbor (8 directions)
3. Priority-flood fills sinks: `BinaryHeap<(Reverse<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.
```rust
enum SettlementClass {
NameLocked, // Has a name in atlas_city_names; always active
PopulationBudget, // Active if body_population_density > threshold
EconomicTriggered, // Active if route_traffic_score > threshold
OrganicGrowth, // Placed by geographic probability; geographically_triggered = false
}
```
---
### D-candidate: Background Generation Queue (GEN-7)
**Domain:** architecture
**Decision:** Background body generation uses a rayon thread pool (not async Tokio). Priority ordering based on player position, travel routes, and body name mentions in dialogue text.
```rust
struct GenerationQueue {
pending: BinaryHeap<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).
```sql
CREATE TABLE atlas_feature_names (
id INTEGER PRIMARY KEY,
body_id INTEGER NOT NULL REFERENCES bodies(id),
name TEXT NOT NULL,
tag_hint TEXT -- nullable; expected FeatureTag for this name
);
CREATE INDEX idx_feature_names_body ON atlas_feature_names(body_id);
```
---
## 4. Final Type Specifications
### CityGenerationContext — Final Fields
```rust
struct CityGenerationContext {
// Core identity
name: String,
body_id: i64,
seed: u64,
// Location
location: (u8, u8), // atlas grid cell
region_size: (u8, u8), // cells the city region occupies
// Population and economics
population: u64,
economic_role: EconomicRole,
corp_presence: Vec<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`:
```rust
// New field — derived at Layer 3 from founding_age_years + spatial_archetype
block_irregularity: BlockIrregularity,
```
### New Enum Types
```rust
enum SpatialArchetype {
CompanyTown, // Spine pattern
AdminCapital, // Radial pattern
FreePort, // Multi-node pattern (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:
```rust
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 thresholds** — `prosperity_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.*