--- 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 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, 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)