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>
This commit is contained in:
2026-05-03 20:18:30 +02:00
co-authored by Claude Opus 4.6
parent 42ee1f0a0e
commit b9fd75b840
23 changed files with 11548 additions and 0 deletions
@@ -0,0 +1,738 @@
---
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 10–30 cities each, hand-authoring all names is not tractable.
**Resolution: Two-tier authorship.**
**Tier 1 — Authored (`reserved = true`):** World designers write named cities for canon locations in `wiki/worlds/{body_slug}.toml`. These are named places with canonical identities — they appear in lore, faction text, and player-facing narrative. Imported by `import_economics.py`. All `corp_id` and `tier_hint` populated by the author.
```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 0–3 fulfillment pipeline (runtime) operates on whatever rows exist at generation time, regardless of source.
---
### Q6 — Full Multiplier Table (Locked)
Adding `Logistics` as the 8th district type (warehousing, distribution, freight staging). The 7-column table from Round 2 omits this type; it's essential for Transit/Port and Manufacturing cities and has minimum 3 weight across all rows.
**Final locked table** (all rows sum to 100; minimum value across all cells: 3):
| Economic Role | Res | Com | Ind | Adm | Ent | Civ | Mix | Log |
|-------------------|-----|-----|-----|-----|-----|-----|-----|-----|
| Mining/Extraction | 33 | 11 | 26 | 8 | 3 | 5 | 9 | 5 |
| Manufacturing | 27 | 13 | 23 | 8 | 5 | 7 | 10 | 7 |
| Research Hub | 26 | 17 | 9 | 15 | 8 | 12 | 9 | 4 |
| Commercial Hub | 20 | 28 | 7 | 10 | 12 | 8 | 10 | 5 |
| Administrative | 18 | 14 | 5 | 28 | 8 | 15 | 7 | 5 |
| Transit/Port | 20 | 16 | 13 | 7 | 5 | 5 | 16 | 18 |
| Energy | 30 | 9 | 27 | 8 | 3 | 6 | 10 | 7 |
| Agricultural | 32 | 16 | 9 | 5 | 7 | 8 | 16 | 7 |
Row sums: 100, 100, 100, 100, 100, 100, 100, 100. Minimum cell value: 3 (Mining/Ent and Energy/Ent). Floor invariant holds.
**Political archetype modifiers (applied after role table; floor at 3):**
All five archetypes need modifiers. Round 2 specified three (CompanyTown, AdminCapital, FreePort). The two missing ones are proposed here and require lead confirmation before locking.
| Archetype | Adjustment |
|-----------------|------------|
| CompanyTown | Administrative −10, Industrial +10 |
| AdminCapital | Administrative +15, Commercial −8, Entertainment −7 |
| FreePort | Commercial +12, Mixed +8, Administrative −20 |
| Contested | Mixed +10, Civic −5, Administrative −5 *(proposed — needs lead confirmation)* |
| OrganicGrowth | Mixed +15, Commercial +5, Administrative −10, Industrial −10 *(proposed — needs lead confirmation)* |
Note: modifier application must re-check the floor — if any cell drops below 3 after modifier, clamp to 3 and redistribute the deficit proportionally across the row.
---
## 3. D-Record Candidates
These are proposed D-records. IDs must be claimed via `tooling/db/decision claim D <domain> "title"` before writing to `decisions/` domain files. All items require corresponding implementation tickets.
---
### D-candidate: Heightmap Storage Schema (ARCH-1)
**Domain:** architecture
**Decision:** Planetary heightmaps are stored as float32 LE BLOBs in `atlas_body_heightmaps` in systems.db; written by `generate_atlas.py`; loaded into `BodyWorldState` via `bytemuck::cast_slice`.
```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 0–3 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()` (H1–H4) runs before scoring; zeros are structural, not low scores
- Minimum non-zero score: 0.10 (no compatible attractor scores below this floor)
- Tier A: `MiningExtraction | 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 (3–5 nodes)
Contested, // Dual-center overlay pattern
OrganicGrowth, // Irregular local density pattern
}
enum EconomicRole {
MiningExtraction,
Manufacturing,
ResearchHub,
CommercialHub,
Administrative,
TransitPort,
Energy,
Agricultural,
}
enum DistrictType {
Residential,
Commercial,
Industrial,
Administrative,
Entertainment,
Civic,
MixedUse,
Logistics, // NEW: warehousing, distribution, freight staging
}
enum MismatchSeverity {
None, // score >= 0.35
Soft, // 0.15 <= score < 0.35
Hard, // score < 0.15; overridden to Synthetic
}
```
### `GenerateChunkData` Upgrade (Prior Workshop Item, Still Required)
`GeneratorChunkData = Vec<bool>` must become `Vec<TileEntry>`. `TileEntry` needs at minimum:
```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 1–3 | 5ms | ~5ms |
| L3 | District mix (all cities) | 15ms | ~20ms |
| L3 | TerritorialStatus (all provinces) | 25ms | ~25ms |
| L3 | WorldTier assignment + prosperity | 5ms | ~10ms |
| **Total** | | **134ms** | **~143ms** |
| Session budget (cold start) | 700ms | | |
| Headroom | | | 4.9× |
**Defensive check:** Hungarian is O(N³). At N = 30: ~15ms. At N = 50: ~400ms (exceeds L2 budget alone). If any body has > 40 named cities: switch Tier C to greedy for cities where best-available-attractor score > 0.5, then run Hungarian only on the remainder. This bound has not been hit on any current body but needs the guard.
---
## 7. Implementation Ticket Dependency Chain
Effort in dev-days. All `SCHEMA-*` tickets can be grouped into a single migration PR.
### Tier 0 — Prerequisites (No Dependencies)
| Ticket | Work | Effort | File(s) |
|--------|------|--------|---------|
| BUG-WorldTier | Fix `WorldTier` enum; update all match arms | 0.5d | `server/src/simulation/generator.rs` |
| SCHEMA-bodies | `body_radius_km REAL` column | 0.25d | `systems-schema.sql`, `import_economics.py` MIGRATION_SQL |
| SCHEMA-heightmaps | `atlas_body_heightmaps` DDL | 0.25d | `systems-schema.sql`, `import_economics.py` MIGRATION_SQL |
| SCHEMA-city-names | `atlas_city_names` DDL + index | 0.25d | `systems-schema.sql`, `import_economics.py` MIGRATION_SQL |
| SCHEMA-feature-names | `atlas_feature_names` DDL + index | 0.25d | `systems-schema.sql`, `import_economics.py` MIGRATION_SQL |
| SCHEMA-province-bounds | `atlas_province_boundaries` DDL + index | 0.25d | `systems-schema.sql`, `import_economics.py` MIGRATION_SQL |
### Tier 1 — Python Pipeline (Depends on Tier 0 schemas)
| Ticket | Work | Effort | Depends on |
|--------|------|--------|------------|
| PY-heightmap-import | `generate_atlas.py`: BLOB-pack elevation float32; INSERT into `atlas_body_heightmaps` | 1d | SCHEMA-heightmaps |
| PY-province-bounds | `generate_atlas.py`: watershed extraction from DEM; store boundary polylines | 2d | SCHEMA-province-bounds, PY-heightmap-import |
| PY-city-names-authored | `import_economics.py`: read `wiki/worlds/*.toml` `[[cities]]`; INSERT with `reserved = true` | 1d | SCHEMA-city-names |
| PY-city-names-corp | `import_economics.py`: generate corp-derived name rows for under-quota bodies | 1d | PY-city-names-authored |
| PY-body-radius | `import_economics.py`: populate `body_radius_km` from planet_class defaults or authored values | 0.5d | SCHEMA-bodies |
### Tier 2 — Rust Type Definitions (Depends on Tier 0)
| Ticket | Work | Effort | Depends on |
|--------|------|--------|------------|
| RS-types-worldtier | Fix `WorldTier` enum in Rust; update all match arms | 0.5d | BUG-WorldTier |
| RS-types-settlement | `SettlementClass`, `MismatchSeverity`, `AttractorAssignment`, `SyntheticPlacementReason` | 0.5d | RS-types-worldtier |
| RS-types-district | Add `BlockIrregularity` to `DistrictSkeleton`; `SpatialArchetype`, `EconomicRole`, `DistrictType::Logistics` | 0.5d | — |
| RS-types-territorial | `TerritorialStatus` with correct variants | 0.5d | — |
| RS-types-city-ctx | Finalize `CityGenerationContext` with all new fields | 1d | RS-types-settlement, RS-types-territorial |
### Tier 3 — Rust Core (Depends on Tier 1 + Tier 2)
| Ticket | Work | Effort | Depends on |
|--------|------|--------|------------|
| RS-heightmap-load | `load_heightmap()` via bytemuck; integration into `BodyWorldState` init | 0.5d | PY-heightmap-import, RS-types-city-ctx |
| RS-body-state | `BodyWorldState` struct + `GenerationCache` Bevy Resource + LRU cache | 1.5d | RS-heightmap-load |
| RS-drainage | D8 priority-flood drainage; attractor extraction; RiverNetwork construction | 3d | RS-body-state |
| RS-attractor-types | `GeographicAttractor`, `AttractorType`, `CompatibilityMatrix` | 0.5d | RS-types-settlement |
### Tier 4 — Generation Algorithms (Depends on Tier 3)
| Ticket | Work | Effort | Depends on |
|--------|------|--------|------------|
| RS-attractor-assign | Five-phase attractor assignment + Hungarian + synthetic overflow + mismatch flags | 4d | RS-drainage, RS-attractor-types, PY-city-names-authored |
| RS-district-mix | Three-component district mix + locked multiplier table + political archetype modifiers | 3d | RS-attractor-assign, RS-types-district |
| RS-territorial | TerritorialStatus priority-ordered derivation; ProvinceWorldState population | 2d | RS-attractor-assign |
| RS-block-irregularity | `BlockIrregularity::from_age_and_archetype()` + integration into district loop | 1d | RS-district-mix |
| RS-tile-conditions | Threshold cache with invalidation for tile conditions (L4-Q1) | 1.5d | RS-district-mix |
### Tier 5 — Background + UI (Depends on Tier 4)
| Ticket | Work | Effort | Depends on |
|--------|------|--------|------------|
| RS-bg-queue | `GenerationQueue` + rayon thread pool + priority ordering | 2d | RS-body-state |
| RS-aho-corasick | `SystemNameIndex` + text scanning → generation trigger | 1d | RS-bg-queue |
| UI-province-bounds | Godot planetary map: load `atlas_province_boundaries`; render as natural polylines | 2d | PY-province-bounds |
### Critical Path
```
SCHEMA-heightmaps
→ PY-heightmap-import
→ RS-heightmap-load
→ RS-body-state
→ RS-drainage
→ RS-attractor-assign
→ RS-district-mix
→ RS-tile-conditions
```
Critical path effort: 0.25 + 1 + 0.5 + 1.5 + 3 + 4 + 3 + 1.5 = **14.75 dev-days**
Total effort (all tiers, parallel where possible): **~33 dev-days**
Parallel acceleration: Tier 0 + Tier 1 + Tier 2 can all run simultaneously. On a two-agent split (Python team / Rust team), calendar time narrows to ~20 days.
---
## 8. Open Items Not Resolved in This Round
### Q4 — Port/Station as Special City Type
Not addressed. Orbital stations lack terrain; no geographic attractors; no drainage. Technical sketch: `SpatialArchetype::SpaceStation` as a sixth variant, bypassing Layer 1 entirely. `body_id` points to orbital body. District mix: no Agricultural; Logistics and Industrial dominant. Defer to lead for whether this needs a Round 4 or a separate ticket.
### Contested + OrganicGrowth Archetype Modifiers
My proposed modifiers (see Q6 above) are proposals, not locked. Lead or Burnelli-Sheldon should confirm or adjust before `RS-district-mix` is implemented.
### Province Watershed Algorithm Detail
I've specified that `generate_atlas.py` adds watershed extraction. The exact algorithm (D8 ridgeline detection, smoothing kernel, UV coordinate normalization) needs a concrete spec before `PY-province-bounds` is assigned. Recommend a brief technical sidequest with Tyre or a consultant review before that ticket starts.
---
## 9. Items Confirmed — No Further Discussion Required
Carried forward from Rounds 1–2, not reopened:
- **SeedChain (FNV-1a)** — D-010; unchanged
- **D8 drainage routing** — locked; ARCH-1 accepted by all agents
- **Scatter deferred** — lead decision; `spawn_category: Option<PropCategory>` hook preserved 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.43–0.63 = Worn, 0.23–0.43 = Cracked, < 0.23 = Broken; Paula's offsets prevent boundary oscillation
- **prosperity_delta = derived, never stored** — confirmed
- **Self-contained district generation** — no cross-city queries; lead requirement
- **L4-Q1: threshold-crossing cache invalidation** for tile conditions — ticket RS-tile-conditions
- **L4-Q4: pre-fetch two ring cells ahead** of player movement — confirmed; handled in chunk_streaming.rs after RS-body-state lands
- **Area = atlas layer, not navigation tier** — confirmed
- **Background generation budget** — 700ms session start; ~143ms on-demand; within budget at 4.9× headroom
---
*Round 3 complete from Tyre's side. Nine D-record candidates produced. Implementation dependency chain: 33 dev-days total, 14.75 critical path. WorldTier enum bug fix (BUG-WorldTier) is the hard prerequisite blocker — no generation ticket can land without it.*