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>
32 KiB
title, description, type, status, workshop, agent, round, created
| title | description | type | status | workshop | agent | round | created |
|---|---|---|---|---|---|---|---|
| Round 3 — Burnelli-Sheldon: Convergence and Economics D-Records | 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. | workshop | active | planet-down-cascade | burnelli-sheldon | 3 | 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:
- Population tier guarantees — mandatory district types by settlement size
- Economic role multiplier table — integer weights distributing remaining slots
- 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
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
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 generationprosperity_current: f32— runtime simulation state, updated by economic simprosperity_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.
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:
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.4for ExtractiveZone: this maps to "at least one corp with health_metric > 0.4 operating an extraction commodity in this Province." Correct.infrastructure_quality > 0.6for 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.5for 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_scorederivation formula (§7 above) - Confirm
atlas_city_namesschema 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
DistrictSkeletonor whetherdensity_pct+perimeter_treatment+prosperity_baselineare 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 ishealth_metric = 0.0the correct proxy? The schema has both; which is authoritative?
Data quality action items (not blocking workshop, but blocking Phase 3 implementation):
- Populate
bodies.founding_age_yearsacross all inhabited bodies (wiki data exists for settlement dates) - Populate
system_economy.economic_tieranddistribution_indexfor all systems (or confirm fallback derivation is sufficient) - Normalize
economic_rolevalues in bodies table (add migration to import_economics.py)