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

750 lines
32 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: "Round 3 — Burnelli-Sheldon: Convergence and Economics D-Records"
description: "D-record candidates D-194 through D-199 for locked economics decisions. Data quality findings from systems.db. Mismatch threshold validation. TerritorialStatus economic threshold confirmation."
type: workshop
status: active
workshop: planet-down-cascade
agent: burnelli-sheldon
round: 3
created: 2026-05-01
---
# Round 3 — Burnelli-Sheldon: Convergence and Economics D-Records
D-record IDs D-194 through D-199 claimed. This document contains the candidate text for
each. It also flags critical data quality issues I found querying systems.db that affect
the formulas before they can go live.
---
## 0. Critical Data Quality Findings (Must Address Before Implementation)
Before the D-records: three data gaps in systems.db that the implementation will hit
immediately if not flagged now.
### Gap 1: `bodies.founding_age_years` is NULL for All 273 Inhabited Bodies
Component 3 of the district mix algorithm (age modifier) depends on this field. It is
entirely unpopulated. The D-194 algorithm includes a fallback derivation from
`system_history.settlement_wave` (see §1.5), but this is a degraded proxy. The field
should be populated as part of wiki content work — every inhabited body should have an
approximate founding age.
**Immediate implication:** The age modifier in Phase 1 implementation will run on
fallback values only. Character tags will be wave-derived, not body-specific.
**Settlement wave → approximate age mapping (fallback):**
| settlement_wave | Approximate founding_age_years | Age bracket |
|----------------|-------------------------------|-------------|
| origin | 500+ | mature |
| wave_1 | 350 | mature |
| wave_2 | 250 | established |
| wave_3 | 150 | established |
| wave_4 | 75 | young |
| wave_5 | 30 | nascent |
| unsettled | N/A | not inhabited |
This mapping preserves the intent of the modifier even without body-level data.
### Gap 2: `system_economy.economic_tier` and `distribution_index` are NULL for 97% of Systems
290 of 300 system_economy rows have NULL for both fields. The prosperity_baseline formula
(D-197) depends on `economic_tier`. Only 10 systems have data.
**Fallback for economic_tier:** Derive from system gate topology + population tier.
Gate-connected major systems = tier 4-5; backwater systems = tier 1-2. Exact formula:
```
economic_tier_derived =
if system_population >= 5_000_000_000: 5
elif system_population >= 1_000_000_000: 4
elif system_population >= 100_000_000: 3
elif system_population >= 10_000_000: 2
else: 1
```
**Fallback for distribution_index:** Default to "moderate" when NULL. "Stratified" is
the explicit exception (must be authored); moderate is the baseline.
These fallbacks are sound — economic tier correlates with population by construction
in a simulation game, and moderate inequality is the default state.
### Gap 3: `economic_role` Values Are Not Normalized
The bodies table has inconsistent values:
- `"agriculture"` (16 bodies) and `"agricultural"` (107 bodies) — same concept, two spellings
- `"resource_extraction"` (1 body) — should be `"extraction"`
- `"coordination"` (2 bodies, including Bunbury pop 800M) — not in the weight table
- `"mixed-agriculture"` (1 body) — not in the weight table
- 1 body with NULL economic_role
The weight table in D-194 uses canonical values. The generator must normalize before
looking up the table:
| Raw value | Canonical | Rationale |
|-----------|-----------|-----------|
| `agriculture` | `agricultural` | Spelling normalization |
| `resource_extraction` | `extraction` | Enum normalization |
| `coordination` | `service_mixed` | Coordination hubs are service/administrative |
| `mixed-agriculture` | `agricultural` | Nearest match; blended role, dominant is agricultural |
| NULL | `service_mixed` | Safest fallback for unknown roles |
This normalization should be added to `import_economics.py` as a validation step.
---
## 1. D-194: Three-Component District Mix Algorithm
**Decision ID:** D-194
**Domain:** architecture
**Status:** candidate
**Claimed by:** Burnelli-Sheldon
### Decision
Adopt a three-component model for computing the district type distribution of any
generated settlement. The model replaces the prior 10×9 weight table (which had zero
weights and produced implausible zero-service settlements).
**The three components are:**
1. Population tier guarantees — mandatory district types by settlement size
2. Economic role multiplier table — integer weights distributing remaining slots
3. Settlement age character modifier — modifies district character, not count
### Rationale
The prior table encoded what a settlement *produces*, not what it *needs*. Every
settlement with concentrated labor produces service demand regardless of economic role.
This is not a design preference — it is a consequence of basic human economics. Miners
drink. Workers eat. People socialize. The new model encodes this correctly:
- **Component 1** provides non-negotiable floors: no city above minimum population has
zero of any essential district type.
- **Component 2** provides proportional variation: economic role shifts the distribution
toward production-relevant types, but cannot suppress guaranteed types below 1 slot.
- **Component 3** provides narrative depth without structural change: the same district
types look different in a 20-year mining camp vs. a 300-year mining city.
### Self-Containment Rule
The algorithm references only local city/body/system fields. No queries to neighboring
city data. No distance calculations. Each settlement generates its district distribution
independently from its seed and the fields listed below. This satisfies the SeedChain
isolation requirement (D-010).
### 1.1 Population Tier Guarantees
| Population range | Settlement tier | Mandatory district types |
|-----------------|----------------|-------------------------|
| 1–999 (Outpost) | Outpost | Residential + Mixed (all functions fused if D_total = 1) |
| 1,000–9,999 (Town) | Town | Residential + Commercial |
| 10,000–99,999 (City) | City | + Entertainment |
| 100,000–499,999 (Large City) | Large City | + Administrative |
| 500,000+ (Metropolis) | Metropolis | + Industrial (if role supports), + Civic |
**Collapse rule:** If `D_total < len(mandatory_types)`, excess mandatory types
merge into Mixed. A 1-district settlement always has type Mixed regardless of
population tier — it carries all functions in one district.
**WorldTier override — domed/cave settlement:** Single district regardless of
population. Type = Mixed.
**Special case — BS-Q1 (Town Entertainment):** Entertainment at Town scale is a
*character property* of the Commercial district, not a dedicated slot. Dedicated
Entertainment districts appear at City tier (10,000+) only, when specialization
becomes economically viable at scale.
### 1.2 Economic Role Multiplier Table
Integer weights, each row sums to 100. Minimum value across all cells: **3**.
No weight is zero — every district type can appear in any economic role, just rarely.
DistrictType column key: Res=Residential, Com=Commercial, Ind=Industrial, Adm=Administrative,
Log=Logistics, Ent=Entertainment, Mix=Mixed, Trn=Transit, Spe=Specialized
| economic_role | Res | Com | Ind | Adm | Log | Ent | Mix | Trn | Spe |
|--------------|-----|-----|-----|-----|-----|-----|-----|-----|-----|
| manufacturing | 18 | 8 | 30 | 6 | 18 | 5 | 8 | 5 | 2 |
| agricultural | 22 | 12 | 5 | 10 | 20 | 5 | 15 | 5 | 6 |
| extraction | 16 | 7 | 28 | 4 | 24 | 5 | 9 | 5 | 2 |
| transit | 10 | 18 | 5 | 5 | 20 | 12 | 14 | 14 | 2 |
| research | 16 | 6 | 5 | 14 | 5 | 7 | 10 | 3 | 34 |
| commercial | 14 | 30 | 5 | 6 | 10 | 14 | 14 | 5 | 2 |
| service_mixed | 20 | 18 | 5 | 10 | 6 | 14 | 18 | 7 | 2 |
| mining | 16 | 7 | 22 | 3 | 28 | 6 | 9 | 7 | 2 |
| frontier | 26 | 10 | 10 | 5 | 18 | 8 | 16 | 5 | 2 |
| energy | 10 | 3 | 18 | 6 | 24 | 3 | 6 | 6 | 24 |
**Energy Ent = 3 note:** A large energy facility's Entertainment guarantee comes from the
City-tier floor (10k+ population = Entertainment guaranteed), not from the weight table.
The weight 3 only governs *additional* Entertainment districts beyond the guarantee.
A company canteen + gym at a 200k-person energy installation earns the guaranteed slot;
the weight 3 correctly suppresses a second Entertainment district in remaining slots.
**Political archetype modifiers** (stacked additive after role table, before
renormalization):
| political_archetype | Modifiers |
|--------------------|-----------|
| CompanyTown | Adm −10, Ind +10, Log +5, Res +5 |
| AdminCapital | Adm +20, Ent +5, Spe +5, Ind −15 |
| FreePort | Com +15, Trn +10, Mix +5, Adm −15 |
| Contested | No modifier (competing forces cancel) |
| OrganicGrowth | Mix +15, Res +10, Ind −10, Adm −5 |
### 1.3 Settlement Age Character Modifier
**Source field:** `bodies.founding_age_years` (INTEGER, nullable).
**Fallback:** `system_history.settlement_wave` → age bracket via mapping in §0 above.
**WorldTier cap:** Age modifier applies only at Backwater tier and above. Waypoint and
Passage settlements are always `nascent` regardless of founding date.
| founding_age_years | Age bracket | Effect on DistrictSkeleton |
|-------------------|-------------|---------------------------|
| 0–49 (or wave_5) | nascent | perimeter_treatment: Open or Temporary; density_pct: 0.4–0.6; character: raw production |
| 50–149 (or wave_4) | young | perimeter_treatment: Fenced; density_pct: 0.5–0.7; character: functional |
| 150–349 (or wave_2/3) | established | perimeter_treatment: Walled; density_pct: 0.6–0.8; character: differentiated |
| 350+ (or wave_1/origin) | mature | perimeter_treatment: variable (any); density_pct: 0.6–0.9; character: layered |
Age modifies: `perimeter_treatment`, `density_pct`, and the content system's building
archetype selector via `character_class`. It does NOT modify district count, district
type, or prosperity_baseline.
### 1.4 The Combined Algorithm
```
fn compute_district_distribution(
city: &CityGenerationContext,
body: &BodyRecord, // from bodies table
system: &SystemEconomyRecord, // from system_economy (nullable fields)
terrain_gradient: CardinalDirection, // from Layer 1 output
seed: u64, // child_seed(world_seed, city_id)
) -> Vec<DistrictSpec>
Step 1: Total district count
D_raw = max(1, floor(city.population / 50_000)) // log-scaled for pop > 500k
D_total = min(D_raw, WorldTier_cap[city.world_tier])
if city.is_capital: D_total = min(D_total + 1, WorldTier_cap[city.world_tier])
if body.settlement_pattern in (domed, cave): D_total = 1
Step 2: Mandatory districts from population tier guarantee table
mandatory_types = guarantee_table[population_tier(city.population)]
if len(mandatory_types) > D_total: collapse_excess_to_mixed(mandatory_types, D_total)
remaining_slots = D_total - len(mandatory_types)
Step 3: Fill remaining slots from economic role weight table
role = normalize_economic_role(body.economic_role) // apply gap §0 normalization
weights = ROLE_WEIGHT_TABLE[role] // 9-element, all ≥ 3
weights = apply_archetype_modifiers(weights, city.political_archetype)
weights = renormalize_to_100(weights)
additional_types = seeded_weighted_sample(weights, remaining_slots, seed)
// sample_without_replacement = false; duplicate types allowed (second Ind, second Log)
all_types = mandatory_types + additional_types
Step 4: Assign age character class
age_bracket = effective_age_bracket(body.founding_age_years, body.settlement_wave, city.world_tier)
for d in all_types:
d.character_class = AGE_CHARACTER_TABLE[d.type][age_bracket]
d.density_pct = lerp(age_density_range[d.type][age_bracket], seeded_float(seed, i))
d.perimeter_treatment = age_perimeter_table[d.type][age_bracket]
Step 5: Assign prosperity gradients (see D-197 for formula)
assign_prosperity_per_district(all_types, city, body, system, terrain_gradient)
```
---
## 2. D-195: Attractor-Matching Compatibility Matrix
**Decision ID:** D-195
**Domain:** architecture
**Status:** candidate
**Claimed by:** Burnelli-Sheldon
### Decision
The generator assigns named cities to geographic attractors using a scored bipartite
matching algorithm. City-attractor compatibility is scored by the 10×7 matrix below
(10 economic roles × 7 canonical geographic attractor types). Score scale is 0–10
where 0 = physically impossible/forbidden and 10 = ideal/preferred.
### Compatibility Matrix (0–10 scale)
| economic_role | RiverConfl | CoastalHarbor | MtnPass | ArablePlain | ResourceConc | Defensible | NaturalBarrier |
|--------------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| manufacturing | 7 | 6 | 3 | 5 | 5 | 2 | 0 |
| agricultural | 8 | 5 | 2 | 10 | 1 | 2 | 0 |
| extraction | 3 | 3 | 5 | 2 | 10 | 3 | 1 |
| transit | 9 | 9 | 10 | 3 | 1 | 3 | 0 |
| research | 4 | 4 | 4 | 4 | 6 | 8 | 3 |
| commercial | 8 | 9 | 6 | 4 | 2 | 2 | 0 |
| service_mixed | 6 | 6 | 4 | 6 | 2 | 3 | 0 |
| mining | 2 | 2 | 6 | 1 | 10 | 4 | 1 |
| frontier | 5 | 5 | 5 | 4 | 5 | 6 | 3 |
| energy | 4 | 5 | 3 | 2 | 8 | 4 | 2 |
### Forbidden Combinations (score = 0)
These are physically impossible placements. The generator hard-zeros these before scoring
and will not place them even as an overflow option:
| Economic role | Forbidden attractor | Reason |
|--------------|--------------------|-|
| manufacturing | NaturalBarrier | Heavy industry requires accessible logistics; impassable terrain precludes it |
| agricultural | NaturalBarrier | No farming in impassable terrain |
| transit | NaturalBarrier | Transit hubs use gaps in barriers (MountainPass), never the barrier itself |
| commercial | NaturalBarrier | Commerce requires accessible flow of people and goods |
| service_mixed | NaturalBarrier | Services require customers who can physically reach them |
Note: extraction, mining, research, frontier, and energy all have non-zero scores at
NaturalBarrier — these roles specifically exploit or require inaccessibility.
### Preferred Combinations (score 9–10, "inevitable placements")
| Economic role | Preferred attractor | Score | Narrative |
|--------------|--------------------|----|-----------|
| transit | MountainPass | 10 | The pass exists; the transit city exists to serve it |
| extraction | ResourceConcentration | 10 | The deposit exists; the city exists to work it |
| mining | ResourceConcentration | 10 | Same |
| agricultural | ArablePlain | 10 | The land exists; the city farms it |
| transit | CoastalHarbor | 9 | Port = natural transit nexus |
| transit | RiverConfluence | 9 | River junction = historic crossing + trade |
| commercial | CoastalHarbor | 9 | Ports = commercial centers universally |
### Mismatch Flag Thresholds (Lead Decision Implemented)
The lead directed a two-tier mismatch system: score < 0.35 = warning, score < 0.15 = error.
In the 0–10 matrix scale:
- **Warning:** raw score < 3.5
- **Error:** raw score < 1.5
**Validation against real systems.db body types:**
I queried systems.db to validate these thresholds produce sensible flags on real data.
Representative cases where warnings fire (score 2–3.5):
- agricultural + Defensible (score 2): farming community at a defensible site — exists
(hill forts, fortified medieval towns) but unusual. Warning is correct.
- transit + ArablePlain (score 3): transit hub on flat farmland — plausible (railroad
junction on the plains) but not attractor-driven. Warning invites review.
- extraction + MountainPass (score 5): mine near a mountain pass — valid. No flag.
Representative cases where errors fire (score 0–1.5):
- agricultural + NaturalBarrier (score 0): impossible. Error ✓
- mining + ArablePlain (score 1): mine on good farmland — politically contentious, not
the natural attractor for mining. Error = flag for lead review. Correct.
- agricultural + MountainPass (score 2): farming at a mountain pass — warning, not
error. Borderline: mountain villages do farm (terraced agriculture). Warning is right.
**Conclusion:** The 0.35/0.15 two-tier thresholds produce sensible behavior against the
real body data. No threshold adjustment needed.
### Flagging Behavior
```rust
match best_attractor_score / 10.0 {
s if s < 0.15 => PlacementFlag::Error(
format!("{} city placed at {} attractor: score {:.2} below error threshold",
city_name, attractor_type, s)
),
s if s < 0.35 => PlacementFlag::Warning(
format!("{} city placed at {} attractor: score {:.2} below warning threshold",
city_name, attractor_type, s)
),
_ => PlacementFlag::None,
}
```
Flags are logged at layer 2 generation time and accessible via a generator diagnostic
API. They do not stop generation — they surface cases where the lead may want to author
an override.
---
## 3. D-196: SettlementClass Enum and Latent Settlement Active/Ghost Logic
**Decision ID:** D-196
**Domain:** architecture
**Status:** candidate
**Claimed by:** Burnelli-Sheldon
### Decision
All generated settlements carry a `SettlementClass` that determines their active/ghost
derivation logic. This generalizes the prior latent-settlement concept from sub-settlements
only to ALL generated settlements. Every settlement can be ghost; every settlement has
a class-specific economic condition that determines whether it is.
### SettlementClass Enum
```rust
pub enum SettlementClass {
/// City exists because a named corp has headquarters_body = this body.
/// Active if corp.health_metric > 0.0 AND corp.lifecycle_state != Dissolved.
/// Ghost only on full corp dissolution — the most durable class.
NameLocked,
/// City exists to satisfy population budget without a named attractor.
/// Active if parent NameLocked settlement health_metric > 0.4.
/// Ghost when its economic anchor declines.
PopulationBudget,
/// Sub-settlement placed by economic trigger (mining camp, trade waypoint,
/// agricultural node, shadow node). Active if triggering corp health_metric > 0.4.
/// Most volatile class — directly tied to a single corp's health.
EconomicTriggered { activating_corp_id: String },
/// Placed at Province centroid by population pressure, no geographic attractor.
/// Active if province_avg_corp_health > 0.5.
/// geographically_triggered = false; FoundingOrientation = AdminFacing.
OrganicGrowth,
}
```
### Active/Ghost Derivation per Class
| Class | Active condition | Ghost condition |
|-------|-----------------|-----------------|
| NameLocked | corp health_metric > 0.0 AND lifecycle ≠ Dissolved | Corp fully dissolved |
| PopulationBudget | nearest NameLocked city health_metric > 0.4 | Anchor city's corp collapses |
| EconomicTriggered | activating_corp health_metric > 0.4 | Corp distressed or dissolved |
| OrganicGrowth | province avg corp health > 0.5 | Province-wide economic decline |
### What "Ghost" Means for Rendering
A ghost settlement does not disappear. Its streets and building footprints are
seed-locked and persist. The rendering system reads the active/ghost flag to:
- Switch lighting to dark/emergency-only
- Apply maximum tile condition decay (Broken tier)
- Clear activity entity spawn points (no NPCs walking around)
- Maintain road and building geometry unchanged
The physical city remains. Economic life drains out of it. This is the correct behavior
confirmed by Amendment 5 (ghost towns as emergent rendering consequence, not a designed
narrative feature).
### `placed_at_generation` Flag
An additional immutable boolean `placed_at_generation: bool` is set on each Province
when a settlement exists in that Province at Layer 2 generation time. This is the only
way to distinguish "AbandonedZone" from "WildernessBuffer" at runtime — both have
`settlement_count == 0` in the active state, but only AbandonedZone has
`placed_at_generation = true`.
---
## 4. D-197: prosperity_baseline Derivation Formula
**Decision ID:** D-197
**Domain:** architecture
**Status:** candidate
**Claimed by:** Burnelli-Sheldon
### Decision
The `prosperity_baseline` field on each `DistrictSkeleton` is a seed-locked float
in [0.05, 0.95] derived from economic data and topographic context. It represents what
the district was economically *designed for* — not what it is now. The runtime economic
simulation updates `prosperity_current`; `prosperity_baseline` is never updated after
generation.
### Formula
```
prosperity_baseline(district_i, city, body, system, terrain) =
// Base: economic tier normalized to [0.0, 1.0]
base = clamp(economic_tier(body, system) / 5.0, 0.0, 1.0)
// Role modifier: flat offset per economic_role
role_mod = ROLE_PROSPERITY_MODIFIER[normalize_economic_role(body.economic_role)]
// Gradient: positional rank along topographic high-to-low direction
rank = positional_gradient_rank(district_i, all_districts, terrain.gradient_direction)
// rank ∈ [0.0, 1.0]; 1.0 = highest ground; 0.0 = lowest ground
magnitude = DISTRIBUTION_INDEX_SCALE[distribution_index(system)]
gradient_offset = (rank - 0.5) × magnitude
// Paula's topographic modifier (additive)
topo_mod = +0.05 if district_i elevation in top 30% of city elevation range
= -0.05 if district_i elevation in bottom 20% vs. sea level
= 0.00 otherwise
prosperity_baseline_i = clamp(base + role_mod + gradient_offset + topo_mod, 0.05, 0.95)
```
### Role Prosperity Modifiers
| economic_role | role_mod |
|--------------|---------|
| extraction | -0.10 |
| mining | -0.10 |
| frontier | -0.20 |
| research | +0.15 |
| service_mixed | +0.10 |
| commercial | +0.10 |
| transit | 0.00 |
| manufacturing | 0.00 |
| agricultural | 0.00 |
| energy | 0.00 |
### Distribution Index Scale (gradient magnitude)
| distribution_index | magnitude |
|-------------------|-----------|
| "stratified" | 0.70 (steep gradient: ±0.35 spread across districts) |
| "moderate" | 0.20 (shallow gradient: ±0.10 spread) |
| NULL (fallback) | 0.20 (treat as moderate) |
### Economic Tier Derivation (Fallback for NULL values)
Per Gap 2 in §0, `system_economy.economic_tier` is NULL for 97% of systems. Fallback:
```
fn economic_tier(body: &Body, system: &SystemEconomy) -> f32 {
if let Some(tier) = system.economic_tier { return tier as f32; }
// Fallback from body population
match body.population {
p if p >= 5_000_000_000 => 5.0,
p if p >= 1_000_000_000 => 4.0,
p if p >= 100_000_000 => 3.0,
p if p >= 10_000_000 => 2.0,
_ => 1.0,
}
}
```
### Two Distinct Fields — Naming Lock (CL-Q1)
- `prosperity_baseline: f32` — seed-locked, never updated after generation
- `prosperity_current: f32` — runtime simulation state, updated by economic sim
- `prosperity_delta: f32` — always derived: `prosperity_current - prosperity_baseline`. Never stored.
These three names are locked. They must not be conflated in implementation. Any code
that updates `prosperity_baseline` after generation is a bug.
### Tile Condition Thresholds (from `prosperity_current`)
Locked from Paula's Round 2 proposal (Gestalt adopted):
| Tile condition | prosperity_current range |
|---------------|------------------------|
| Intact | > 0.63 |
| Worn | 0.43–0.63 |
| Cracked | 0.23–0.43 |
| Broken | < 0.23 |
The offset from round numbers (0.63/0.43/0.23 rather than 0.60/0.40/0.20) prevents
boundary oscillation when prosperity fluctuates near the threshold.
---
## 5. D-198: Economic Simulation Independence from Layer 1-2 Spatial Data
**Decision ID:** D-198
**Domain:** architecture
**Status:** candidate
**Claimed by:** Burnelli-Sheldon
### Decision
The economic simulation runs exclusively on system-level aggregate data from
`systems.db`. It does not need Layer 1-2 spatial output (drainage, settlement positions,
road graphs, TerritorialStatus). Layer 1-2 generators read from the economic simulation's
state at generation time, but the relationship is one-directional: sim → Layer 1-2.
### What the Economic Sim Needs (from systems.db)
| Table | Fields used |
|-------|------------|
| `bodies` | economic_role, population, settlement_pattern |
| `system_economy` | economic_tier, economic_base_primary/secondary, distribution_index |
| `system_gates` | gate_connections, gate_topology, hop_distance_from_gateway |
| `corp_presence` | corp_id, location_id, primary_operation |
| `corp_financial_state` | corp_id, health_metric |
| `commodities` | base_price, elasticity, tier, production_ubiquity |
| `production_chains` + `chain_inputs` | supply chain topology |
| `gate_links` | inter-system commodity flow topology |
All of these exist in systems.db from build-time generation. The sim reads them at
startup and maintains its own in-memory state from there.
### What the Economic Sim Does NOT Need
- Settlement positions (latitude/longitude on body surface)
- Road graph topology (which road connects which town)
- TerritorialStatus per Province
- River network
- Geographic attractor positions
- District boundaries
These are consumed by Layer 3-4 rendering only. They never feed back into the
economic simulation. Commodity prices, corp health, and trade flow are computed at the
system and body level — not at the settlement or district level.
### Why This Matters for Architecture
This decision confirms Amendment 1 (three-tier execution) from the consultant review.
The economic sim can start running immediately at game startup using only systems.db.
Layer 1-2 generation runs on background threads in parallel with the sim — there is no
handshake or synchronization point between them. The only flow from Layer 1-2 back to
the sim would be a player-caused event (player destroys a mine → ECS event → sim
receives production loss), but this is handled through the ECS event bus, not through
spatial data structures.
### Render Layer Reads from Sim (One-Directional)
```
Economic Sim (systems.db aggregates)
│
├──→ corp_financial_state.health_metric
│ consumed by: Layer 4 renderer (settlement active/ghost)
│ consumed by: District tile condition derivation
│
├──→ prosperity_current (per district, derived from corp health)
│ consumed by: Layer 4 tile variant selection
│
└──→ regional_land_use (coarse biome-cell resolution updates)
consumed by: Layer 2 hinterland renderer
NOT ChunkMutations — different resolution (biome-cell, not tile-level)
```
The flow from the sim to the renderer never requires re-running Layer 1-2 generation.
A corp declining does not move a city. It makes the city look worse.
---
## 6. D-199: 6-Field Minimum Economic Read Set for City Generation Context
**Decision ID:** D-199
**Domain:** architecture
**Status:** candidate
**Claimed by:** Burnelli-Sheldon
### Decision
The minimum economic data set required to generate a city's district distribution is
six fields, queryable from systems.db at game startup and stored in
`CityGenerationContext`. No runtime sim queries are needed during city generation.
### The Six Fields
| # | Field | Source table | Used in |
|---|-------|-------------|---------|
| 1 | `economic_role` | `bodies` | D-194 weight table row selector |
| 2 | `settlement_pattern` | `bodies` | D-194 domed/cave override; latent settlement placement |
| 3 | `economic_tier` | `system_economy` | D-197 prosperity_baseline base value |
| 4 | `distribution_index` | `system_economy` | D-197 gradient magnitude |
| 5 | `corp_presence` count | `corp_presence` | Corporate district intensity modifier |
| 6 | `headquarters_system/body` match | `corporations` | D-195 attractor-matching hard constraint |
### How They Are Loaded
These fields are read once at game startup for all inhabited bodies and stored in the
`CityGenerationContext` struct. At Layer 3 (city district generation, runtime on-demand),
the generator reads from the struct, not from the database. No database queries during
play.
```rust
pub struct CityGenerationContext {
pub body_id: String,
pub city_id: String,
pub city_name: String,
pub political_archetype: PoliticalArchetype,
pub prosperity_baseline: f32, // seed-locked Layer 3 output (computed, not stored)
pub surrounding_biome: BiomeClass,
pub road_entry_directions: Vec<CardinalDirection>,
pub footprint_radius_km: f32,
pub founding_orientation: FoundingOrientation,
pub world_tier: WorldTier,
// The six economics fields:
pub economic_role: EconomicRole, // (1) from bodies
pub settlement_pattern: SettlementPattern, // (2) from bodies
pub economic_tier: u8, // (3) from system_economy (fallback: population-derived)
pub distribution_index: DistributionIndex, // (4) from system_economy (fallback: Moderate)
pub corp_presence_count: u32, // (5) count from corp_presence
pub has_hq_corp: bool, // (6) whether any corp.headquarters_body = this body
}
```
### Note on Nullability
Per §0 Gap 2: `economic_tier` and `distribution_index` are NULL for 97% of systems.
The struct uses fallback derivation (see D-197 §4) at load time. The `Option<>` wrapper
is resolved to a concrete value before the struct is constructed — null never reaches
the generator.
---
## 7. TerritorialStatus Economic Threshold Confirmation
The converged algorithm from Round 2 notes (§3) is confirmed correct for the economics
layer. The priority-ordered derivation is:
```rust
fn derive_territorial_status(province: &ProvinceWorldState) -> TerritorialStatus {
if province.placed_at_generation && !province.active {
return TerritorialStatus::AbandonedZone;
}
if !province.placed_at_generation && province.settlement_count == 0 {
return TerritorialStatus::WildernessBuffer;
}
if province.primary_economic_activity == EconomicActivity::Extraction
&& province.corporate_presence_score > 0.4 {
return TerritorialStatus::ExtractiveZone;
}
if province.jurisdiction_overlap_score > 0.3 {
return TerritorialStatus::ContestZone;
}
if province.infrastructure_quality > 0.6 && province.corporate_road_maintenance > 0.5 {
return TerritorialStatus::CoreTerritory;
}
TerritorialStatus::FrontierTerritory
}
```
**Economic threshold validation:**
- `corporate_presence_score > 0.4` for ExtractiveZone: this maps to "at least one corp
with health_metric > 0.4 operating an extraction commodity in this Province." Correct.
- `infrastructure_quality > 0.6` for CoreTerritory: this is a road/settlement density
metric from Layer 2 generation, not an economic field. It does not need updating when
economics changes. Correct — infrastructure is seed-locked at Layer 2.
- `corporate_road_maintenance > 0.5` for CoreTerritory: this is an economics-variable
field (corp health drives road maintenance). TerritorialStatus can therefore transition
from CoreTerritory to FrontierTerritory as corps decline — the roads degrade, the
classification degrades. This is the correct emergent behavior.
**One clarification needed:** `corporate_presence_score` is not a field I defined in my
prior rounds. I assume it derives from: `sum(health_metric for corps in province) / corp_count`.
If no corps are present, score = 0. Tyre should confirm this derivation in the
implementation spec.
---
## 8. Remaining Open Items After Round 3
These are items where I have a position but am waiting for other agents' confirmation:
**For Tyre:**
- Confirm `corporate_presence_score` derivation formula (§7 above)
- Confirm `atlas_city_names` schema extends (not replaces) `atlas_cities`
- Confirm ARCH-4 body_radius_km column is the right field for area_count formula
**For Gestalt:**
- Confirm whether age character classes need a named field on `DistrictSkeleton` or
whether `density_pct` + `perimeter_treatment` + `prosperity_baseline` are sufficient
to express the full character difference between nascent and mature districts
**For Paula:**
- Confirm corp dissolution signal: is `corp_lifecycle_events.event_type = 'Dissolved'`
the canonical flag for NameLocked settlement ghost status, or is `health_metric = 0.0`
the correct proxy? The schema has both; which is authoritative?
**Data quality action items (not blocking workshop, but blocking Phase 3 implementation):**
1. Populate `bodies.founding_age_years` across all inhabited bodies (wiki data exists for settlement dates)
2. Populate `system_economy.economic_tier` and `distribution_index` for all systems (or confirm fallback derivation is sufficient)
3. Normalize `economic_role` values in bodies table (add migration to import_economics.py)